mirror of
https://github.com/pgsty/minio.git
synced 2026-07-31 07:45:19 +03:00
Compare commits
619 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 |
@@ -26,8 +26,8 @@
|
||||
|
||||
## Your Environment
|
||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||
* Version used:
|
||||
* Version used (`minio version`):
|
||||
* Environment name and version (e.g. nginx 1.9.1):
|
||||
* Server type and version:
|
||||
* Operating System and version:
|
||||
* Operating System and version (`uname -a`):
|
||||
* Link to your project:
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
- [ ] 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 tests to cover my changes.
|
||||
- [ ] 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
|
||||
*.syso
|
||||
coverage.txt
|
||||
.vscode/
|
||||
.snap
|
||||
*.tar.bz2
|
||||
parts/
|
||||
prime/
|
||||
snap/.snapcraft/
|
||||
stage/
|
||||
|
||||
+8
-3
@@ -1,6 +1,9 @@
|
||||
go_import_path: github.com/minio/minio
|
||||
sudo: required
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
dist: trusty
|
||||
|
||||
language: go
|
||||
@@ -10,15 +13,17 @@ os:
|
||||
|
||||
env:
|
||||
- ARCH=x86_64
|
||||
- ARCH=i686
|
||||
|
||||
script:
|
||||
## Run all the tests
|
||||
- make
|
||||
- make test GOFLAGS="-timeout 20m -race -v"
|
||||
- diff -au <(gofmt -d cmd) <(printf "")
|
||||
- diff -au <(gofmt -d pkg) <(printf "")
|
||||
- make test GOFLAGS="-timeout 15m -race -v"
|
||||
- make coverage
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
go:
|
||||
- 1.7.4
|
||||
- 1.9.1
|
||||
|
||||
+49
-49
@@ -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](https://docs.minio.io/docs/how-to-install-golang).
|
||||
``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
|
||||
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
|
||||
$ mkdir -p $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
|
||||
```
|
||||
|
||||
### Compiling Minio from source
|
||||
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``
|
||||
### Set up git remote as ``upstream``
|
||||
```sh
|
||||
$ cd $GOPATH/src/github.com/minio/minio
|
||||
$ git remote add upstream https://github.com/minio/minio
|
||||
$ git fetch upstream
|
||||
$ git merge upstream/master
|
||||
...
|
||||
...
|
||||
$ make
|
||||
Checking if proper environment variables are set.. Done
|
||||
...
|
||||
Checking dependencies for Minio.. Done
|
||||
Installed govet
|
||||
Building Libraries
|
||||
...
|
||||
```
|
||||
|
||||
### Developer Guidelines
|
||||
``Minio`` community welcomes your contribution. To make the process as seamless as possible, we ask for the following:
|
||||
* 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
|
||||
### Create your feature branch
|
||||
Before making code changes, make sure you create a separate branch for these changes
|
||||
|
||||
* If you have additional dependencies for ``Minio``, ``Minio`` manages its dependencies using [govendor](https://github.com/kardianos/govendor)
|
||||
- Run `go get foo/bar`
|
||||
- Edit your code to import foo/bar
|
||||
- Run `make pkg-add PKG=foo/bar` from top-level directory
|
||||
```
|
||||
$ git checkout -b my-new-feature
|
||||
```
|
||||
|
||||
* If you have dependencies for ``Minio`` which needs to be removed
|
||||
- Edit your code to not import foo/bar
|
||||
- Run `make pkg-remove PKG=foo/bar` from top-level directory
|
||||
### Test Minio server changes
|
||||
After your code changes, make sure
|
||||
|
||||
* When you're ready to create a pull request, be sure to:
|
||||
- Have test cases for the new code. If you have questions about how to do it, please ask in your pull request.
|
||||
- Run `make verifiers`
|
||||
- Squash your commits into a single commit. `git rebase -i`. It's okay to force update your pull request.
|
||||
- Make sure `go test -race ./...` and `go build` completes.
|
||||
- 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.
|
||||
- To run `make verifiers`
|
||||
- To 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.
|
||||
|
||||
* Read [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project
|
||||
- `Minio` project is fully conformant with Golang style
|
||||
- if you happen to observe offending code, please feel free to send a pull request
|
||||
### Commit changes
|
||||
After verification, commit your changes. This is a [great post](https://chris.beams.io/posts/git-commit/) on how to write useful commit messages
|
||||
|
||||
```
|
||||
$ 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).
|
||||
|
||||
+25
-11
@@ -1,17 +1,31 @@
|
||||
FROM golang:1.7-alpine
|
||||
FROM golang:1.9.1-alpine3.6
|
||||
|
||||
WORKDIR /go/src/app
|
||||
MAINTAINER Minio Inc <dev@minio.io>
|
||||
|
||||
COPY . /go/src/app
|
||||
ENV GOPATH /go
|
||||
ENV PATH $PATH:$GOPATH/bin
|
||||
ENV CGO_ENABLED 0
|
||||
|
||||
RUN \
|
||||
apk add --no-cache git && \
|
||||
go-wrapper download && \
|
||||
go-wrapper install -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
|
||||
mkdir -p /export/docker && \
|
||||
rm -rf /go/pkg /go/src && \
|
||||
apk del git
|
||||
WORKDIR /go/src/github.com/minio/
|
||||
|
||||
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
|
||||
ENTRYPOINT ["minio"]
|
||||
|
||||
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"]
|
||||
|
||||
@@ -1,146 +1,101 @@
|
||||
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
||||
PWD := $(shell pwd)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
||||
|
||||
BUILD_LDFLAGS := '$(LDFLAGS)'
|
||||
TAG := latest
|
||||
|
||||
HOST ?= $(shell uname)
|
||||
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
|
||||
all: build
|
||||
|
||||
checks:
|
||||
@echo "Checking deps:"
|
||||
@echo "Check deps"
|
||||
@(env bash $(PWD)/buildscripts/checkdeps.sh)
|
||||
@echo "Checking project is in GOPATH"
|
||||
@(env bash $(PWD)/buildscripts/checkgopath.sh)
|
||||
|
||||
getdeps: checks
|
||||
@echo "Installing golint:" && go get -u github.com/golang/lint/golint
|
||||
@echo "Installing gocyclo:" && go get -u github.com/fzipp/gocyclo
|
||||
@echo "Installing deadcode:" && go get -u github.com/remyoudompheng/go-misc/deadcode
|
||||
@echo "Installing misspell:" && go get -u github.com/client9/misspell/cmd/misspell
|
||||
@echo "Installing ineffassign:" && go get -u github.com/gordonklaus/ineffassign
|
||||
@echo "Installing golint" && go get -u github.com/golang/lint/golint
|
||||
@echo "Installing gocyclo" && go get -u github.com/fzipp/gocyclo
|
||||
@echo "Installing deadcode" && go get -u github.com/remyoudompheng/go-misc/deadcode
|
||||
@echo "Installing misspell" && go get -u github.com/client9/misspell/cmd/misspell
|
||||
@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:
|
||||
@echo "Running $@:"
|
||||
@go tool vet -all ./cmd
|
||||
@go tool vet -all ./pkg
|
||||
@go tool vet -shadow=true ./cmd
|
||||
@go tool vet -shadow=true ./pkg
|
||||
@echo "Running $@"
|
||||
@go tool vet -atomic -bool -copylocks -nilfunc -printf -shadow -rangeloops -unreachable -unsafeptr -unusedresult cmd
|
||||
@go tool vet -atomic -bool -copylocks -nilfunc -printf -shadow -rangeloops -unreachable -unsafeptr -unusedresult pkg
|
||||
|
||||
fmt:
|
||||
@echo "Running $@:"
|
||||
@gofmt -s -l cmd
|
||||
@gofmt -s -l pkg
|
||||
@echo "Running $@"
|
||||
@gofmt -d cmd
|
||||
@gofmt -d pkg
|
||||
|
||||
lint:
|
||||
@echo "Running $@:"
|
||||
@echo "Running $@"
|
||||
@${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/cmd...
|
||||
@${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/pkg...
|
||||
|
||||
ineffassign:
|
||||
@echo "Running $@:"
|
||||
@echo "Running $@"
|
||||
@${GOPATH}/bin/ineffassign .
|
||||
|
||||
cyclo:
|
||||
@echo "Running $@:"
|
||||
@echo "Running $@"
|
||||
@${GOPATH}/bin/gocyclo -over 100 cmd
|
||||
@${GOPATH}/bin/gocyclo -over 100 pkg
|
||||
|
||||
build: getdeps verifiers $(UI_ASSETS)
|
||||
|
||||
deadcode:
|
||||
@${GOPATH}/bin/deadcode
|
||||
@echo "Running $@"
|
||||
@${GOPATH}/bin/deadcode -test $(shell go list ./...) || true
|
||||
|
||||
spelling:
|
||||
@${GOPATH}/bin/misspell -error `find cmd/`
|
||||
@${GOPATH}/bin/misspell -error `find pkg/`
|
||||
@${GOPATH}/bin/misspell -error `find docs/`
|
||||
|
||||
test: build
|
||||
@echo "Running all minio testing:"
|
||||
# Builds minio, runs the verifiers then runs the tests.
|
||||
check: test
|
||||
test: verifiers build
|
||||
@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
|
||||
@echo "Running all coverage for minio:"
|
||||
@echo "Running all coverage for minio"
|
||||
@./buildscripts/go-coverage.sh
|
||||
|
||||
gomake-all: build
|
||||
@echo "Installing minio:"
|
||||
@go build --ldflags $(BUILD_LDFLAGS) -o $(GOPATH)/bin/minio
|
||||
# Builds minio locally.
|
||||
build:
|
||||
@echo "Building minio binary: $(PWD)/minio"
|
||||
@CGO_ENABLED=0 go build --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
|
||||
|
||||
pkg-add:
|
||||
${GOPATH}/bin/govendor add $(PKG)
|
||||
@echo "Adding new package $(PKG)"
|
||||
@${GOPATH}/bin/govendor add $(PKG)
|
||||
|
||||
pkg-update:
|
||||
${GOPATH}/bin/govendor update $(PKG)
|
||||
@echo "Updating new package $(PKG)"
|
||||
@${GOPATH}/bin/govendor update $(PKG)
|
||||
|
||||
pkg-remove:
|
||||
${GOPATH}/bin/govendor remove $(PKG)
|
||||
@echo "Remove new package $(PKG)"
|
||||
@${GOPATH}/bin/govendor remove $(PKG)
|
||||
|
||||
pkg-list:
|
||||
@$(GOPATH)/bin/govendor list
|
||||
|
||||
install: gomake-all
|
||||
|
||||
release: verifiers
|
||||
@MINIO_RELEASE=RELEASE ./buildscripts/build.sh
|
||||
|
||||
experimental: verifiers
|
||||
@MINIO_RELEASE=EXPERIMENTAL ./buildscripts/build.sh
|
||||
# Builds minio and installs it to $GOPATH/bin.
|
||||
install: build
|
||||
@echo "Installing minio binary: $(GOPATH)/bin/minio"
|
||||
@cp $(PWD)/minio $(GOPATH)/bin/minio
|
||||
@echo "\nInstallation successful. To learn more, try \"minio --help\"."
|
||||
|
||||
clean:
|
||||
@echo "Cleaning up all the generated files:"
|
||||
@echo "Cleaning up all the generated files"
|
||||
@find . -name '*.test' | xargs rm -fv
|
||||
@rm -rf build
|
||||
@rm -rf release
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# 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)
|
||||
# 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.
|
||||
|
||||
@@ -8,78 +9,90 @@ Minio server is light enough to be bundled with the application stack, similar t
|
||||
### Stable
|
||||
```
|
||||
docker pull minio/minio
|
||||
docker run -p 9000:9000 minio/minio server /export
|
||||
docker run -p 9000:9000 minio/minio server /data
|
||||
```
|
||||
|
||||
### Edge
|
||||
```
|
||||
docker pull minio/minio:edge
|
||||
docker run -p 9000:9000 minio/minio:edge server /export
|
||||
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)
|
||||
|
||||
## OS X
|
||||
## macOS
|
||||
### Homebrew
|
||||
Install minio packages using [Homebrew](http://brew.sh/)
|
||||
|
||||
```sh
|
||||
brew install minio
|
||||
minio server ~/Photos
|
||||
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|
|
||||
| ----------| -------- | ------|
|
||||
|Apple OS X|64-bit Intel|https://dl.minio.io/server/minio/release/darwin-amd64/minio|
|
||||
|Apple macOS|64-bit Intel|https://dl.minio.io/server/minio/release/darwin-amd64/minio |
|
||||
```sh
|
||||
chmod 755 minio
|
||||
./minio server ~/Photos
|
||||
./minio server /data
|
||||
```
|
||||
|
||||
## GNU/Linux
|
||||
### Binary Download
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio|
|
||||
||32-bit Intel|https://dl.minio.io/server/minio/release/linux-386/minio|
|
||||
||32-bit ARM|https://dl.minio.io/server/minio/release/linux-arm/minio|
|
||||
||64-bit ARM|https://dl.minio.io/server/minio/release/linux-arm64/minio|
|
||||
||32-bit ARMv6|https://dl.minio.io/server/minio/release/linux-arm6vl/minio|
|
||||
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio |
|
||||
```sh
|
||||
chmod +x minio
|
||||
./minio server ~/Photos
|
||||
./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|
|
||||
||32-bit|https://dl.minio.io/server/minio/release/windows-386/minio.exe|
|
||||
|Microsoft Windows|64-bit|https://dl.minio.io/server/minio/release/windows-amd64/minio.exe |
|
||||
```sh
|
||||
minio.exe server D:\Photos
|
||||
```
|
||||
|
||||
## FreeBSD
|
||||
### Binary Download
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|FreeBSD|64-bit|https://dl.minio.io/server/minio/release/freebsd-amd64/minio|
|
||||
```sh
|
||||
chmod 755 minio
|
||||
./minio server ~/Photos
|
||||
```
|
||||
### Port
|
||||
Install minio packages using [pkg](https://github.com/freebsd/pkg)
|
||||
|
||||
You can run Minio on FreeBSD with FreeNAS storage-backend - see [here](https://docs.minio.io/docs/how-to-run-minio-in-freenas) for more details.
|
||||
```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).
|
||||
|
||||
```sh
|
||||
|
||||
go get -u github.com/minio/minio
|
||||
|
||||
```
|
||||
|
||||
## Test using Minio Browser
|
||||
|
||||
+4
-9
@@ -1,4 +1,4 @@
|
||||
# Minio 快速入门 [](https://slack.minio.io) [](https://goreportcard.com/report/minio/minio) [](https://codecov.io/gh/minio/minio)
|
||||
# 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不等。
|
||||
|
||||
@@ -9,13 +9,8 @@ Minio是一个非常轻量的服务,可以很简单的和其他应用的结合
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio|
|
||||
||32-bit Intel|https://dl.minio.io/server/minio/release/linux-386/minio|
|
||||
||32-bit ARM|https://dl.minio.io/server/minio/release/linux-arm/minio|
|
||||
||64-bit ARM|https://dl.minio.io/server/minio/release/linux-arm64/minio|
|
||||
||32-bit ARMv6|https://dl.minio.io/server/minio/release/linux-arm6vl/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|
|
||||
||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|
|
||||
|
||||
### Homebrew 安装
|
||||
@@ -47,7 +42,7 @@ $ go get -u github.com/minio/minio
|
||||
|
||||
$ chmod +x minio
|
||||
$ ./minio --help
|
||||
$ ./minio server ~/Photos
|
||||
$ ./minio server /data
|
||||
|
||||
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
@@ -75,7 +70,7 @@ SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
|
||||
$ chmod 755 minio
|
||||
$ ./minio --help
|
||||
$ ./minio server ~/Photos
|
||||
$ ./minio server /data
|
||||
|
||||
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
@@ -139,7 +134,7 @@ $ docker run -p 9000:9000 minio/minio
|
||||
|
||||
$ chmod 755 minio
|
||||
$ ./minio --help
|
||||
$ ./minio server ~/Photos
|
||||
$ ./minio server /data
|
||||
|
||||
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
|
||||
+14
-9
@@ -12,10 +12,11 @@ clone_folder: c:\gopath\src\github.com\minio\minio
|
||||
# Environment variables
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
GOROOT: c:\go
|
||||
|
||||
# scripts that run after cloning repository
|
||||
install:
|
||||
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
|
||||
- set PATH=%GOPATH%\bin;%GOROOT%\bin;%PATH%
|
||||
- go version
|
||||
- go env
|
||||
- python --version
|
||||
@@ -23,6 +24,8 @@ install:
|
||||
# To run your custom scripts instead of automatic MSBuild
|
||||
build_script:
|
||||
# Compile
|
||||
# We need to disable firewall - https://github.com/appveyor/ci/issues/1579#issuecomment-309830648
|
||||
- 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
|
||||
@@ -35,19 +38,21 @@ test_script:
|
||||
# Unit tests
|
||||
- ps: Add-AppveyorTest "Unit Tests" -Outcome Running
|
||||
- mkdir build\coverage
|
||||
- go test -timeout 20m -v -race github.com/minio/minio/cmd...
|
||||
- go test -v -timeout 17m -race github.com/minio/minio/cmd...
|
||||
- go test -v -race github.com/minio/minio/pkg...
|
||||
- go test -timeout 15m -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd
|
||||
# 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:
|
||||
- go tool cover -html=build\coverage\coverage.txt -o build\coverage\coverage.html
|
||||
- ps: Push-AppveyorArtifact build\coverage\coverage.txt
|
||||
- ps: Push-AppveyorArtifact build\coverage\coverage.html
|
||||
# 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
|
||||
# - ps: Push-AppveyorArtifact build\coverage\coverage.txt
|
||||
# - 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"
|
||||
# - "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%"
|
||||
# - pip install codecov
|
||||
# - codecov -X gcov -f "build\coverage\coverage.txt"
|
||||
|
||||
# to disable deployment
|
||||
deploy: off
|
||||
|
||||
+8
-7
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react"
|
||||
],
|
||||
|
||||
"plugins": ["transform-object-rest-spread"]
|
||||
}
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread"
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -27,7 +27,7 @@ go get github.com/elazarl/go-bindata-assetfs/...
|
||||
yarn release
|
||||
```
|
||||
|
||||
This generates ui-assets.go in the current direcotry. Now do `make` in the parent directory to build the minio binary with the newly generated ``ui-assets.go``
|
||||
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
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Minio Browser</title>
|
||||
<link rel="stylesheet" href="/minio/loader.css" type="text/css">
|
||||
<link rel="stylesheet" href="loader.css" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page-load">
|
||||
<div class="pl-inner">
|
||||
<img src="/minio/logo.svg" alt="">
|
||||
<img src="logo.svg" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div id="root"></div>
|
||||
@@ -27,19 +27,19 @@
|
||||
<ul>
|
||||
<li>
|
||||
<a href="http://www.google.com/chrome/">
|
||||
<img src="/minio/chrome.png" alt="">
|
||||
<img src="chrome.png" alt="">
|
||||
<div>Chrome</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.mozilla.org/en-US/firefox/new/">
|
||||
<img src="/minio/firefox.png" alt="">
|
||||
<img src="firefox.png" alt="">
|
||||
<div>Firefox</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.apple.com/safari/">
|
||||
<img src="/minio/safari.png" alt="">
|
||||
<img src="safari.png" alt="">
|
||||
<div>Safari</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -51,6 +51,6 @@
|
||||
<![endif]-->
|
||||
|
||||
<script>currentUiVersion = 'MINIO_UI_VERSION'</script>
|
||||
<script src="/minio/index_bundle.js"></script>
|
||||
<script src="index_bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import 'babel-polyfill'
|
||||
|
||||
import './less/main.less'
|
||||
|
||||
@@ -44,7 +45,6 @@ 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)
|
||||
@@ -81,7 +81,7 @@ ReactDOM.render((
|
||||
<Provider store={ store } web={ web }>
|
||||
<Router history={ browserHistory }>
|
||||
<Route path='/' component={ App }>
|
||||
<Route path='minio' 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 } />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
|
||||
+107
-25
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -14,13 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import url from 'url'
|
||||
import Moment from 'moment'
|
||||
import browserHistory from 'react-router/lib/browserHistory'
|
||||
import web from './web'
|
||||
import * as utils from './utils'
|
||||
import storage from 'local-storage-fallback'
|
||||
|
||||
import { minioBrowserPrefix } from './constants'
|
||||
|
||||
export const SET_WEB = 'SET_WEB'
|
||||
@@ -28,9 +24,10 @@ 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 ADD_OBJECT = 'ADD_OBJECT'
|
||||
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'
|
||||
@@ -57,6 +54,9 @@ 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 {
|
||||
@@ -78,11 +78,12 @@ export const hideDeleteConfirmation = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const showShareObject = url => {
|
||||
export const showShareObject = (object, url) => {
|
||||
return {
|
||||
type: SET_SHARE_OBJECT,
|
||||
shareObject: {
|
||||
url: url,
|
||||
object,
|
||||
url,
|
||||
show: true
|
||||
}
|
||||
}
|
||||
@@ -98,15 +99,17 @@ export const hideShareObject = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const shareObject = (object, expiry) => (dispatch, getState) => {
|
||||
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(`${host}/${bucket}/${object}`))
|
||||
dispatch(showShareObject(object, `${host}/${bucket}/${object}`))
|
||||
return
|
||||
}
|
||||
|
||||
let expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60
|
||||
web.PresignedGet({
|
||||
host,
|
||||
bucket,
|
||||
@@ -114,7 +117,11 @@ export const shareObject = (object, expiry) => (dispatch, getState) => {
|
||||
expiry
|
||||
})
|
||||
.then(obj => {
|
||||
dispatch(showShareObject(obj.url))
|
||||
dispatch(showShareObject(object, obj.url))
|
||||
dispatch(showAlert({
|
||||
type: 'success',
|
||||
message: `Object shared. Expires in ${days} days ${hours} hours ${minutes} minutes.`
|
||||
}))
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(showAlert({
|
||||
@@ -235,15 +242,28 @@ export const setVisibleBuckets = visibleBuckets => {
|
||||
}
|
||||
}
|
||||
|
||||
export const setObjects = (objects, marker, istruncated) => {
|
||||
const appendObjects = (objects, marker, istruncated) => {
|
||||
return {
|
||||
type: SET_OBJECTS,
|
||||
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,
|
||||
@@ -304,14 +324,14 @@ export const listObjects = () => {
|
||||
marker: marker
|
||||
})
|
||||
.then(res => {
|
||||
let objects = res.objects
|
||||
let objects = res.objects
|
||||
if (!objects)
|
||||
objects = []
|
||||
objects = objects.map(object => {
|
||||
object.name = object.name.replace(`${currentPath}`, '');
|
||||
return object
|
||||
})
|
||||
dispatch(setObjects(objects, res.nextmarker, res.istruncated))
|
||||
object.name = object.name.replace(`${currentPath}`, '');
|
||||
return object
|
||||
})
|
||||
dispatch(appendObjects(objects, res.nextmarker, res.istruncated))
|
||||
dispatch(setPrefixWritable(res.writable))
|
||||
dispatch(setLoadBucket(''))
|
||||
dispatch(setLoadPath(''))
|
||||
@@ -332,7 +352,7 @@ export const listObjects = () => {
|
||||
export const selectPrefix = prefix => {
|
||||
return (dispatch, getState) => {
|
||||
const {currentBucket, web} = getState()
|
||||
dispatch(setObjects([], "", false))
|
||||
dispatch(resetObjects())
|
||||
dispatch(setLoadPath(prefix))
|
||||
web.ListObjects({
|
||||
bucketName: currentBucket,
|
||||
@@ -344,10 +364,10 @@ export const selectPrefix = prefix => {
|
||||
if (!objects)
|
||||
objects = []
|
||||
objects = objects.map(object => {
|
||||
object.name = object.name.replace(`${prefix}`, '');
|
||||
return object
|
||||
})
|
||||
dispatch(setObjects(
|
||||
object.name = object.name.replace(`${prefix}`, '');
|
||||
return object
|
||||
})
|
||||
dispatch(appendObjects(
|
||||
objects,
|
||||
res.nextmarker,
|
||||
res.istruncated
|
||||
@@ -410,11 +430,42 @@ export const setLoginError = () => {
|
||||
}
|
||||
}
|
||||
|
||||
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}/minio/upload/${currentBucket}/${objectName}`
|
||||
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}`
|
||||
|
||||
@@ -431,7 +482,7 @@ export const uploadFile = (file, xhr) => {
|
||||
}))
|
||||
|
||||
xhr.onload = function(event) {
|
||||
if (xhr.status == 401 || xhr.status == 403 || xhr.status == 500) {
|
||||
if (xhr.status == 401 || xhr.status == 403) {
|
||||
setShowAbortModal(false)
|
||||
dispatch(stopUpload({
|
||||
slug
|
||||
@@ -441,6 +492,16 @@ export const uploadFile = (file, xhr) => {
|
||||
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({
|
||||
@@ -563,3 +624,24 @@ export const setPolicies = (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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -27,7 +27,6 @@ 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'
|
||||
@@ -69,7 +68,7 @@ export default class Browse extends React.Component {
|
||||
memory: res.MinioMemory,
|
||||
platform: res.MinioPlatform,
|
||||
runtime: res.MinioRuntime,
|
||||
envVars: res.MinioEnvVars
|
||||
info: res.MinioGlobalInfo
|
||||
})
|
||||
dispatch(actions.setServerInfo(serverInfo))
|
||||
})
|
||||
@@ -151,7 +150,21 @@ export default class Browse extends React.Component {
|
||||
if (prefix === currentPath) return
|
||||
browserHistory.push(utils.pathJoin(currentBucket, encPrefix))
|
||||
} else {
|
||||
window.location = `${window.location.origin}/minio/download/${currentBucket}/${encPrefix}?token=${storage.getItem('token')}`
|
||||
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
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,14 +240,29 @@ export default class Browse extends React.Component {
|
||||
}
|
||||
|
||||
removeObject() {
|
||||
const {web, dispatch, currentPath, currentBucket, deleteConfirmation} = this.props
|
||||
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,
|
||||
objectName: deleteConfirmation.object
|
||||
bucketname: currentBucket,
|
||||
objects: objects
|
||||
})
|
||||
.then(() => {
|
||||
this.hideDeleteConfirmation()
|
||||
dispatch(actions.removeObject(deleteConfirmation.object))
|
||||
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',
|
||||
@@ -262,7 +290,8 @@ export default class Browse extends React.Component {
|
||||
shareObject(e, object) {
|
||||
e.preventDefault()
|
||||
const {dispatch} = this.props
|
||||
dispatch(actions.shareObject(object))
|
||||
// let expiry = 5 * 24 * 60 * 60 // 5 days expiry by default
|
||||
dispatch(actions.shareObject(object, 5, 0, 0))
|
||||
}
|
||||
|
||||
hideShareObjectModal() {
|
||||
@@ -354,18 +383,75 @@ export default class Browse extends React.Component {
|
||||
this.refs.copyTextInput.select()
|
||||
}
|
||||
|
||||
handleExpireValue(targetInput, inc) {
|
||||
inc === -1 ? this.refs[targetInput].stepDown(1) : this.refs[targetInput].stepUp(1)
|
||||
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} = this.props
|
||||
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
|
||||
@@ -416,25 +502,28 @@ export default class Browse extends React.Component {
|
||||
settingsFunc={ this.showSettings.bind(this) }
|
||||
logoutFunc={ this.logout.bind(this) } />
|
||||
} else {
|
||||
loginButton = <a className='btn btn-danger' href='/minio/login'>Login</a>
|
||||
loginButton = <a className='btn btn-danger' href={minioBrowserPrefix+'/login'}>Login</a>
|
||||
}
|
||||
|
||||
if (web.LoggedIn()) {
|
||||
storageUsageDetails = <div className="feh-usage">
|
||||
<div className="fehu-chart">
|
||||
<div style={ { width: usedPercent } }></div>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
Used:
|
||||
{ humanize.filesize(total - free) }
|
||||
</li>
|
||||
<li className="pull-right">
|
||||
Free:
|
||||
{ humanize.filesize(total - used) }
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
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>
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -490,6 +579,14 @@ export default class Browse extends React.Component {
|
||||
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">
|
||||
@@ -515,7 +612,8 @@ export default class Browse extends React.Component {
|
||||
</header>
|
||||
<div className="feb-container">
|
||||
<header className="fesl-row" data-type="folder">
|
||||
<div className="fesl-item fi-name" onClick={ this.sortObjectsByName.bind(this) } data-sort="name">
|
||||
<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,
|
||||
@@ -524,7 +622,7 @@ export default class Browse extends React.Component {
|
||||
'fa-sort-alpha-asc': !sortNameOrder
|
||||
}) } />
|
||||
</div>
|
||||
<div className="fesl-item fi-size" onClick={ this.sortObjectsBySize.bind(this) } data-sort="size">
|
||||
<div className="fesl-item fesl-item-size" onClick={ this.sortObjectsBySize.bind(this) } data-sort="size">
|
||||
Size
|
||||
<i className={ classNames({
|
||||
'fesli-sort': true,
|
||||
@@ -533,7 +631,7 @@ export default class Browse extends React.Component {
|
||||
'fa-sort-amount-asc': !sortSizeOrder
|
||||
}) } />
|
||||
</div>
|
||||
<div className="fesl-item fi-modified" onClick={ this.sortObjectsByDate.bind(this) } data-sort="last-modified">
|
||||
<div className="fesl-item fesl-item-modified" onClick={ this.sortObjectsByDate.bind(this) } data-sort="last-modified">
|
||||
Last Modified
|
||||
<i className={ classNames({
|
||||
'fesli-sort': true,
|
||||
@@ -542,7 +640,7 @@ export default class Browse extends React.Component {
|
||||
'fa-sort-numeric-asc': !sortDateOrder
|
||||
}) } />
|
||||
</div>
|
||||
<div className="fesl-item fi-actions"></div>
|
||||
<div className="fesl-item fesl-item-actions"></div>
|
||||
</header>
|
||||
</div>
|
||||
<div className="feb-container">
|
||||
@@ -553,9 +651,11 @@ export default class Browse extends React.Component {
|
||||
<ObjectsList dataType={ this.dataType.bind(this) }
|
||||
selectPrefix={ this.selectPrefix.bind(this) }
|
||||
showDeleteConfirmation={ this.showDeleteConfirmation.bind(this) }
|
||||
shareObject={ this.shareObject.bind(this) } />
|
||||
shareObject={ this.shareObject.bind(this) }
|
||||
checkObject={ this.checkObject.bind(this) }
|
||||
checkedObjectsArray={ checkedObjects } />
|
||||
</InfiniteScroll>
|
||||
<div className="text-center" style={ { display: istruncated ? 'block' : 'none' } }>
|
||||
<div className="text-center" style={ { display: (istruncated && currentBucket) ? 'block' : 'none' } }>
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -669,11 +769,11 @@ export default class Browse extends React.Component {
|
||||
</div>
|
||||
<div className="input-group" style={ { display: web.LoggedIn() ? 'block' : 'none' } }>
|
||||
<label>
|
||||
Expires in
|
||||
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) }></i>
|
||||
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireDays', 1, shareObject.object) } />
|
||||
<div className="set-expire-title">
|
||||
Days
|
||||
</div>
|
||||
@@ -682,12 +782,14 @@ export default class Browse extends React.Component {
|
||||
type="number"
|
||||
min={ 0 }
|
||||
max={ 7 }
|
||||
defaultValue={ 0 } />
|
||||
defaultValue={ 5 }
|
||||
readOnly="readOnly"
|
||||
/>
|
||||
</div>
|
||||
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireDays', -1) }></i>
|
||||
<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) }></i>
|
||||
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireHours', 1, shareObject.object) } />
|
||||
<div className="set-expire-title">
|
||||
Hours
|
||||
</div>
|
||||
@@ -695,30 +797,34 @@ export default class Browse extends React.Component {
|
||||
<input ref="expireHours"
|
||||
type="number"
|
||||
min={ 0 }
|
||||
max={ 24 }
|
||||
defaultValue={ 0 } />
|
||||
max={ 23 }
|
||||
defaultValue={ 0 }
|
||||
readOnly="readOnly"
|
||||
/>
|
||||
</div>
|
||||
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireHours', -1) }></i>
|
||||
<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) }></i>
|
||||
<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={ 1 }
|
||||
max={ 60 }
|
||||
defaultValue={ 45 } />
|
||||
min={ 0 }
|
||||
max={ 59 }
|
||||
defaultValue={ 0 }
|
||||
readOnly="readOnly"
|
||||
/>
|
||||
</div>
|
||||
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireMins', -1) }></i>
|
||||
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireMins', -1, shareObject.object) } />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<div className="modal-footer">
|
||||
<CopyToClipboard text={ shareObject.url } onCopy={ this.showMessage.bind(this) }>
|
||||
<CopyToClipboard text={ window.location.protocol + '//' + shareObject.url } onCopy={ this.showMessage.bind(this) }>
|
||||
<button className="btn btn-success">
|
||||
Copy Link
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016, 2017 Minio, Inc.
|
||||
* 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.
|
||||
@@ -27,7 +27,7 @@ let BrowserDropdown = ({fullScreenFunc, aboutFunc, settingsFunc, logoutFunc}) =>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
<li>
|
||||
<a target="_blank" href="https://github.com/minio/miniobrowser">Github <i className="fa fa-github"></i></a>
|
||||
<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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -39,11 +39,12 @@ export default class Dropzone extends React.Component {
|
||||
// won't handle child elements correctly.
|
||||
const style = {
|
||||
height: '100%',
|
||||
borderWidth: '2px',
|
||||
borderWidth: '0',
|
||||
borderStyle: 'dashed',
|
||||
borderColor: '#fff'
|
||||
}
|
||||
const activeStyle = {
|
||||
borderWidth: '2px',
|
||||
borderColor: '#777'
|
||||
}
|
||||
const rejectStyle = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -89,10 +89,6 @@ export default class Login extends React.Component {
|
||||
{ alertBox }
|
||||
<div className="l-wrap">
|
||||
<form onSubmit={ this.handleSubmit.bind(this) }>
|
||||
<input name="fixBrowser"
|
||||
autoComplete="username"
|
||||
type="text"
|
||||
style={ { display: 'none' } } />
|
||||
<InputGroup className="ig-dark"
|
||||
label="Access Key"
|
||||
id="accessKey"
|
||||
@@ -102,7 +98,6 @@ export default class Login extends React.Component {
|
||||
required="required"
|
||||
autoComplete="username">
|
||||
</InputGroup>
|
||||
<input type="text" autoComplete="new-password" style={ { display: 'none' } } />
|
||||
<InputGroup className="ig-dark"
|
||||
label="Secret Key"
|
||||
id="secretKey"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -20,8 +20,7 @@ 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}) => {
|
||||
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')
|
||||
@@ -30,29 +29,51 @@ let ObjectsList = ({objects, currentPath, selectPrefix, dataType, showDeleteConf
|
||||
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 (!object.name.endsWith('/')) {
|
||||
actionButtons = <Dropdown id="fia-dropdown">
|
||||
<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>
|
||||
|
||||
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 } data-type={ dataType(object.name, object.contentType) }>
|
||||
<div className="fesl-item fi-name">
|
||||
<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 fi-size">
|
||||
<div className="fesl-item fesl-item-size">
|
||||
{ size }
|
||||
</div>
|
||||
<div className="fesl-item fi-modified">
|
||||
<div className="fesl-item fesl-item-modified">
|
||||
{ lastModified }
|
||||
</div>
|
||||
<div className="fesl-item fi-actions">
|
||||
<div className="fesl-item fesl-item-actions">
|
||||
{ actionButtons }
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,4 +93,4 @@ export default connect(state => {
|
||||
currentPath: state.currentPath,
|
||||
loadPath: state.loadPath
|
||||
}
|
||||
})(ObjectsList)
|
||||
})(ObjectsList)
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
|
||||
@@ -77,4 +77,4 @@ class Policy extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => state)(Policy)
|
||||
export default connect(state => state)(Policy)
|
||||
@@ -7,6 +7,7 @@ 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 => {
|
||||
@@ -27,8 +28,23 @@ class PolicyInput extends Component {
|
||||
|
||||
handlePolicySubmit(e) {
|
||||
e.preventDefault()
|
||||
const {web, dispatch} = this.props
|
||||
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,
|
||||
@@ -36,8 +52,7 @@ class PolicyInput extends Component {
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(actions.setPolicies([{
|
||||
policy: this.policy.value,
|
||||
prefix: this.prefix.value + '*',
|
||||
policy, prefix
|
||||
}, ...this.props.policies]))
|
||||
this.prefix.value = ''
|
||||
})
|
||||
@@ -80,4 +95,4 @@ class PolicyInput extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => state)(PolicyInput)
|
||||
export default connect(state => state)(PolicyInput)
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -34,23 +34,12 @@ class SettingsModal extends React.Component {
|
||||
|
||||
let accessKeyEnv = ''
|
||||
let secretKeyEnv = ''
|
||||
// Check environment variables first. They may or may not have been
|
||||
// loaded already; they load in Browse#componentDidMount.
|
||||
if (serverInfo.envVars) {
|
||||
serverInfo.envVars.forEach(envVar => {
|
||||
let keyVal = envVar.split('=')
|
||||
if (keyVal[0] == 'MINIO_ACCESS_KEY') {
|
||||
accessKeyEnv = keyVal[1]
|
||||
} else if (keyVal[0] == 'MINIO_SECRET_KEY') {
|
||||
secretKeyEnv = keyVal[1]
|
||||
}
|
||||
})
|
||||
}
|
||||
if (accessKeyEnv != '' || secretKeyEnv != '') {
|
||||
// Check environment variables first.
|
||||
if (serverInfo.info.isEnvCreds) {
|
||||
dispatch(actions.setSettings({
|
||||
accessKey: accessKeyEnv,
|
||||
secretKey: secretKeyEnv,
|
||||
keysReadOnly: true
|
||||
accessKey: 'xxxxxxxxx',
|
||||
secretKey: 'xxxxxxxxx',
|
||||
keysReadOnly: true
|
||||
}))
|
||||
} else {
|
||||
web.GetAuth()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -17,7 +17,9 @@
|
||||
// File for all the browser constants.
|
||||
|
||||
// minioBrowserPrefix absolute path.
|
||||
export const minioBrowserPrefix = '/minio'
|
||||
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'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
|
||||
+28
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -54,9 +54,10 @@ export default (state = {
|
||||
shareObject: {
|
||||
show: false,
|
||||
url: '',
|
||||
expiry: 604800
|
||||
object: ''
|
||||
},
|
||||
prefixWritable: false
|
||||
prefixWritable: false,
|
||||
checkedObjects: []
|
||||
}, action) => {
|
||||
let newState = Object.assign({}, state)
|
||||
switch (action.type) {
|
||||
@@ -76,16 +77,18 @@ export default (state = {
|
||||
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:
|
||||
if (!action.objects.length) {
|
||||
newState.objects = []
|
||||
newState.marker = ""
|
||||
newState.istruncated = action.istruncated
|
||||
} else {
|
||||
newState.objects = [...newState.objects, ...action.objects]
|
||||
newState.marker = action.marker
|
||||
newState.istruncated = action.istruncated
|
||||
}
|
||||
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
|
||||
@@ -185,6 +188,19 @@ export default (state = {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -112,6 +112,9 @@ export default class Web {
|
||||
return res
|
||||
})
|
||||
}
|
||||
CreateURLToken() {
|
||||
return this.makeCall('CreateURLToken')
|
||||
}
|
||||
GetBucketPolicy(args) {
|
||||
return this.makeCall('GetBucketPolicy', args)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-white {
|
||||
.btn-variant(#fff, darken(@text-color, 20%));
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
.btn-variant(#eee, #545454);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ select.form-control {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
background: transparent !important;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -114,8 +114,8 @@ select.form-control {
|
||||
|
||||
.ig-dark {
|
||||
.ig-text {
|
||||
color: @white;
|
||||
border-color: rgba(255,255,255,0.1);
|
||||
color: @white !important;
|
||||
border-color: rgba(255,255,255,0.1) !important;
|
||||
}
|
||||
|
||||
.ig-helpers {
|
||||
@@ -183,6 +183,17 @@ select.form-control {
|
||||
.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 {
|
||||
@@ -191,6 +202,7 @@ select.form-control {
|
||||
display: table-cell;
|
||||
width: 1%;
|
||||
text-align: center;
|
||||
.user-select(none);
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid @input-border;
|
||||
@@ -209,6 +221,7 @@ select.form-control {
|
||||
left: -8px;
|
||||
|
||||
input {
|
||||
.user-select(none);
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
Header
|
||||
----------------------------*/
|
||||
.fe-header {
|
||||
padding: 45px 55px 20px;
|
||||
|
||||
@media(min-width: @screen-md-min) {
|
||||
@media(min-width: (@screen-sm-min - 100)) {
|
||||
position: relative;
|
||||
padding: 40px 40px 20px 45px;
|
||||
}
|
||||
|
||||
@media(max-width: (@screen-xs-max - 100)) {
|
||||
padding: 25px 25px 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@@ -239,4 +238,3 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+236
-102
@@ -2,17 +2,19 @@
|
||||
Row
|
||||
----------------------------*/
|
||||
.fesl-row {
|
||||
padding-right: 40px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -20,7 +22,7 @@ header.fesl-row {
|
||||
@media (min-width:(@screen-sm-min - 100px)) {
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid lighten(@text-muted-color, 20%);
|
||||
padding-left: 40px;
|
||||
padding-left: 30px;
|
||||
|
||||
.fesl-item,
|
||||
.fesli-sort {
|
||||
@@ -42,7 +44,7 @@ header.fesl-row {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:hover:not(.fi-actions) {
|
||||
&:hover:not(.fesl-item-actions) {
|
||||
background: lighten(@text-muted-color, 22%);
|
||||
color: @dark-gray;
|
||||
|
||||
@@ -58,54 +60,42 @@ header.fesl-row {
|
||||
}
|
||||
}
|
||||
|
||||
.list-type(@background, @icon) {
|
||||
.fis-icon {
|
||||
background-color: @background;
|
||||
|
||||
&:before {
|
||||
content: @icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.fesl-row {
|
||||
padding-left: 85px;
|
||||
border-bottom: 1px solid transparent;
|
||||
cursor: default;
|
||||
.transition(background-color);
|
||||
.transition-duration(500ms);
|
||||
|
||||
@media (max-width: (@screen-xs-max - 100px)) {
|
||||
padding-left: 70px;
|
||||
padding-right: 45px;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #fbf7dc;
|
||||
}
|
||||
|
||||
&[data-type]:before {
|
||||
font-family: @font-family-icon;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
text-align: center;
|
||||
line-height: 35px;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
font-size: 16px;
|
||||
left: 50px;
|
||||
top: 9px;
|
||||
color: @white;
|
||||
|
||||
@media (max-width: (@screen-xs-max - 100px)) {
|
||||
left: 20px;
|
||||
&:not(.fesl-row-selected) {
|
||||
&:nth-child(even) {
|
||||
background-color: @list-row-even-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-type="folder"] {
|
||||
@media (max-width: (@screen-xs-max - 100px)) {
|
||||
.fesl-item {
|
||||
&.fi-name {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
&:hover {
|
||||
.fis-icon {
|
||||
&:before {
|
||||
.opacity(0)
|
||||
}
|
||||
}
|
||||
|
||||
&.fi-size,
|
||||
&.fi-modified {
|
||||
display: none;
|
||||
}
|
||||
.fis-helper {
|
||||
&:before {
|
||||
.opacity(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,54 +103,18 @@ div.fesl-row {
|
||||
/*--------------------------
|
||||
Icons
|
||||
----------------------------*/
|
||||
&[data-type=folder]:before {
|
||||
content: '\f114';
|
||||
background-color: #a1d6dd;
|
||||
}
|
||||
&[data-type=pdf]:before {
|
||||
content: "\f1c1";
|
||||
background-color: #fa7775;
|
||||
}
|
||||
&[data-type=zip]:before {
|
||||
content: "\f1c6";
|
||||
background-color: #427089;
|
||||
}
|
||||
&[data-type=audio]:before {
|
||||
content: "\f1c7";
|
||||
background-color: #009688
|
||||
}
|
||||
&[data-type=code]:before {
|
||||
content: "\f1c9";
|
||||
background-color: #997867;
|
||||
}
|
||||
&[data-type=excel]:before {
|
||||
content: "\f1c3";
|
||||
background-color: #64c866;
|
||||
}
|
||||
&[data-type=image]:before {
|
||||
content: "\f1c5";
|
||||
background-color: #f06292;
|
||||
}
|
||||
&[data-type=video]:before {
|
||||
content: "\f1c8";
|
||||
background-color: #f8c363;
|
||||
}
|
||||
&[data-type=other]:before {
|
||||
content: "\f016";
|
||||
background-color: #afafaf;
|
||||
}
|
||||
&[data-type=text]:before {
|
||||
content: "\f0f6";
|
||||
background-color: #8a8a8a;
|
||||
}
|
||||
&[data-type=doc]:before {
|
||||
content: "\f1c2";
|
||||
background-color: #2196f5;
|
||||
}
|
||||
&[data-type=presentation]:before {
|
||||
content: "\f1c4";
|
||||
background-color: #896ea6;
|
||||
}
|
||||
&[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 {
|
||||
@@ -180,6 +134,113 @@ div.fesl-row {
|
||||
}
|
||||
}
|
||||
|
||||
.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
|
||||
@@ -192,26 +253,26 @@ div.fesl-row {
|
||||
}
|
||||
|
||||
@media(min-width: (@screen-sm-min - 100px)) {
|
||||
&:not(.fi-actions) {
|
||||
&:not(.fesl-item-actions):not(.fesl-item-icon) {
|
||||
text-overflow: ellipsis;
|
||||
padding: 10px 15px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.fi-name {
|
||||
&.fesl-item-name {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
&.fi-size {
|
||||
&.fesl-item-size {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
&.fi-modified {
|
||||
&.fesl-item-modified {
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
&.fi-actions {
|
||||
&.fesl-item-actions {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
@@ -219,29 +280,29 @@ div.fesl-row {
|
||||
@media(max-width: (@screen-xs-max - 100px)) {
|
||||
padding: 0;
|
||||
|
||||
&.fi-name {
|
||||
&.fesl-item-name {
|
||||
width: 100%;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
&.fi-size,
|
||||
&.fi-modified {
|
||||
&.fesl-item-size,
|
||||
&.fesl-item-modified {
|
||||
font-size: 12px;
|
||||
color: #B5B5B5;
|
||||
float: left;
|
||||
}
|
||||
|
||||
&.fi-modified {
|
||||
&.fesl-item-modified {
|
||||
max-width: 72px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.fi-size {
|
||||
&.fesl-item-size {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&.fi-actions {
|
||||
&.fesl-item-actions {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 10px;
|
||||
@@ -266,7 +327,7 @@ div.fesl-row {
|
||||
}
|
||||
}
|
||||
|
||||
.fi-actions {
|
||||
.fesl-item-actions {
|
||||
.dropdown-menu {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
@@ -324,6 +385,79 @@ div.fesl-row {
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -99,4 +99,18 @@
|
||||
content: '7 days';
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.modal-aheader {
|
||||
height: 100px;
|
||||
|
||||
&:before {
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
@@ -49,4 +49,4 @@
|
||||
z-index: 1;
|
||||
-webkit-animation: zoomIn 250ms, spin 700ms 250ms infinite linear;
|
||||
animation: zoomIn 250ms, spin 700ms 250ms infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding: 35px;
|
||||
padding: 25px;
|
||||
|
||||
@media(min-width: @screen-md-min) {
|
||||
.translate3d(0, 0, 0);
|
||||
@@ -63,15 +63,15 @@
|
||||
height: ~"calc(100vh - 260px)";
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
margin: 0 -35px;
|
||||
margin: 0 -25px;
|
||||
|
||||
& li {
|
||||
position: relative;
|
||||
|
||||
& > a {
|
||||
display: block;
|
||||
padding: 10px 40px 12px 65px;
|
||||
.text-overflow();
|
||||
padding: 10px 45px 12px 55px;
|
||||
word-wrap: break-word;
|
||||
|
||||
&:before {
|
||||
font-family: FontAwesome;
|
||||
@@ -79,7 +79,7 @@
|
||||
font-size: 17px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 35px;
|
||||
left: 25px;
|
||||
.opacity(0.8);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
background-color: #282e32;
|
||||
|
||||
& > a {
|
||||
color: @white;
|
||||
@@ -139,10 +139,10 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 40px;
|
||||
width: 35px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
background: url(../../img/more-h-light.svg) no-repeat left;
|
||||
background: url(../../img/more-h-light.svg) no-repeat top 20px left;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
/*--------------------------
|
||||
File Explorer
|
||||
----------------------------*/
|
||||
@fe-sidebar-width : 300px;
|
||||
@fe-sidebar-width : 320px;
|
||||
@text-muted-color : #BDBDBD;
|
||||
@text-strong-color : #333;
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
/*-------------------------
|
||||
Colors
|
||||
--------------------------*/
|
||||
@brand-primary: #2196F3;
|
||||
@brand-primary: #2298f7;
|
||||
@brand-success: #4CAF50;
|
||||
@brand-info: #00BCD4;
|
||||
@brand-warning: #FF9800;
|
||||
@@ -91,4 +91,11 @@
|
||||
/*-------------------------
|
||||
Form
|
||||
--------------------------*/
|
||||
@input-border: #eee;
|
||||
@input-border: #eee;
|
||||
|
||||
|
||||
/*-------------------------
|
||||
List
|
||||
--------------------------*/
|
||||
@list-row-selected-bg: #fbf2bf;
|
||||
@list-row-even-bg: #fafafa;
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -70,9 +70,9 @@ async.waterfall([
|
||||
commitId = stdout.replace('\n', '')
|
||||
if (commitId.length !== 40) throw new Error('commitId invalid : ' + commitId)
|
||||
assetsFileName = 'ui-assets.go';
|
||||
var cmd = 'go-bindata-assetfs -pkg miniobrowser -nocompress=true production/...'
|
||||
var cmd = 'go-bindata-assetfs -pkg browser -nocompress=true production/...'
|
||||
if (!isProduction) {
|
||||
cmd = 'go-bindata-assetfs -pkg miniobrowser -nocompress=true dev/...'
|
||||
cmd = 'go-bindata-assetfs -pkg browser -nocompress=true dev/...'
|
||||
}
|
||||
console.log('Running', cmd)
|
||||
exec(cmd, cb)
|
||||
|
||||
+18
-19
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "minio-browser",
|
||||
"name": "browser",
|
||||
"version": "0.0.1",
|
||||
"description": "Minio Browser",
|
||||
"scripts": {
|
||||
@@ -11,14 +11,14 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/minio/miniobrowser"
|
||||
"url": "https://github.com/minio/minio"
|
||||
},
|
||||
"author": "Minio Inc",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/minio/miniobrowser/issues"
|
||||
"url": "https://github.com/minio/minio/issues"
|
||||
},
|
||||
"homepage": "https://github.com/minio/miniobrowser",
|
||||
"homepage": "https://github.com/minio/minio",
|
||||
"devDependencies": {
|
||||
"async": "^1.5.2",
|
||||
"babel-cli": "^6.14.0",
|
||||
@@ -26,6 +26,7 @@
|
||||
"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",
|
||||
@@ -34,8 +35,6 @@
|
||||
"esformatter": "^0.10.0",
|
||||
"esformatter-jsx": "^7.4.1",
|
||||
"esformatter-jsx-ignore": "^1.0.6",
|
||||
"expect": "^1.20.2",
|
||||
"history": "^1.17.0",
|
||||
"html-webpack-plugin": "^2.22.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"karma": "^0.13.22",
|
||||
@@ -47,38 +46,38 @@
|
||||
"less": "^2.7.1",
|
||||
"less-loader": "^2.2.3",
|
||||
"mocha": "^2.5.3",
|
||||
"moment": "^2.15.1",
|
||||
"purifycss-webpack-plugin": "^2.0.3",
|
||||
"react": "^0.14.8",
|
||||
"react-addons-test-utils": "^0.14.8",
|
||||
"react-bootstrap": "^0.28.5",
|
||||
"react-custom-scrollbars": "^2.3.0",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^2.8.1",
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^1.0.3",
|
||||
"style-loader": "^0.13.1",
|
||||
"superagent": "^1.8.4",
|
||||
"superagent-es6-promise": "^1.0.0",
|
||||
"url-loader": "^0.5.7",
|
||||
"webpack": "^1.12.11",
|
||||
"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-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"
|
||||
}
|
||||
}
|
||||
|
||||
+62
-55
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -71,6 +71,10 @@ var exports = {
|
||||
target: 'http://localhost:9000',
|
||||
secure: false
|
||||
},
|
||||
'/minio/zip': {
|
||||
target: 'http://localhost:9000',
|
||||
secure: false
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
|
||||
+1356
-1031
File diff suppressed because it is too large
Load Diff
@@ -1,122 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
_init() {
|
||||
# Save release LDFLAGS
|
||||
LDFLAGS=$(go run buildscripts/gen-ldflags.go)
|
||||
|
||||
# Extract release tag
|
||||
release_tag=$(echo $LDFLAGS | awk {'print $4'} | cut -f2 -d=)
|
||||
|
||||
# Verify release tag.
|
||||
if [ -z "$release_tag" ]; then
|
||||
echo "Release tag cannot be empty. Please check return value of \`go run buildscripts/gen-ldflags.go\`"
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# Extract release string.
|
||||
release_str=$(echo $MINIO_RELEASE | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Verify release string.
|
||||
if [ -z "$release_str" ]; then
|
||||
echo "Release string cannot be empty. Please set \`MINIO_RELEASE\` env variable."
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# List of supported architectures
|
||||
SUPPORTED_OSARCH='linux/386 linux/amd64 linux/arm linux/arm64 windows/386 windows/amd64 darwin/amd64 freebsd/amd64'
|
||||
|
||||
## System binaries
|
||||
CP=`which cp`
|
||||
SHASUM=`which shasum`
|
||||
SED=`which sed`
|
||||
}
|
||||
|
||||
go_build() {
|
||||
local osarch=$1
|
||||
os=$(echo $osarch | cut -f1 -d'/')
|
||||
arch=$(echo $osarch | cut -f2 -d'/')
|
||||
package=$(go list -f '{{.ImportPath}}')
|
||||
echo -n "-->"
|
||||
printf "%15s:%s\n" "${osarch}" "${package}"
|
||||
|
||||
# Release binary name
|
||||
release_bin="$release_str/$os-$arch/$(basename $package).$release_tag"
|
||||
# Release binary downloadable name
|
||||
release_real_bin="$release_str/$os-$arch/$(basename $package)"
|
||||
# Release shasum name
|
||||
release_shasum="$release_str/$os-$arch/$(basename $package).shasum"
|
||||
|
||||
# Go build to build the binary.
|
||||
if [ "${arch}" == "arm" ]; then
|
||||
# Release binary downloadable name
|
||||
release_real_bin_6="$release_str/$os-${arch}6vl/$(basename $package)"
|
||||
|
||||
release_bin_6="$release_str/$os-${arch}6vl/$(basename $package).$release_tag"
|
||||
## Support building for ARM6vl
|
||||
GOARM=6 GOOS=$os GOARCH=$arch go build --ldflags "${LDFLAGS}" -o $release_bin_6
|
||||
|
||||
## Copy
|
||||
$CP -p $release_bin_6 $release_real_bin_6
|
||||
|
||||
# Release shasum name
|
||||
release_shasum_6="$release_str/$os-${arch}6vl/$(basename $package).shasum"
|
||||
|
||||
# Calculate shasum
|
||||
shasum_str=$(${SHASUM} ${release_bin_6})
|
||||
echo ${shasum_str} | $SED "s/$release_str\/$os-${arch}6vl\///g" > $release_shasum_6
|
||||
|
||||
# Release binary downloadable name
|
||||
release_real_bin_7="$release_str/$os-$arch/$(basename $package)"
|
||||
|
||||
release_bin_7="$release_str/$os-$arch/$(basename $package).$release_tag"
|
||||
## Support building for ARM7vl
|
||||
GOARM=7 GOOS=$os GOARCH=$arch go build --ldflags "${LDFLAGS}" -o $release_bin_7
|
||||
|
||||
## Copy
|
||||
$CP -p $release_bin_7 $release_real_bin_7
|
||||
|
||||
# Release shasum name
|
||||
release_shasum_7="$release_str/$os-$arch/$(basename $package).shasum"
|
||||
|
||||
# Calculate shasum
|
||||
shasum_str=$(${SHASUM} ${release_bin_7})
|
||||
echo ${shasum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_shasum_7
|
||||
else
|
||||
GOOS=$os GOARCH=$arch go build --ldflags "${LDFLAGS}" -o $release_bin
|
||||
|
||||
# Create copy
|
||||
if [ $os == "windows" ]; then
|
||||
$CP -p $release_bin ${release_real_bin}.exe
|
||||
else
|
||||
$CP -p $release_bin $release_real_bin
|
||||
fi
|
||||
|
||||
# Calculate shasum
|
||||
shasum_str=$(${SHASUM} ${release_bin})
|
||||
echo ${shasum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_shasum
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
# Build releases.
|
||||
echo "Executing $release_str builds for OS: ${SUPPORTED_OSARCH}"
|
||||
echo "Choose an OS Arch from the below"
|
||||
for osarch in ${SUPPORTED_OSARCH}; do
|
||||
echo ${osarch}
|
||||
done
|
||||
|
||||
read -p "If you want to build for all, Just press Enter: " chosen_osarch
|
||||
if [ "$chosen_osarch" = "" ] || [ "$chosen_osarch" = "all" ]; then
|
||||
for each_osarch in ${SUPPORTED_OSARCH}; do
|
||||
go_build ${each_osarch}
|
||||
done
|
||||
else
|
||||
for each_osarch in $(echo $chosen_osarch | sed 's/,/ /g'); do
|
||||
go_build ${each_osarch}
|
||||
done
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
# Run main.
|
||||
_init && main
|
||||
@@ -21,10 +21,15 @@ _init() {
|
||||
|
||||
## Minimum required versions for build dependencies
|
||||
GIT_VERSION="1.0"
|
||||
GO_VERSION="1.7.1"
|
||||
GO_VERSION="1.9.1"
|
||||
OSX_VERSION="10.8"
|
||||
KNAME=$(uname -s)
|
||||
ARCH=$(uname -m)
|
||||
case "${KNAME}" in
|
||||
SunOS )
|
||||
ARCH=$(isainfo -k)
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
## FIXME:
|
||||
@@ -97,7 +102,7 @@ assert_is_supported_arch() {
|
||||
|
||||
assert_is_supported_os() {
|
||||
case "${KNAME}" in
|
||||
Linux | FreeBSD | OpenBSD | NetBSD | DragonFly )
|
||||
Linux | FreeBSD | OpenBSD | NetBSD | DragonFly | SunOS )
|
||||
return
|
||||
;;
|
||||
Darwin )
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
#
|
||||
|
||||
main() {
|
||||
echo "Checking project is in GOPATH:"
|
||||
|
||||
IFS=':' read -r -a paths <<< "$GOPATH"
|
||||
for path in "${paths[@]}"; do
|
||||
minio_path="$path/src/github.com/minio/minio"
|
||||
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
)
|
||||
|
||||
func genLDFlags(version string) string {
|
||||
var ldflagsStr string
|
||||
ldflagsStr = "-X github.com/minio/minio/cmd.Version=" + version
|
||||
ldflagsStr := "-s -w"
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.Version=" + version
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.ReleaseTag=" + releaseTag(version)
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.CommitID=" + commitID()
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.ShortCommitID=" + commitID()[:12]
|
||||
|
||||
Executable
+237
@@ -0,0 +1,237 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Minio Cloud Storage, (C) 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.
|
||||
#
|
||||
|
||||
set -e
|
||||
set -E
|
||||
set -o pipefail
|
||||
|
||||
if [ ! -x "$PWD/minio" ]; then
|
||||
echo "minio executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||
|
||||
export MINT_MODE=core
|
||||
export MINT_DATA_DIR="$WORK_DIR/data"
|
||||
export SERVER_ENDPOINT="127.0.0.1:9000"
|
||||
export ACCESS_KEY="minio"
|
||||
export SECRET_KEY="minio123"
|
||||
export ENABLE_HTTPS=0
|
||||
|
||||
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
||||
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" )
|
||||
|
||||
FILE_1_MB="$MINT_DATA_DIR/datafile-1-MB"
|
||||
FILE_65_MB="$MINT_DATA_DIR/datafile-65-MB"
|
||||
|
||||
FUNCTIONAL_TESTS="$WORK_DIR/functional-tests.sh"
|
||||
|
||||
function start_minio_fs()
|
||||
{
|
||||
"${MINIO[@]}" server "${WORK_DIR}/fs-disk" >"$WORK_DIR/fs-minio.log" 2>&1 &
|
||||
minio_pid=$!
|
||||
sleep 3
|
||||
|
||||
echo "$minio_pid"
|
||||
}
|
||||
|
||||
function start_minio_xl()
|
||||
{
|
||||
"${MINIO[@]}" server "${WORK_DIR}/xl-disk1" "${WORK_DIR}/xl-disk2" "${WORK_DIR}/xl-disk3" "${WORK_DIR}/xl-disk4" >"$WORK_DIR/xl-minio.log" 2>&1 &
|
||||
minio_pid=$!
|
||||
sleep 3
|
||||
|
||||
echo "$minio_pid"
|
||||
}
|
||||
|
||||
function start_minio_dist()
|
||||
{
|
||||
declare -a minio_pids
|
||||
"${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9000.log" 2>&1 &
|
||||
minio_pids[0]=$!
|
||||
"${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9001.log" 2>&1 &
|
||||
minio_pids[1]=$!
|
||||
"${MINIO[@]}" server --address=:9002 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9002.log" 2>&1 &
|
||||
minio_pids[2]=$!
|
||||
"${MINIO[@]}" server --address=:9003 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9003.log" 2>&1 &
|
||||
minio_pids[3]=$!
|
||||
|
||||
sleep 30
|
||||
echo "${minio_pids[@]}"
|
||||
}
|
||||
|
||||
function start_minio_gateway_s3()
|
||||
{
|
||||
MINIO_ACCESS_KEY=Q3AM3UQ867SPQQA43P2F MINIO_SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG \
|
||||
"${MINIO[@]}" gateway s3 https://play.minio.io:9000 >"$WORK_DIR/minio-gateway-s3.log" 2>&1 &
|
||||
minio_pid=$!
|
||||
sleep 3
|
||||
|
||||
echo "$minio_pid"
|
||||
}
|
||||
|
||||
function run_test_fs()
|
||||
{
|
||||
minio_pid="$(start_minio_fs)"
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
kill "$minio_pid"
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
cat "$WORK_DIR/fs-minio.log"
|
||||
fi
|
||||
rm -f "$WORK_DIR/fs-minio.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_xl()
|
||||
{
|
||||
minio_pid="$(start_minio_xl)"
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
kill "$minio_pid"
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
cat "$WORK_DIR/xl-minio.log"
|
||||
fi
|
||||
rm -f "$WORK_DIR/xl-minio.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_dist()
|
||||
{
|
||||
minio_pids=( $(start_minio_dist) )
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
kill "$pid"
|
||||
done
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
echo "server1 log:"
|
||||
cat "$WORK_DIR/dist-minio-9000.log"
|
||||
echo "server2 log:"
|
||||
cat "$WORK_DIR/dist-minio-9001.log"
|
||||
echo "server3 log:"
|
||||
cat "$WORK_DIR/dist-minio-9002.log"
|
||||
echo "server4 log:"
|
||||
cat "$WORK_DIR/dist-minio-9003.log"
|
||||
fi
|
||||
|
||||
rm -f "$WORK_DIR/dist-minio-9000.log" "$WORK_DIR/dist-minio-9001.log" "$WORK_DIR/dist-minio-9002.log" "$WORK_DIR/dist-minio-9003.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_gateway_s3()
|
||||
{
|
||||
minio_pid="$(start_minio_gateway_s3)"
|
||||
|
||||
export ACCESS_KEY=Q3AM3UQ867SPQQA43P2F
|
||||
export SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
kill "$minio_pid"
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
cat "$WORK_DIR/minio-gateway-s3.log"
|
||||
fi
|
||||
rm -f "$WORK_DIR/minio-gateway-s3.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function __init__()
|
||||
{
|
||||
echo "Initializing environment"
|
||||
mkdir -p "$WORK_DIR"
|
||||
mkdir -p "$MINIO_CONFIG_DIR"
|
||||
mkdir -p "$MINT_DATA_DIR"
|
||||
|
||||
if ! go get -u github.com/minio/mc; then
|
||||
echo "failed to download https://github.com/minio/mc"
|
||||
exit 1
|
||||
fi
|
||||
/bin/cp -a "$(go env GOPATH)"/bin/mc "$WORK_DIR/mc"
|
||||
|
||||
chmod a+x "$WORK_DIR/mc"
|
||||
|
||||
shred -n 1 -s 1M - 1>"$FILE_1_MB" 2>/dev/null
|
||||
shred -n 1 -s 65M - 1>"$FILE_65_MB" 2>/dev/null
|
||||
|
||||
## version is purposefully set to '3' for minio to migrate configuration file
|
||||
echo '{"version": "3", "credential": {"accessKey": "minio", "secretKey": "minio123"}, "region": "us-east-1"}' > "$MINIO_CONFIG_DIR/config.json"
|
||||
|
||||
if ! wget -q -O "$FUNCTIONAL_TESTS" https://raw.githubusercontent.com/minio/mc/master/functional-tests.sh; then
|
||||
echo "failed to download https://raw.githubusercontent.com/minio/mc/master/functional-tests.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod a+x "$FUNCTIONAL_TESTS"
|
||||
}
|
||||
|
||||
function main()
|
||||
{
|
||||
echo "Testing in FS setup"
|
||||
if ! run_test_fs; then
|
||||
echo "FAILED"
|
||||
rm -fr "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in XL setup"
|
||||
if ! run_test_xl; then
|
||||
echo "FAILED"
|
||||
rm -fr "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Distribute XL setup"
|
||||
if ! run_test_dist; then
|
||||
echo "FAILED"
|
||||
rm -fr "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Gateway S3 setup"
|
||||
if ! run_test_gateway_s3; then
|
||||
echo "FAILED"
|
||||
rm -fr "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -fr "$WORK_DIR"
|
||||
}
|
||||
|
||||
( __init__ "$@" && main "$@" )
|
||||
rv=$?
|
||||
rm -fr "$WORK_DIR"
|
||||
exit "$rv"
|
||||
+444
-74
@@ -17,32 +17,43 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
minioAdminOpHeader = "X-Minio-Operation"
|
||||
minioAdminOpHeader = "X-Minio-Operation"
|
||||
minioConfigTmpFormat = "config-%s.json"
|
||||
)
|
||||
|
||||
// Type-safe query params.
|
||||
type mgmtQueryKey string
|
||||
|
||||
// Only valid query params for list/clear locks management APIs.
|
||||
// Only valid query params for mgmt admin APIs.
|
||||
const (
|
||||
mgmtBucket mgmtQueryKey = "bucket"
|
||||
mgmtObject mgmtQueryKey = "object"
|
||||
mgmtPrefix mgmtQueryKey = "prefix"
|
||||
mgmtLockDuration mgmtQueryKey = "duration"
|
||||
mgmtDelimiter mgmtQueryKey = "delimiter"
|
||||
mgmtMarker mgmtQueryKey = "marker"
|
||||
mgmtMaxKey mgmtQueryKey = "max-key"
|
||||
mgmtDryRun mgmtQueryKey = "dry-run"
|
||||
mgmtBucket mgmtQueryKey = "bucket"
|
||||
mgmtObject mgmtQueryKey = "object"
|
||||
mgmtPrefix mgmtQueryKey = "prefix"
|
||||
mgmtLockDuration mgmtQueryKey = "duration"
|
||||
mgmtDelimiter mgmtQueryKey = "delimiter"
|
||||
mgmtMarker mgmtQueryKey = "marker"
|
||||
mgmtKeyMarker mgmtQueryKey = "key-marker"
|
||||
mgmtMaxKey mgmtQueryKey = "max-key"
|
||||
mgmtDryRun mgmtQueryKey = "dry-run"
|
||||
mgmtUploadIDMarker mgmtQueryKey = "upload-id-marker"
|
||||
mgmtMaxUploads mgmtQueryKey = "max-uploads"
|
||||
mgmtUploadID mgmtQueryKey = "upload-id"
|
||||
)
|
||||
|
||||
// ServerVersion - server version
|
||||
@@ -159,18 +170,12 @@ func (adminAPI adminAPIHandlers) ServiceCredentialsHandler(w http.ResponseWriter
|
||||
return
|
||||
}
|
||||
|
||||
// Check passed credentials
|
||||
err = validateAuthKeys(req.Username, req.Password)
|
||||
creds, err := auth.CreateCredentials(req.Username, req.Password)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
creds := credential{
|
||||
AccessKey: req.Username,
|
||||
SecretKey: req.Password,
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to update credentials
|
||||
updateErrs := updateCredsOnPeers(creds)
|
||||
for peer, err := range updateErrs {
|
||||
@@ -178,8 +183,14 @@ func (adminAPI adminAPIHandlers) ServiceCredentialsHandler(w http.ResponseWriter
|
||||
}
|
||||
|
||||
// Update local credentials in memory.
|
||||
serverConfig.SetCredential(creds)
|
||||
prevCred := serverConfig.SetCredential(creds)
|
||||
|
||||
// Save credentials to config file
|
||||
if err = serverConfig.Save(); err != nil {
|
||||
// Save the current creds when failed to update.
|
||||
serverConfig.SetCredential(prevCred)
|
||||
|
||||
errorIf(err, "Unable to update the config with new credentials.")
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -205,14 +216,45 @@ type ServerConnStats struct {
|
||||
Throughput uint64 `json:"throughput,omitempty"`
|
||||
}
|
||||
|
||||
// ServerInfo holds the information that will be returned by ServerInfo API
|
||||
type ServerInfo struct {
|
||||
// ServerHTTPMethodStats holds total number of HTTP operations from/to the server,
|
||||
// including the average duration the call was spent.
|
||||
type ServerHTTPMethodStats struct {
|
||||
Count uint64 `json:"count"`
|
||||
AvgDuration string `json:"avgDuration"`
|
||||
}
|
||||
|
||||
// ServerHTTPStats holds all type of http operations performed to/from the server
|
||||
// including their average execution time.
|
||||
type ServerHTTPStats struct {
|
||||
TotalHEADStats ServerHTTPMethodStats `json:"totalHEADs"`
|
||||
SuccessHEADStats ServerHTTPMethodStats `json:"successHEADs"`
|
||||
TotalGETStats ServerHTTPMethodStats `json:"totalGETs"`
|
||||
SuccessGETStats ServerHTTPMethodStats `json:"successGETs"`
|
||||
TotalPUTStats ServerHTTPMethodStats `json:"totalPUTs"`
|
||||
SuccessPUTStats ServerHTTPMethodStats `json:"successPUTs"`
|
||||
TotalPOSTStats ServerHTTPMethodStats `json:"totalPOSTs"`
|
||||
SuccessPOSTStats ServerHTTPMethodStats `json:"successPOSTs"`
|
||||
TotalDELETEStats ServerHTTPMethodStats `json:"totalDELETEs"`
|
||||
SuccessDELETEStats ServerHTTPMethodStats `json:"successDELETEs"`
|
||||
}
|
||||
|
||||
// ServerInfoData holds storage, connections and other
|
||||
// information of a given server.
|
||||
type ServerInfoData struct {
|
||||
StorageInfo StorageInfo `json:"storage"`
|
||||
ConnStats ServerConnStats `json:"network"`
|
||||
HTTPStats ServerHTTPStats `json:"http"`
|
||||
Properties ServerProperties `json:"server"`
|
||||
}
|
||||
|
||||
// ServerInfoHandler - GET /?server-info
|
||||
// ServerInfo holds server information result of one node
|
||||
type ServerInfo struct {
|
||||
Error string `json:"error"`
|
||||
Addr string `json:"addr"`
|
||||
Data *ServerInfoData `json:"data"`
|
||||
}
|
||||
|
||||
// ServerInfoHandler - GET /?info
|
||||
// ----------
|
||||
// Get server information
|
||||
func (adminAPI adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -223,58 +265,43 @@ func (adminAPI adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
// Build storage info
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
storage := objLayer.StorageInfo()
|
||||
// Web service response
|
||||
reply := make([]ServerInfo, len(globalAdminPeers))
|
||||
|
||||
// Build list of enabled ARNs queues
|
||||
var arns []string
|
||||
for queueArn := range globalEventNotifier.GetAllExternalTargets() {
|
||||
arns = append(arns, queueArn)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Gather server information for all nodes
|
||||
for i, p := range globalAdminPeers {
|
||||
wg.Add(1)
|
||||
|
||||
// Gather information from a peer in a goroutine
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
|
||||
// Initialize server info at index
|
||||
reply[idx] = ServerInfo{Addr: peer.addr}
|
||||
|
||||
serverInfoData, err := peer.cmdRunner.ServerInfoData()
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to get server info from %s.", peer.addr)
|
||||
reply[idx].Error = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
reply[idx].Data = &serverInfoData
|
||||
}(i, p)
|
||||
}
|
||||
|
||||
// Fetch uptimes from all peers. This may fail to due to lack
|
||||
// of read-quorum availability.
|
||||
uptime, err := getPeerUptimes(globalAdminPeers)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
errorIf(err, "Unable to get uptime from majority of servers.")
|
||||
return
|
||||
}
|
||||
|
||||
// Build server properties information
|
||||
properties := ServerProperties{
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
Region: serverConfig.GetRegion(),
|
||||
SQSARN: arns,
|
||||
Uptime: uptime,
|
||||
}
|
||||
|
||||
// Build network info
|
||||
connStats := ServerConnStats{
|
||||
TotalInputBytes: globalConnStats.getTotalInputBytes(),
|
||||
TotalOutputBytes: globalConnStats.getTotalOutputBytes(),
|
||||
}
|
||||
|
||||
// Build the whole returned information
|
||||
info := ServerInfo{
|
||||
StorageInfo: storage,
|
||||
ConnStats: connStats,
|
||||
Properties: properties,
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(info)
|
||||
jsonBytes, err := json.Marshal(reply)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
errorIf(err, "Failed to marshal storage info into json.")
|
||||
return
|
||||
}
|
||||
|
||||
// Reply with storage information (across nodes in a
|
||||
// distributed setup) as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
@@ -397,8 +424,57 @@ func (adminAPI adminAPIHandlers) ClearLocksHandler(w http.ResponseWriter, r *htt
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
}
|
||||
|
||||
// validateHealQueryParams - Validates query params for heal list management API.
|
||||
func validateHealQueryParams(vars url.Values) (string, string, string, string, int, APIErrorCode) {
|
||||
// ListUploadsHealHandler - similar to listObjectsHealHandler
|
||||
// GET
|
||||
// /?heal&bucket=mybucket&prefix=myprefix&key-marker=mymarker&upload-id-marker=myuploadid&delimiter=mydelimiter&max-uploads=1000
|
||||
// - bucket is mandatory query parameter
|
||||
// - rest are optional query parameters List upto maxKey objects that
|
||||
// need healing in a given bucket matching the given prefix.
|
||||
func (adminAPI adminAPIHandlers) ListUploadsHealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Get object layer instance.
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request signature.
|
||||
adminAPIErr := checkRequestAuthType(r, "", "", "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponse(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate query params.
|
||||
vars := r.URL.Query()
|
||||
bucket := vars.Get(string(mgmtBucket))
|
||||
prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, _ := getBucketMultipartResources(r.URL.Query())
|
||||
|
||||
if err := checkListMultipartArgs(bucket, prefix, keyMarker, uploadIDMarker, delimiter, objLayer); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if maxUploads <= 0 || maxUploads > maxUploadsList {
|
||||
writeErrorResponse(w, ErrInvalidMaxUploads, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the list objects to be healed.
|
||||
listMultipartInfos, err := objLayer.ListUploadsHeal(bucket, prefix,
|
||||
keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
listResponse := generateListMultipartUploadsResponse(bucket, listMultipartInfos)
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, encodeResponse(listResponse))
|
||||
}
|
||||
|
||||
// extractListObjectsHealQuery - Validates query params for heal objects list management API.
|
||||
func extractListObjectsHealQuery(vars url.Values) (string, string, string, string, int, APIErrorCode) {
|
||||
bucket := vars.Get(string(mgmtBucket))
|
||||
prefix := vars.Get(string(mgmtPrefix))
|
||||
marker := vars.Get(string(mgmtMarker))
|
||||
@@ -415,10 +491,13 @@ func validateHealQueryParams(vars url.Values) (string, string, string, string, i
|
||||
return "", "", "", "", 0, ErrInvalidObjectName
|
||||
}
|
||||
|
||||
// check if maxKey is a valid integer.
|
||||
maxKey, err := strconv.Atoi(maxKeyStr)
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, ErrInvalidMaxKeys
|
||||
// check if maxKey is a valid integer, if present.
|
||||
var maxKey int
|
||||
var err error
|
||||
if maxKeyStr != "" {
|
||||
if maxKey, err = strconv.Atoi(maxKeyStr); err != nil {
|
||||
return "", "", "", "", 0, ErrInvalidMaxKeys
|
||||
}
|
||||
}
|
||||
|
||||
// Validate prefix, marker, delimiter and maxKey.
|
||||
@@ -451,7 +530,7 @@ func (adminAPI adminAPIHandlers) ListObjectsHealHandler(w http.ResponseWriter, r
|
||||
|
||||
// Validate query params.
|
||||
vars := r.URL.Query()
|
||||
bucket, prefix, marker, delimiter, maxKey, adminAPIErr := validateHealQueryParams(vars)
|
||||
bucket, prefix, marker, delimiter, maxKey, adminAPIErr := extractListObjectsHealQuery(vars)
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponse(w, adminAPIErr, r.URL)
|
||||
return
|
||||
@@ -550,6 +629,42 @@ func isDryRun(qval url.Values) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// healResult - represents result of a heal operation like
|
||||
// heal-object, heal-upload.
|
||||
type healResult struct {
|
||||
State healState `json:"state"`
|
||||
}
|
||||
|
||||
// healState - different states of heal operation
|
||||
type healState int
|
||||
|
||||
const (
|
||||
// healNone - none of the disks healed
|
||||
healNone healState = iota
|
||||
// healPartial - some disks were healed, others were offline
|
||||
healPartial
|
||||
// healOK - all disks were healed
|
||||
healOK
|
||||
)
|
||||
|
||||
// newHealResult - returns healResult given number of disks healed and
|
||||
// number of disks offline
|
||||
func newHealResult(numHealedDisks, numOfflineDisks int) healResult {
|
||||
var state healState
|
||||
switch {
|
||||
case numHealedDisks == 0:
|
||||
state = healNone
|
||||
|
||||
case numOfflineDisks > 0:
|
||||
state = healPartial
|
||||
|
||||
default:
|
||||
state = healOK
|
||||
}
|
||||
|
||||
return healResult{State: state}
|
||||
}
|
||||
|
||||
// HealObjectHandler - POST /?heal&bucket=mybucket&object=myobject&dry-run
|
||||
// - x-minio-operation = object
|
||||
// - bucket and object are both mandatory query parameters
|
||||
@@ -592,14 +707,95 @@ func (adminAPI adminAPIHandlers) HealObjectHandler(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
err := objLayer.HealObject(bucket, object)
|
||||
numOfflineDisks, numHealedDisks, err := objLayer.HealObject(bucket, object)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(newHealResult(numHealedDisks, numOfflineDisks))
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Return 200 on success.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
}
|
||||
|
||||
// HealUploadHandler - POST /?heal&bucket=mybucket&object=myobject&upload-id=myuploadID&dry-run
|
||||
// - x-minio-operation = upload
|
||||
// - bucket, object and upload-id are mandatory query parameters
|
||||
// Heal a given upload, if present.
|
||||
func (adminAPI adminAPIHandlers) HealUploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Get object layer instance.
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request signature.
|
||||
adminAPIErr := checkRequestAuthType(r, "", "", "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponse(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := r.URL.Query()
|
||||
bucket := vars.Get(string(mgmtBucket))
|
||||
object := vars.Get(string(mgmtObject))
|
||||
uploadID := vars.Get(string(mgmtUploadID))
|
||||
uploadObj := path.Join(bucket, object, uploadID)
|
||||
|
||||
// Validate bucket and object names as supplied via query
|
||||
// parameters.
|
||||
if err := checkBucketAndObjectNames(bucket, object); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the bucket and object w.r.t backend representation
|
||||
// of an upload.
|
||||
if err := checkBucketAndObjectNames(minioMetaMultipartBucket,
|
||||
uploadObj); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if upload exists.
|
||||
if _, err := objLayer.GetObjectInfo(minioMetaMultipartBucket,
|
||||
uploadObj); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// if dry-run is set in query params then perform validations
|
||||
// and return success.
|
||||
if isDryRun(vars) {
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
return
|
||||
}
|
||||
|
||||
//We are able to use HealObject for healing an upload since an
|
||||
//ongoing upload has the same backend representation as an
|
||||
//object. The 'object' corresponding to a given bucket,
|
||||
//object and uploadID is
|
||||
//.minio.sys/multipart/bucket/object/uploadID.
|
||||
numOfflineDisks, numHealedDisks, err := objLayer.HealObject(minioMetaMultipartBucket, uploadObj)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(newHealResult(numHealedDisks, numOfflineDisks))
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Return 200 on success.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
}
|
||||
|
||||
// HealFormatHandler - POST /?heal&dry-run
|
||||
@@ -644,15 +840,19 @@ func (adminAPI adminAPIHandlers) HealFormatHandler(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
// Wrap into retrying disks
|
||||
retryingDisks := initRetryableStorageDisks(bootstrapDisks,
|
||||
time.Millisecond, time.Millisecond*5)
|
||||
|
||||
// Heal format.json on available storage.
|
||||
err = healFormatXL(bootstrapDisks)
|
||||
err = healFormatXL(retryingDisks)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Instantiate new object layer with newly formatted storage.
|
||||
newObjectAPI, err := newXLObjects(bootstrapDisks)
|
||||
newObjectAPI, err := newXLObjects(retryingDisks)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -672,3 +872,173 @@ func (adminAPI adminAPIHandlers) HealFormatHandler(w http.ResponseWriter, r *htt
|
||||
// Return 200 on success.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
// GetConfigHandler - GET /?config
|
||||
// - x-minio-operation = get
|
||||
// Get config.json of this minio setup.
|
||||
func (adminAPI adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Validate request signature.
|
||||
adminAPIErr := checkRequestAuthType(r, "", "", "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponse(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// check if objectLayer is initialized, if not return.
|
||||
if newObjectLayerFn() == nil {
|
||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Get config.json from all nodes. In a single node setup, it
|
||||
// returns local config.json.
|
||||
configBytes, err := getPeerConfig(globalAdminPeers)
|
||||
if err != nil {
|
||||
errorIf(err, "Failed to get config from peers")
|
||||
writeErrorResponse(w, toAdminAPIErrCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, configBytes)
|
||||
}
|
||||
|
||||
// toAdminAPIErrCode - converts errXLWriteQuorum error to admin API
|
||||
// specific error.
|
||||
func toAdminAPIErrCode(err error) APIErrorCode {
|
||||
switch err {
|
||||
case errXLWriteQuorum:
|
||||
return ErrAdminConfigNoQuorum
|
||||
}
|
||||
return toAPIErrorCode(err)
|
||||
}
|
||||
|
||||
// SetConfigResult - represents detailed results of a set-config
|
||||
// operation.
|
||||
type nodeSummary struct {
|
||||
Name string `json:"name"`
|
||||
ErrSet bool `json:"errSet"`
|
||||
ErrMsg string `json:"errMsg"`
|
||||
}
|
||||
|
||||
type setConfigResult struct {
|
||||
NodeResults []nodeSummary `json:"nodeResults"`
|
||||
Status bool `json:"status"`
|
||||
}
|
||||
|
||||
// writeSetConfigResponse - writes setConfigResult value as json depending on the status.
|
||||
func writeSetConfigResponse(w http.ResponseWriter, peers adminPeers, errs []error, status bool, reqURL *url.URL) {
|
||||
var nodeResults []nodeSummary
|
||||
// Build nodeResults based on error values received during
|
||||
// set-config operation.
|
||||
for i := range errs {
|
||||
nodeResults = append(nodeResults, nodeSummary{
|
||||
Name: peers[i].addr,
|
||||
ErrSet: errs[i] != nil,
|
||||
ErrMsg: fmt.Sprintf("%v", errs[i]),
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
result := setConfigResult{
|
||||
Status: status,
|
||||
NodeResults: nodeResults,
|
||||
}
|
||||
|
||||
// The following elaborate json encoding is to avoid escaping
|
||||
// '<', '>' in <nil>. Note: json.Encoder.Encode() adds a
|
||||
// gratuitous "\n".
|
||||
var resultBuf bytes.Buffer
|
||||
enc := json.NewEncoder(&resultBuf)
|
||||
enc.SetEscapeHTML(false)
|
||||
jsonErr := enc.Encode(result)
|
||||
if jsonErr != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(jsonErr), reqURL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, resultBuf.Bytes())
|
||||
return
|
||||
}
|
||||
|
||||
// SetConfigHandler - PUT /?config
|
||||
// - x-minio-operation = set
|
||||
func (adminAPI adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request signature.
|
||||
adminAPIErr := checkRequestAuthType(r, "", "", "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponse(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Read configuration bytes from request body.
|
||||
configBytes, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
errorIf(err, "Failed to read config from request body.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var config serverConfigV20
|
||||
err = json.Unmarshal(configBytes, &config)
|
||||
|
||||
if err != nil {
|
||||
errorIf(err, "Failed to unmarshal config from request body.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if globalIsEnvCreds {
|
||||
creds := serverConfig.GetCredential()
|
||||
if config.Credential.AccessKey != creds.AccessKey ||
|
||||
config.Credential.SecretKey != creds.SecretKey {
|
||||
writeErrorResponse(w, ErrAdminCredentialsMismatch, r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Write config received from request onto a temporary file on
|
||||
// all nodes.
|
||||
tmpFileName := fmt.Sprintf(minioConfigTmpFormat, mustGetUUID())
|
||||
errs := writeTmpConfigPeers(globalAdminPeers, tmpFileName, configBytes)
|
||||
|
||||
// Check if the operation succeeded in quorum or more nodes.
|
||||
rErr := reduceWriteQuorumErrs(errs, nil, len(globalAdminPeers)/2+1)
|
||||
if rErr != nil {
|
||||
writeSetConfigResponse(w, globalAdminPeers, errs, false, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Take a lock on minio/config.json. NB minio is a reserved
|
||||
// bucket name and wouldn't conflict with normal object
|
||||
// operations.
|
||||
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
|
||||
if configLock.GetLock(globalObjectTimeout) != nil {
|
||||
writeErrorResponse(w, ErrOperationTimedOut, r.URL)
|
||||
return
|
||||
}
|
||||
defer configLock.Unlock()
|
||||
|
||||
// Rename the temporary config file to config.json
|
||||
errs = commitConfigPeers(globalAdminPeers, tmpFileName)
|
||||
rErr = reduceWriteQuorumErrs(errs, nil, len(globalAdminPeers)/2+1)
|
||||
if rErr != nil {
|
||||
writeSetConfigResponse(w, globalAdminPeers, errs, false, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// serverMux (cmd/server-mux.go) implements graceful shutdown,
|
||||
// where all listeners are closed and process restart/shutdown
|
||||
// happens after 5s or completion of all ongoing http
|
||||
// requests, whichever is earlier.
|
||||
writeSetConfigResponse(w, globalAdminPeers, errs, true, r.URL)
|
||||
|
||||
// Restart all node for the modified config to take effect.
|
||||
sendServiceCmd(globalAdminPeers, serviceRestart)
|
||||
}
|
||||
|
||||
+695
-57
@@ -20,16 +20,117 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
router "github.com/gorilla/mux"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
var configJSON = []byte(`{
|
||||
"version": "13",
|
||||
"credential": {
|
||||
"accessKey": "minio",
|
||||
"secretKey": "minio123"
|
||||
},
|
||||
"region": "us-west-1",
|
||||
"logger": {
|
||||
"console": {
|
||||
"enable": true,
|
||||
"level": "fatal"
|
||||
},
|
||||
"file": {
|
||||
"enable": false,
|
||||
"fileName": "",
|
||||
"level": ""
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"amqp": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"exchange": "",
|
||||
"routingKey": "",
|
||||
"exchangeType": "",
|
||||
"mandatory": false,
|
||||
"immediate": false,
|
||||
"durable": false,
|
||||
"internal": false,
|
||||
"noWait": false,
|
||||
"autoDeleted": false
|
||||
}
|
||||
},
|
||||
"nats": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"subject": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"token": "",
|
||||
"secure": false,
|
||||
"pingInterval": 0,
|
||||
"streaming": {
|
||||
"enable": false,
|
||||
"clusterID": "",
|
||||
"clientID": "",
|
||||
"async": false,
|
||||
"maxPubAcksInflight": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"elasticsearch": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"index": ""
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"password": "",
|
||||
"key": ""
|
||||
}
|
||||
},
|
||||
"postgresql": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"connectionString": "",
|
||||
"table": "",
|
||||
"host": "",
|
||||
"port": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"database": ""
|
||||
}
|
||||
},
|
||||
"kafka": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"brokers": null,
|
||||
"topic": ""
|
||||
}
|
||||
},
|
||||
"webhook": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"endpoint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
// adminXLTestBed - encapsulates subsystems that need to be setup for
|
||||
// admin-handler unit tests.
|
||||
type adminXLTestBed struct {
|
||||
@@ -57,14 +158,9 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
||||
}
|
||||
|
||||
// Initialize boot time
|
||||
globalBootTime = time.Now().UTC()
|
||||
globalBootTime = UTCNow()
|
||||
|
||||
// Set globalEndpoints for a single node XL setup.
|
||||
for _, xlDir := range xlDirs {
|
||||
globalEndpoints = append(globalEndpoints, &url.URL{
|
||||
Path: xlDir,
|
||||
})
|
||||
}
|
||||
globalEndpoints = mustGetNewEndpointList(xlDirs...)
|
||||
|
||||
// Set globalIsXL to indicate that the setup uses an erasure code backend.
|
||||
globalIsXL = true
|
||||
@@ -88,7 +184,7 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
||||
// TearDown - method that resets the test bed for subsequent unit
|
||||
// tests to start afresh.
|
||||
func (atb *adminXLTestBed) TearDown() {
|
||||
removeAll(atb.configPath)
|
||||
os.RemoveAll(atb.configPath)
|
||||
removeRoots(atb.xlDirs)
|
||||
resetTestGlobals()
|
||||
}
|
||||
@@ -168,7 +264,7 @@ func testServiceSignalReceiver(cmd cmdType, t *testing.T) {
|
||||
|
||||
// getServiceCmdRequest - Constructs a management REST API request for service
|
||||
// subcommands for a given cmdType value.
|
||||
func getServiceCmdRequest(cmd cmdType, cred credential, body []byte) (*http.Request, error) {
|
||||
func getServiceCmdRequest(cmd cmdType, cred auth.Credentials, body []byte) (*http.Request, error) {
|
||||
req, err := newTestRequest(cmd.apiMethod(), "/?service", 0, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -202,14 +298,8 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
||||
// Initialize admin peers to make admin RPC calls. Note: In a
|
||||
// single node setup, this degenerates to a simple function
|
||||
// call under the hood.
|
||||
eps, err := parseStorageEndpoints([]string{"http://127.0.0.1"})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse storage end point - %v", err)
|
||||
}
|
||||
|
||||
// Set globalMinioAddr to be able to distinguish local endpoints from remote.
|
||||
globalMinioAddr = eps[0].Host
|
||||
initGlobalAdminPeers(eps)
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
// Setting up a go routine to simulate ServerMux's
|
||||
// handleServiceSignals for stop and restart commands.
|
||||
@@ -268,14 +358,8 @@ func TestServiceSetCreds(t *testing.T) {
|
||||
// Initialize admin peers to make admin RPC calls. Note: In a
|
||||
// single node setup, this degenerates to a simple function
|
||||
// call under the hood.
|
||||
eps, err := parseStorageEndpoints([]string{"http://127.0.0.1"})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse storage end point - %v", err)
|
||||
}
|
||||
|
||||
// Set globalMinioAddr to be able to distinguish local endpoints from remote.
|
||||
globalMinioAddr = eps[0].Host
|
||||
initGlobalAdminPeers(eps)
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
credentials := serverConfig.GetCredential()
|
||||
var body []byte
|
||||
@@ -356,14 +440,8 @@ func TestListLocksHandler(t *testing.T) {
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
eps, err := parseStorageEndpoints([]string{"http://127.0.0.1"})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse storage end point - %v", err)
|
||||
}
|
||||
|
||||
// Set globalMinioAddr to be able to distinguish local endpoints from remote.
|
||||
globalMinioAddr = eps[0].Host
|
||||
initGlobalAdminPeers(eps)
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
testCases := []struct {
|
||||
bucket string
|
||||
@@ -431,11 +509,7 @@ func TestClearLocksHandler(t *testing.T) {
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
eps, err := parseStorageEndpoints([]string{"http://127.0.0.1"})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse storage end point - %v", err)
|
||||
}
|
||||
initGlobalAdminPeers(eps)
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
testCases := []struct {
|
||||
bucket string
|
||||
@@ -643,7 +717,7 @@ func TestValidateHealQueryParams(t *testing.T) {
|
||||
}
|
||||
for i, test := range testCases {
|
||||
vars := mkListObjectsQueryVal(test.bucket, test.prefix, test.marker, test.delimiter, test.maxKeys)
|
||||
_, _, _, _, _, actualErr := validateHealQueryParams(vars)
|
||||
_, _, _, _, _, actualErr := extractListObjectsHealQuery(vars)
|
||||
if actualErr != test.apiErr {
|
||||
t.Errorf("Test %d - Expected %v but received %v",
|
||||
i+1, getAPIError(test.apiErr), getAPIError(actualErr))
|
||||
@@ -659,7 +733,7 @@ func TestListObjectsHealHandler(t *testing.T) {
|
||||
}
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
err = adminTestBed.objLayer.MakeBucket("mybucket")
|
||||
err = adminTestBed.objLayer.MakeBucketWithLocation("mybucket", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make bucket - %v", err)
|
||||
}
|
||||
@@ -759,9 +833,6 @@ func TestListObjectsHealHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
if i != 0 {
|
||||
continue
|
||||
}
|
||||
queryVal := mkListObjectsQueryVal(test.bucket, test.prefix, test.marker, test.delimiter, test.maxKeys)
|
||||
req, err := newTestRequest("GET", "/?"+queryVal.Encode(), 0, nil)
|
||||
if err != nil {
|
||||
@@ -790,7 +861,7 @@ func TestHealBucketHandler(t *testing.T) {
|
||||
}
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
err = adminTestBed.objLayer.MakeBucket("mybucket")
|
||||
err = adminTestBed.objLayer.MakeBucketWithLocation("mybucket", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make bucket - %v", err)
|
||||
}
|
||||
@@ -867,13 +938,13 @@ func TestHealObjectHandler(t *testing.T) {
|
||||
// Create an object myobject under bucket mybucket.
|
||||
bucketName := "mybucket"
|
||||
objName := "myobject"
|
||||
err = adminTestBed.objLayer.MakeBucket(bucketName)
|
||||
err = adminTestBed.objLayer.MakeBucketWithLocation(bucketName, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make bucket %s - %v", bucketName, err)
|
||||
}
|
||||
|
||||
_, err = adminTestBed.objLayer.PutObject(bucketName, objName,
|
||||
int64(len("hello")), bytes.NewReader([]byte("hello")), nil, "")
|
||||
mustGetHashReader(t, bytes.NewReader([]byte("hello")), int64(len("hello")), "", ""), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create %s - %v", objName, err)
|
||||
}
|
||||
@@ -954,6 +1025,160 @@ func TestHealObjectHandler(t *testing.T) {
|
||||
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.statusCode, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// buildAdminRequest - helper function to build an admin API request.
|
||||
func buildAdminRequest(queryVal url.Values, opHdr, method string,
|
||||
contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error) {
|
||||
req, err := newTestRequest(method, "/?"+queryVal.Encode(), contentLength, bodySeeker)
|
||||
if err != nil {
|
||||
return nil, traceError(err)
|
||||
}
|
||||
|
||||
req.Header.Set(minioAdminOpHeader, opHdr)
|
||||
|
||||
cred := serverConfig.GetCredential()
|
||||
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
||||
if err != nil {
|
||||
return nil, traceError(err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// mkHealUploadQuery - helper to build HealUploadHandler query string.
|
||||
func mkHealUploadQuery(bucket, object, uploadID, dryRun string) url.Values {
|
||||
queryVal := url.Values{}
|
||||
queryVal.Set(string(mgmtBucket), bucket)
|
||||
queryVal.Set(string(mgmtObject), object)
|
||||
queryVal.Set(string(mgmtUploadID), uploadID)
|
||||
queryVal.Set("heal", "")
|
||||
queryVal.Set(string(mgmtDryRun), dryRun)
|
||||
return queryVal
|
||||
}
|
||||
|
||||
// TestHealUploadHandler - test for HealUploadHandler.
|
||||
func TestHealUploadHandler(t *testing.T) {
|
||||
adminTestBed, err := prepareAdminXLTestBed()
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
||||
}
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
// Create an object myobject under bucket mybucket.
|
||||
bucketName := "mybucket"
|
||||
objName := "myobject"
|
||||
err = adminTestBed.objLayer.MakeBucketWithLocation(bucketName, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make bucket %s - %v", bucketName, err)
|
||||
}
|
||||
|
||||
// Create a new multipart upload.
|
||||
uploadID, err := adminTestBed.objLayer.NewMultipartUpload(bucketName, objName, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create a new multipart upload %s/%s - %v",
|
||||
bucketName, objName, err)
|
||||
}
|
||||
|
||||
// Upload a part.
|
||||
partID := 1
|
||||
_, err = adminTestBed.objLayer.PutObjectPart(bucketName, objName, uploadID,
|
||||
partID, mustGetHashReader(t, bytes.NewReader([]byte("hello")), int64(len("hello")), "", ""))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to upload part %d of %s/%s - %v", partID,
|
||||
bucketName, objName, err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
bucket string
|
||||
object string
|
||||
dryrun string
|
||||
statusCode int
|
||||
}{
|
||||
// 1. Valid test case.
|
||||
{
|
||||
bucket: bucketName,
|
||||
object: objName,
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
// 2. Invalid bucket name.
|
||||
{
|
||||
bucket: `invalid\\Bucket`,
|
||||
object: "myobject",
|
||||
statusCode: http.StatusBadRequest,
|
||||
},
|
||||
// 3. Bucket not found.
|
||||
{
|
||||
bucket: "bucketnotfound",
|
||||
object: "myobject",
|
||||
statusCode: http.StatusNotFound,
|
||||
},
|
||||
// 4. Invalid object name.
|
||||
{
|
||||
bucket: bucketName,
|
||||
object: `invalid\\Object`,
|
||||
statusCode: http.StatusBadRequest,
|
||||
},
|
||||
// 5. Object not found.
|
||||
{
|
||||
bucket: bucketName,
|
||||
object: "objectnotfound",
|
||||
statusCode: http.StatusNotFound,
|
||||
},
|
||||
// 6. Valid test case with dry-run.
|
||||
{
|
||||
bucket: bucketName,
|
||||
object: objName,
|
||||
dryrun: "yes",
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
for i, test := range testCases {
|
||||
// Prepare query params.
|
||||
queryVal := mkHealUploadQuery(test.bucket, test.object, uploadID, test.dryrun)
|
||||
req, err1 := buildAdminRequest(queryVal, "upload", http.MethodPost, 0, nil)
|
||||
if err1 != nil {
|
||||
t.Fatalf("Test %d - Failed to construct heal object request - %v", i+1, err1)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
adminTestBed.mux.ServeHTTP(rec, req)
|
||||
if test.statusCode != rec.Code {
|
||||
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.statusCode, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
sample := testCases[0]
|
||||
// Modify authorization header after signing to test signature
|
||||
// mismatch handling.
|
||||
queryVal := mkHealUploadQuery(sample.bucket, sample.object, uploadID, sample.dryrun)
|
||||
req, err := buildAdminRequest(queryVal, "upload", "POST", 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct heal object request - %v", err)
|
||||
}
|
||||
|
||||
// Set x-amz-date to a date different than time of signing.
|
||||
req.Header.Set("x-amz-date", time.Time{}.Format(iso8601Format))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
adminTestBed.mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Errorf("Expected %d but received %d", http.StatusBadRequest, rec.Code)
|
||||
}
|
||||
|
||||
// Set objectAPI to nil to test Server not initialized case.
|
||||
resetGlobalObjectAPI()
|
||||
queryVal = mkHealUploadQuery(sample.bucket, sample.object, uploadID, sample.dryrun)
|
||||
req, err = buildAdminRequest(queryVal, "upload", "POST", 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct heal object request - %v", err)
|
||||
}
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
adminTestBed.mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusServiceUnavailable {
|
||||
t.Errorf("Expected %d but received %d", http.StatusServiceUnavailable, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHealFormatHandler - test for HealFormatHandler.
|
||||
@@ -967,24 +1192,437 @@ func TestHealFormatHandler(t *testing.T) {
|
||||
// Prepare query params for heal-format mgmt REST API.
|
||||
queryVal := url.Values{}
|
||||
queryVal.Set("heal", "")
|
||||
req, err := newTestRequest("POST", "/?"+queryVal.Encode(), 0, nil)
|
||||
req, err := buildAdminRequest(queryVal, "format", "POST", 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct heal object request - %v", err)
|
||||
}
|
||||
|
||||
// Set x-minio-operation header to format.
|
||||
req.Header.Set(minioAdminOpHeader, "format")
|
||||
|
||||
// Sign the request using signature v4.
|
||||
cred := serverConfig.GetCredential()
|
||||
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to sign heal object request - %v", err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
adminTestBed.mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Expected to succeed but failed with %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetConfigHandler - test for GetConfigHandler.
|
||||
func TestGetConfigHandler(t *testing.T) {
|
||||
adminTestBed, err := prepareAdminXLTestBed()
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
||||
}
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
// Prepare query params for get-config mgmt REST API.
|
||||
queryVal := url.Values{}
|
||||
queryVal.Set("config", "")
|
||||
|
||||
req, err := buildAdminRequest(queryVal, "get", http.MethodGet, 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct get-config object request - %v", err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
adminTestBed.mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Expected to succeed but failed with %d", rec.Code)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestSetConfigHandler - test for SetConfigHandler.
|
||||
func TestSetConfigHandler(t *testing.T) {
|
||||
adminTestBed, err := prepareAdminXLTestBed()
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
||||
}
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
// SetConfigHandler restarts minio setup - need to start a
|
||||
// signal receiver to receive on globalServiceSignalCh.
|
||||
go testServiceSignalReceiver(restartCmd, t)
|
||||
|
||||
// Prepare query params for set-config mgmt REST API.
|
||||
queryVal := url.Values{}
|
||||
queryVal.Set("config", "")
|
||||
|
||||
req, err := buildAdminRequest(queryVal, "set", http.MethodPut, int64(len(configJSON)),
|
||||
bytes.NewReader(configJSON))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct get-config object request - %v", err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
adminTestBed.mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Expected to succeed but failed with %d", rec.Code)
|
||||
}
|
||||
|
||||
result := setConfigResult{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&result)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decode set config result json %v", err)
|
||||
}
|
||||
|
||||
if !result.Status {
|
||||
t.Error("Expected set-config to succeed, but failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminServerInfo(t *testing.T) {
|
||||
adminTestBed, err := prepareAdminXLTestBed()
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
||||
}
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
// Prepare query params for set-config mgmt REST API.
|
||||
queryVal := url.Values{}
|
||||
queryVal.Set("info", "")
|
||||
|
||||
req, err := buildAdminRequest(queryVal, "", http.MethodGet, 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct get-config object request - %v", err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
adminTestBed.mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Expected to succeed but failed with %d", rec.Code)
|
||||
}
|
||||
|
||||
results := []ServerInfo{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&results)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decode set config result json %v", err)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Error("Expected at least one server info result")
|
||||
}
|
||||
|
||||
for _, serverInfo := range results {
|
||||
if len(serverInfo.Addr) == 0 {
|
||||
t.Error("Expected server address to be non empty")
|
||||
}
|
||||
if serverInfo.Error != "" {
|
||||
t.Errorf("Unexpected error = %v\n", serverInfo.Error)
|
||||
}
|
||||
if serverInfo.Data.StorageInfo.Free == 0 {
|
||||
t.Error("Expected StorageInfo.Free to be non empty")
|
||||
}
|
||||
if serverInfo.Data.Properties.Region != globalMinioDefaultRegion {
|
||||
t.Errorf("Expected %s, got %s", globalMinioDefaultRegion, serverInfo.Data.Properties.Region)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestToAdminAPIErr - test for toAdminAPIErr helper function.
|
||||
func TestToAdminAPIErr(t *testing.T) {
|
||||
testCases := []struct {
|
||||
err error
|
||||
expectedAPIErr APIErrorCode
|
||||
}{
|
||||
// 1. Server not in quorum.
|
||||
{
|
||||
err: errXLWriteQuorum,
|
||||
expectedAPIErr: ErrAdminConfigNoQuorum,
|
||||
},
|
||||
// 2. No error.
|
||||
{
|
||||
err: nil,
|
||||
expectedAPIErr: ErrNone,
|
||||
},
|
||||
// 3. Non-admin API specific error.
|
||||
{
|
||||
err: errDiskNotFound,
|
||||
expectedAPIErr: toAPIErrorCode(errDiskNotFound),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
actualErr := toAdminAPIErrCode(test.err)
|
||||
if actualErr != test.expectedAPIErr {
|
||||
t.Errorf("Test %d: Expected %v but received %v",
|
||||
i+1, test.expectedAPIErr, actualErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSetConfigResponse(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
testCases := []struct {
|
||||
status bool
|
||||
errs []error
|
||||
}{
|
||||
// 1. all nodes returned success.
|
||||
{
|
||||
status: true,
|
||||
errs: []error{nil, nil, nil, nil},
|
||||
},
|
||||
// 2. some nodes returned errors.
|
||||
{
|
||||
status: false,
|
||||
errs: []error{errDiskNotFound, nil, errDiskAccessDenied, errFaultyDisk},
|
||||
},
|
||||
}
|
||||
|
||||
testPeers := []adminPeer{
|
||||
{
|
||||
addr: "localhost:9001",
|
||||
},
|
||||
{
|
||||
addr: "localhost:9002",
|
||||
},
|
||||
{
|
||||
addr: "localhost:9003",
|
||||
},
|
||||
{
|
||||
addr: "localhost:9004",
|
||||
},
|
||||
}
|
||||
|
||||
testURL, err := url.Parse("http://dummy.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse a place-holder url")
|
||||
}
|
||||
|
||||
var actualResult setConfigResult
|
||||
for i, test := range testCases {
|
||||
rec := httptest.NewRecorder()
|
||||
writeSetConfigResponse(rec, testPeers, test.errs, test.status, testURL)
|
||||
resp := rec.Result()
|
||||
jsonBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to read response %v", i+1, err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(jsonBytes, &actualResult)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to unmarshal json %v", i+1, err)
|
||||
}
|
||||
if actualResult.Status != test.status {
|
||||
t.Errorf("Test %d: Expected status %v but received %v", i+1, test.status, actualResult.Status)
|
||||
}
|
||||
for p, res := range actualResult.NodeResults {
|
||||
if res.Name != testPeers[p].addr {
|
||||
t.Errorf("Test %d: Expected node name %s but received %s", i+1, testPeers[p].addr, res.Name)
|
||||
}
|
||||
expectedErrMsg := fmt.Sprintf("%v", test.errs[p])
|
||||
if res.ErrMsg != expectedErrMsg {
|
||||
t.Errorf("Test %d: Expected error %s but received %s", i+1, expectedErrMsg, res.ErrMsg)
|
||||
}
|
||||
expectedErrSet := test.errs[p] != nil
|
||||
if res.ErrSet != expectedErrSet {
|
||||
t.Errorf("Test %d: Expected ErrSet %v but received %v", i+1, expectedErrSet, res.ErrSet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mkListUploadsHealQuery - helper function to construct query values for
|
||||
// listUploadsHeal.
|
||||
func mkListUploadsHealQuery(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploadsStr string) url.Values {
|
||||
|
||||
queryVal := make(url.Values)
|
||||
queryVal.Set("heal", "")
|
||||
queryVal.Set(string(mgmtBucket), bucket)
|
||||
queryVal.Set(string(mgmtPrefix), prefix)
|
||||
queryVal.Set(string(mgmtKeyMarker), keyMarker)
|
||||
queryVal.Set(string(mgmtUploadIDMarker), uploadIDMarker)
|
||||
queryVal.Set(string(mgmtDelimiter), delimiter)
|
||||
queryVal.Set(string(mgmtMaxUploads), maxUploadsStr)
|
||||
return queryVal
|
||||
}
|
||||
|
||||
func TestListHealUploadsHandler(t *testing.T) {
|
||||
adminTestBed, err := prepareAdminXLTestBed()
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
||||
}
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
err = adminTestBed.objLayer.MakeBucketWithLocation("mybucket", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make bucket - %v", err)
|
||||
}
|
||||
|
||||
// Delete bucket after running all test cases.
|
||||
defer adminTestBed.objLayer.DeleteBucket("mybucket")
|
||||
|
||||
testCases := []struct {
|
||||
bucket string
|
||||
prefix string
|
||||
keyMarker string
|
||||
delimiter string
|
||||
maxKeys string
|
||||
statusCode int
|
||||
expectedResp ListMultipartUploadsResponse
|
||||
}{
|
||||
// 1. Valid params.
|
||||
{
|
||||
bucket: "mybucket",
|
||||
prefix: "prefix",
|
||||
keyMarker: "prefix11",
|
||||
delimiter: "/",
|
||||
maxKeys: "10",
|
||||
statusCode: http.StatusOK,
|
||||
expectedResp: ListMultipartUploadsResponse{
|
||||
XMLName: xml.Name{Space: "http://s3.amazonaws.com/doc/2006-03-01/", Local: "ListMultipartUploadsResult"},
|
||||
Bucket: "mybucket",
|
||||
KeyMarker: "prefix11",
|
||||
Delimiter: "/",
|
||||
Prefix: "prefix",
|
||||
MaxUploads: 10,
|
||||
},
|
||||
},
|
||||
// 2. Valid params with empty prefix.
|
||||
{
|
||||
bucket: "mybucket",
|
||||
prefix: "",
|
||||
keyMarker: "",
|
||||
delimiter: "/",
|
||||
maxKeys: "10",
|
||||
statusCode: http.StatusOK,
|
||||
expectedResp: ListMultipartUploadsResponse{
|
||||
XMLName: xml.Name{Space: "http://s3.amazonaws.com/doc/2006-03-01/", Local: "ListMultipartUploadsResult"},
|
||||
Bucket: "mybucket",
|
||||
KeyMarker: "",
|
||||
Delimiter: "/",
|
||||
Prefix: "",
|
||||
MaxUploads: 10,
|
||||
},
|
||||
},
|
||||
// 3. Invalid params with invalid bucket.
|
||||
{
|
||||
bucket: `invalid\\Bucket`,
|
||||
prefix: "prefix",
|
||||
keyMarker: "prefix11",
|
||||
delimiter: "/",
|
||||
maxKeys: "10",
|
||||
statusCode: getAPIError(ErrInvalidBucketName).HTTPStatusCode,
|
||||
},
|
||||
// 4. Invalid params with invalid prefix.
|
||||
{
|
||||
bucket: "mybucket",
|
||||
prefix: `invalid\\Prefix`,
|
||||
keyMarker: "prefix11",
|
||||
delimiter: "/",
|
||||
maxKeys: "10",
|
||||
statusCode: getAPIError(ErrInvalidObjectName).HTTPStatusCode,
|
||||
},
|
||||
// 5. Invalid params with invalid maxKeys.
|
||||
{
|
||||
bucket: "mybucket",
|
||||
prefix: "prefix",
|
||||
keyMarker: "prefix11",
|
||||
delimiter: "/",
|
||||
maxKeys: "-1",
|
||||
statusCode: getAPIError(ErrInvalidMaxUploads).HTTPStatusCode,
|
||||
},
|
||||
// 6. Invalid params with unsupported prefix marker combination.
|
||||
{
|
||||
bucket: "mybucket",
|
||||
prefix: "prefix",
|
||||
keyMarker: "notmatchingmarker",
|
||||
delimiter: "/",
|
||||
maxKeys: "10",
|
||||
statusCode: getAPIError(ErrNotImplemented).HTTPStatusCode,
|
||||
},
|
||||
// 7. Invalid params with unsupported delimiter.
|
||||
{
|
||||
bucket: "mybucket",
|
||||
prefix: "prefix",
|
||||
keyMarker: "notmatchingmarker",
|
||||
delimiter: "unsupported",
|
||||
maxKeys: "10",
|
||||
statusCode: getAPIError(ErrNotImplemented).HTTPStatusCode,
|
||||
},
|
||||
// 8. Invalid params with invalid max Keys
|
||||
{
|
||||
bucket: "mybucket",
|
||||
prefix: "prefix",
|
||||
keyMarker: "prefix11",
|
||||
delimiter: "/",
|
||||
maxKeys: "999999999999999999999999999",
|
||||
statusCode: getAPIError(ErrInvalidMaxUploads).HTTPStatusCode,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
queryVal := mkListUploadsHealQuery(test.bucket, test.prefix, test.keyMarker, "", test.delimiter, test.maxKeys)
|
||||
|
||||
req, err := buildAdminRequest(queryVal, "list-uploads", http.MethodGet, 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d - Failed to construct list uploads needing heal request - %v", i+1, err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
adminTestBed.mux.ServeHTTP(rec, req)
|
||||
|
||||
if test.statusCode != rec.Code {
|
||||
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.statusCode, rec.Code)
|
||||
}
|
||||
|
||||
// Compare result with the expected one only when we receive 200 OK
|
||||
if rec.Code == http.StatusOK {
|
||||
resp := rec.Result()
|
||||
xmlBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: Failed to read response %v", i+1, err)
|
||||
}
|
||||
|
||||
var actualResult ListMultipartUploadsResponse
|
||||
err = xml.Unmarshal(xmlBytes, &actualResult)
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: Failed to unmarshal xml %v", i+1, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(test.expectedResp, actualResult) {
|
||||
t.Fatalf("Test %d: Unexpected response `%+v`, expected: `%+v`", i+1, test.expectedResp, actualResult)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Test for newHealResult helper function.
|
||||
func TestNewHealResult(t *testing.T) {
|
||||
testCases := []struct {
|
||||
healedDisks int
|
||||
offlineDisks int
|
||||
state healState
|
||||
}{
|
||||
// 1. No disks healed, no disks offline.
|
||||
{0, 0, healNone},
|
||||
// 2. No disks healed, non-zero disks offline.
|
||||
{0, 1, healNone},
|
||||
// 3. Non-zero disks healed, no disks offline.
|
||||
{1, 0, healOK},
|
||||
// 4. Non-zero disks healed, non-zero disks offline.
|
||||
{1, 1, healPartial},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
actual := newHealResult(test.healedDisks, test.offlineDisks)
|
||||
if actual.State != test.state {
|
||||
t.Errorf("Test %d: Expected %v but received %v", i+1,
|
||||
test.state, actual.State)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ func registerAdminRouter(mux *router.Router) {
|
||||
|
||||
// List Objects needing heal.
|
||||
adminRouter.Methods("GET").Queries("heal", "").Headers(minioAdminOpHeader, "list-objects").HandlerFunc(adminAPI.ListObjectsHealHandler)
|
||||
// List Uploads needing heal.
|
||||
adminRouter.Methods("GET").Queries("heal", "").Headers(minioAdminOpHeader, "list-uploads").HandlerFunc(adminAPI.ListUploadsHealHandler)
|
||||
// List Buckets needing heal.
|
||||
adminRouter.Methods("GET").Queries("heal", "").Headers(minioAdminOpHeader, "list-buckets").HandlerFunc(adminAPI.ListBucketsHealHandler)
|
||||
|
||||
@@ -62,4 +64,13 @@ func registerAdminRouter(mux *router.Router) {
|
||||
adminRouter.Methods("POST").Queries("heal", "").Headers(minioAdminOpHeader, "object").HandlerFunc(adminAPI.HealObjectHandler)
|
||||
// Heal Format.
|
||||
adminRouter.Methods("POST").Queries("heal", "").Headers(minioAdminOpHeader, "format").HandlerFunc(adminAPI.HealFormatHandler)
|
||||
// Heal Uploads.
|
||||
adminRouter.Methods("POST").Queries("heal", "").Headers(minioAdminOpHeader, "upload").HandlerFunc(adminAPI.HealUploadHandler)
|
||||
|
||||
/// Config operations
|
||||
|
||||
// Get config
|
||||
adminRouter.Methods("GET").Queries("config", "").Headers(minioAdminOpHeader, "get").HandlerFunc(adminAPI.GetConfigHandler)
|
||||
// Set Config
|
||||
adminRouter.Methods("PUT").Queries("config", "").Headers(minioAdminOpHeader, "set").HandlerFunc(adminAPI.SetConfigHandler)
|
||||
}
|
||||
|
||||
+340
-53
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2014-2016 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2014, 2015, 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.
|
||||
@@ -17,11 +17,30 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
)
|
||||
|
||||
const (
|
||||
// Admin service names
|
||||
serviceRestartRPC = "Admin.Restart"
|
||||
listLocksRPC = "Admin.ListLocks"
|
||||
reInitDisksRPC = "Admin.ReInitDisks"
|
||||
serverInfoDataRPC = "Admin.ServerInfoData"
|
||||
getConfigRPC = "Admin.GetConfig"
|
||||
writeTmpConfigRPC = "Admin.WriteTmpConfig"
|
||||
commitConfigRPC = "Admin.CommitConfig"
|
||||
)
|
||||
|
||||
// localAdminClient - represents admin operation to be executed locally.
|
||||
@@ -40,7 +59,10 @@ type adminCmdRunner interface {
|
||||
Restart() error
|
||||
ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error)
|
||||
ReInitDisks() error
|
||||
Uptime() (time.Duration, error)
|
||||
ServerInfoData() (ServerInfoData, error)
|
||||
GetConfig() ([]byte, error)
|
||||
WriteTmpConfig(tmpFileName string, configBytes []byte) error
|
||||
CommitConfig(tmpFileName string) error
|
||||
}
|
||||
|
||||
// Restart - Sends a message over channel to the go-routine
|
||||
@@ -59,7 +81,7 @@ func (lc localAdminClient) ListLocks(bucket, prefix string, duration time.Durati
|
||||
func (rc remoteAdminClient) Restart() error {
|
||||
args := AuthRPCArgs{}
|
||||
reply := AuthRPCReply{}
|
||||
return rc.Call("Admin.Restart", &args, &reply)
|
||||
return rc.Call(serviceRestartRPC, &args, &reply)
|
||||
}
|
||||
|
||||
// ListLocks - Sends list locks command to remote server via RPC.
|
||||
@@ -70,7 +92,7 @@ func (rc remoteAdminClient) ListLocks(bucket, prefix string, duration time.Durat
|
||||
duration: duration,
|
||||
}
|
||||
var reply ListLocksReply
|
||||
if err := rc.Call("Admin.ListLocks", &listArgs, &reply); err != nil {
|
||||
if err := rc.Call(listLocksRPC, &listArgs, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reply.volLocks, nil
|
||||
@@ -87,29 +109,120 @@ func (lc localAdminClient) ReInitDisks() error {
|
||||
func (rc remoteAdminClient) ReInitDisks() error {
|
||||
args := AuthRPCArgs{}
|
||||
reply := AuthRPCReply{}
|
||||
return rc.Call("Admin.ReInitDisks", &args, &reply)
|
||||
return rc.Call(reInitDisksRPC, &args, &reply)
|
||||
}
|
||||
|
||||
// Uptime - Returns the uptime of this server. Timestamp is taken
|
||||
// after object layer is initialized.
|
||||
func (lc localAdminClient) Uptime() (time.Duration, error) {
|
||||
// ServerInfoData - Returns the server info of this server.
|
||||
func (lc localAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
|
||||
if globalBootTime.IsZero() {
|
||||
return time.Duration(0), errServerNotInitialized
|
||||
return sid, errServerNotInitialized
|
||||
}
|
||||
|
||||
return time.Now().UTC().Sub(globalBootTime), nil
|
||||
// Build storage info
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
return sid, errServerNotInitialized
|
||||
}
|
||||
storage := objLayer.StorageInfo()
|
||||
|
||||
var arns []string
|
||||
for queueArn := range globalEventNotifier.GetAllExternalTargets() {
|
||||
arns = append(arns, queueArn)
|
||||
}
|
||||
|
||||
return ServerInfoData{
|
||||
StorageInfo: storage,
|
||||
ConnStats: globalConnStats.toServerConnStats(),
|
||||
HTTPStats: globalHTTPStats.toServerHTTPStats(),
|
||||
Properties: ServerProperties{
|
||||
Uptime: UTCNow().Sub(globalBootTime),
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
SQSARN: arns,
|
||||
Region: serverConfig.GetRegion(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Uptime - returns the uptime of the server to which the RPC call is made.
|
||||
func (rc remoteAdminClient) Uptime() (time.Duration, error) {
|
||||
// ServerInfo - returns the server info of the server to which the RPC call is made.
|
||||
func (rc remoteAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
|
||||
args := AuthRPCArgs{}
|
||||
reply := UptimeReply{}
|
||||
err := rc.Call("Admin.Uptime", &args, &reply)
|
||||
reply := ServerInfoDataReply{}
|
||||
err := rc.Call(serverInfoDataRPC, &args, &reply)
|
||||
if err != nil {
|
||||
return time.Duration(0), err
|
||||
return sid, err
|
||||
}
|
||||
|
||||
return reply.Uptime, nil
|
||||
return reply.ServerInfoData, nil
|
||||
}
|
||||
|
||||
// GetConfig - returns config.json of the local server.
|
||||
func (lc localAdminClient) GetConfig() ([]byte, error) {
|
||||
if serverConfig == nil {
|
||||
return nil, errors.New("config not present")
|
||||
}
|
||||
|
||||
return json.Marshal(serverConfig)
|
||||
}
|
||||
|
||||
// GetConfig - returns config.json of the remote server.
|
||||
func (rc remoteAdminClient) GetConfig() ([]byte, error) {
|
||||
args := AuthRPCArgs{}
|
||||
reply := ConfigReply{}
|
||||
if err := rc.Call(getConfigRPC, &args, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reply.Config, nil
|
||||
}
|
||||
|
||||
// WriteTmpConfig - writes config file content to a temporary file on
|
||||
// the local server.
|
||||
func (lc localAdminClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
|
||||
return writeTmpConfigCommon(tmpFileName, configBytes)
|
||||
}
|
||||
|
||||
// WriteTmpConfig - writes config file content to a temporary file on
|
||||
// a remote node.
|
||||
func (rc remoteAdminClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
|
||||
wArgs := WriteConfigArgs{
|
||||
TmpFileName: tmpFileName,
|
||||
Buf: configBytes,
|
||||
}
|
||||
|
||||
err := rc.Call(writeTmpConfigRPC, &wArgs, &WriteConfigReply{})
|
||||
if err != nil {
|
||||
errorIf(err, "Failed to write temporary config file.")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommitConfig - Move the new config in tmpFileName onto config.json
|
||||
// on a local node.
|
||||
func (lc localAdminClient) CommitConfig(tmpFileName string) error {
|
||||
configFile := getConfigFile()
|
||||
tmpConfigFile := filepath.Join(getConfigDir(), tmpFileName)
|
||||
|
||||
err := os.Rename(tmpConfigFile, configFile)
|
||||
errorIf(err, fmt.Sprintf("Failed to rename %s to %s", tmpConfigFile, configFile))
|
||||
return err
|
||||
}
|
||||
|
||||
// CommitConfig - Move the new config in tmpFileName onto config.json
|
||||
// on a remote node.
|
||||
func (rc remoteAdminClient) CommitConfig(tmpFileName string) error {
|
||||
cArgs := CommitConfigArgs{
|
||||
FileName: tmpFileName,
|
||||
}
|
||||
cReply := CommitConfigReply{}
|
||||
err := rc.Call(commitConfigRPC, &cArgs, &cReply)
|
||||
if err != nil {
|
||||
errorIf(err, "Failed to rename config file.")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// adminPeer - represents an entity that implements Restart methods.
|
||||
@@ -122,52 +235,43 @@ type adminPeer struct {
|
||||
type adminPeers []adminPeer
|
||||
|
||||
// makeAdminPeers - helper function to construct a collection of adminPeer.
|
||||
func makeAdminPeers(eps []*url.URL) adminPeers {
|
||||
var servicePeers []adminPeer
|
||||
|
||||
// map to store peers that are already added to ret
|
||||
seenAddr := make(map[string]bool)
|
||||
|
||||
// add local (self) as peer in the array
|
||||
servicePeers = append(servicePeers, adminPeer{
|
||||
globalMinioAddr,
|
||||
func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
|
||||
thisPeer := globalMinioAddr
|
||||
if globalMinioHost == "" {
|
||||
thisPeer = net.JoinHostPort("localhost", globalMinioPort)
|
||||
}
|
||||
adminPeerList = append(adminPeerList, adminPeer{
|
||||
thisPeer,
|
||||
localAdminClient{},
|
||||
})
|
||||
seenAddr[globalMinioAddr] = true
|
||||
|
||||
serverCred := serverConfig.GetCredential()
|
||||
// iterate over endpoints to find new remote peers and add
|
||||
// them to ret.
|
||||
for _, ep := range eps {
|
||||
if ep.Host == "" {
|
||||
hostSet := set.CreateStringSet(globalMinioAddr)
|
||||
cred := serverConfig.GetCredential()
|
||||
serviceEndpoint := path.Join(minioReservedBucketPath, adminPath)
|
||||
for _, host := range GetRemotePeers(endpoints) {
|
||||
if hostSet.Contains(host) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the remote host has been added already
|
||||
if !seenAddr[ep.Host] {
|
||||
cfg := authConfig{
|
||||
accessKey: serverCred.AccessKey,
|
||||
secretKey: serverCred.SecretKey,
|
||||
serverAddr: ep.Host,
|
||||
hostSet.Add(host)
|
||||
adminPeerList = append(adminPeerList, adminPeer{
|
||||
addr: host,
|
||||
cmdRunner: &remoteAdminClient{newAuthRPCClient(authConfig{
|
||||
accessKey: cred.AccessKey,
|
||||
secretKey: cred.SecretKey,
|
||||
serverAddr: host,
|
||||
serviceEndpoint: serviceEndpoint,
|
||||
secureConn: globalIsSSL,
|
||||
serviceEndpoint: path.Join(reservedBucket, adminPath),
|
||||
serviceName: "Admin",
|
||||
}
|
||||
|
||||
servicePeers = append(servicePeers, adminPeer{
|
||||
addr: ep.Host,
|
||||
cmdRunner: &remoteAdminClient{newAuthRPCClient(cfg)},
|
||||
})
|
||||
seenAddr[ep.Host] = true
|
||||
}
|
||||
})},
|
||||
})
|
||||
}
|
||||
|
||||
return servicePeers
|
||||
return adminPeerList
|
||||
}
|
||||
|
||||
// Initialize global adminPeer collection.
|
||||
func initGlobalAdminPeers(eps []*url.URL) {
|
||||
globalAdminPeers = makeAdminPeers(eps)
|
||||
func initGlobalAdminPeers(endpoints EndpointList) {
|
||||
globalAdminPeers = makeAdminPeers(endpoints)
|
||||
}
|
||||
|
||||
// invokeServiceCmd - Invoke Restart command.
|
||||
@@ -291,7 +395,7 @@ func getPeerUptimes(peers adminPeers) (time.Duration, error) {
|
||||
// the setup is the uptime of the single minio server
|
||||
// instance.
|
||||
if !globalIsDistXL {
|
||||
return time.Now().UTC().Sub(globalBootTime), nil
|
||||
return UTCNow().Sub(globalBootTime), nil
|
||||
}
|
||||
|
||||
uptimes := make(uptimeSlice, len(peers))
|
||||
@@ -302,7 +406,8 @@ func getPeerUptimes(peers adminPeers) (time.Duration, error) {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
uptimes[idx].uptime, uptimes[idx].err = peer.cmdRunner.Uptime()
|
||||
serverInfoData, rpcErr := peer.cmdRunner.ServerInfoData()
|
||||
uptimes[idx].uptime, uptimes[idx].err = serverInfoData.Properties.Uptime, rpcErr
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -336,3 +441,185 @@ func getPeerUptimes(peers adminPeers) (time.Duration, error) {
|
||||
|
||||
return latestUptime, nil
|
||||
}
|
||||
|
||||
// getPeerConfig - Fetches config.json from all nodes in the setup and
|
||||
// returns the one that occurs in a majority of them.
|
||||
func getPeerConfig(peers adminPeers) ([]byte, error) {
|
||||
if !globalIsDistXL {
|
||||
return peers[0].cmdRunner.GetConfig()
|
||||
}
|
||||
|
||||
errs := make([]error, len(peers))
|
||||
configs := make([][]byte, len(peers))
|
||||
|
||||
// Get config from all servers.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
configs[idx], errs[idx] = peer.cmdRunner.GetConfig()
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Find the maximally occurring config among peers in a
|
||||
// distributed setup.
|
||||
|
||||
serverConfigs := make([]serverConfigV13, len(peers))
|
||||
for i, configBytes := range configs {
|
||||
if errs[i] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Unmarshal the received config files.
|
||||
err := json.Unmarshal(configBytes, &serverConfigs[i])
|
||||
if err != nil {
|
||||
errorIf(err, "Failed to unmarshal serverConfig from ", peers[i].addr)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
configJSON, err := getValidServerConfig(serverConfigs, errs)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to find a valid server config")
|
||||
return nil, traceError(err)
|
||||
}
|
||||
|
||||
// Return the config.json that was present quorum or more
|
||||
// number of disks.
|
||||
return json.Marshal(configJSON)
|
||||
}
|
||||
|
||||
// getValidServerConfig - finds the server config that is present in
|
||||
// quorum or more number of servers.
|
||||
func getValidServerConfig(serverConfigs []serverConfigV13, errs []error) (scv serverConfigV13, e error) {
|
||||
// majority-based quorum
|
||||
quorum := len(serverConfigs)/2 + 1
|
||||
|
||||
// Count the number of disks a config.json was found in.
|
||||
configCounter := make([]int, len(serverConfigs))
|
||||
|
||||
// We group equal serverConfigs by the lowest index of the
|
||||
// same value; e.g, let us take the following serverConfigs
|
||||
// in a 4-node setup,
|
||||
// serverConfigs == [c1, c2, c1, c1]
|
||||
// configCounter == [3, 1, 0, 0]
|
||||
// c1, c2 are the only distinct values that appear. c1 is
|
||||
// identified by 0, the lowest index it appears in and c2 is
|
||||
// identified by 1. So, we need to find the number of times
|
||||
// each of these distinct values occur.
|
||||
|
||||
// Invariants:
|
||||
|
||||
// 1. At the beginning of the i-th iteration, the number of
|
||||
// unique configurations seen so far is equal to the number of
|
||||
// non-zero counter values in config[:i].
|
||||
|
||||
// 2. At the beginning of the i-th iteration, the sum of
|
||||
// elements of configCounter[:i] is equal to the number of
|
||||
// non-error configurations seen so far.
|
||||
|
||||
// For each of the serverConfig ...
|
||||
for i := range serverConfigs {
|
||||
// Skip nodes where getConfig failed.
|
||||
if errs[i] != nil {
|
||||
continue
|
||||
}
|
||||
// Check if it is equal to any of the configurations
|
||||
// seen so far. If j == i is reached then we have an
|
||||
// unseen configuration.
|
||||
for j := 0; j <= i; j++ {
|
||||
if j < i && configCounter[j] == 0 {
|
||||
// serverConfigs[j] is known to be
|
||||
// equal to a value that was already
|
||||
// seen. See example above for
|
||||
// clarity.
|
||||
continue
|
||||
} else if j < i && reflect.DeepEqual(serverConfigs[i], serverConfigs[j]) {
|
||||
// serverConfigs[i] is equal to
|
||||
// serverConfigs[j], update
|
||||
// serverConfigs[j]'s counter since it
|
||||
// is the lower index.
|
||||
configCounter[j]++
|
||||
break
|
||||
} else if j == i {
|
||||
// serverConfigs[i] is equal to no
|
||||
// other value seen before. It is
|
||||
// unique so far.
|
||||
configCounter[i] = 1
|
||||
break
|
||||
} // else invariants specified above are violated.
|
||||
}
|
||||
}
|
||||
|
||||
// We find the maximally occurring server config and check if
|
||||
// there is quorum.
|
||||
var configJSON serverConfigV13
|
||||
maxOccurrence := 0
|
||||
for i, count := range configCounter {
|
||||
if maxOccurrence < count {
|
||||
maxOccurrence = count
|
||||
configJSON = serverConfigs[i]
|
||||
}
|
||||
}
|
||||
|
||||
// If quorum nodes don't agree.
|
||||
if maxOccurrence < quorum {
|
||||
return scv, errXLWriteQuorum
|
||||
}
|
||||
|
||||
return configJSON, nil
|
||||
}
|
||||
|
||||
// Write config contents into a temporary file on all nodes.
|
||||
func writeTmpConfigPeers(peers adminPeers, tmpFileName string, configBytes []byte) []error {
|
||||
// For a single-node minio server setup.
|
||||
if !globalIsDistXL {
|
||||
err := peers[0].cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
errs := make([]error, len(peers))
|
||||
|
||||
// Write config into temporary file on all nodes.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
errs[idx] = peer.cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Return bytes written and errors (if any) during writing
|
||||
// temporary config file.
|
||||
return errs
|
||||
}
|
||||
|
||||
// Move config contents from the given temporary file onto config.json
|
||||
// on all nodes.
|
||||
func commitConfigPeers(peers adminPeers, tmpFileName string) []error {
|
||||
// For a single-node minio server setup.
|
||||
if !globalIsDistXL {
|
||||
return []error{peers[0].cmdRunner.CommitConfig(tmpFileName)}
|
||||
}
|
||||
|
||||
errs := make([]error, len(peers))
|
||||
|
||||
// Rename temporary config file into configDir/config.json on
|
||||
// all nodes.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
errs[idx] = peer.cmdRunner.CommitConfig(tmpFileName)
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Return errors (if any) received during rename.
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
config1 = []byte(`{
|
||||
"version": "13",
|
||||
"credential": {
|
||||
"accessKey": "minio",
|
||||
"secretKey": "minio123"
|
||||
},
|
||||
"region": "us-east-1",
|
||||
"logger": {
|
||||
"console": {
|
||||
"enable": true,
|
||||
"level": "debug"
|
||||
},
|
||||
"file": {
|
||||
"enable": false,
|
||||
"fileName": "",
|
||||
"level": ""
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"amqp": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"exchange": "",
|
||||
"routingKey": "",
|
||||
"exchangeType": "",
|
||||
"mandatory": false,
|
||||
"immediate": false,
|
||||
"durable": false,
|
||||
"internal": false,
|
||||
"noWait": false,
|
||||
"autoDeleted": false
|
||||
}
|
||||
},
|
||||
"nats": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"subject": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"token": "",
|
||||
"secure": false,
|
||||
"pingInterval": 0,
|
||||
"streaming": {
|
||||
"enable": false,
|
||||
"clusterID": "",
|
||||
"clientID": "",
|
||||
"async": false,
|
||||
"maxPubAcksInflight": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"elasticsearch": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"index": ""
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"password": "",
|
||||
"key": ""
|
||||
}
|
||||
},
|
||||
"postgresql": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"connectionString": "",
|
||||
"table": "",
|
||||
"host": "",
|
||||
"port": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"database": ""
|
||||
}
|
||||
},
|
||||
"kafka": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"brokers": null,
|
||||
"topic": ""
|
||||
}
|
||||
},
|
||||
"webhook": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"endpoint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
// diff from config1 - amqp.Enable is True
|
||||
config2 = []byte(`{
|
||||
"version": "13",
|
||||
"credential": {
|
||||
"accessKey": "minio",
|
||||
"secretKey": "minio123"
|
||||
},
|
||||
"region": "us-east-1",
|
||||
"logger": {
|
||||
"console": {
|
||||
"enable": true,
|
||||
"level": "debug"
|
||||
},
|
||||
"file": {
|
||||
"enable": false,
|
||||
"fileName": "",
|
||||
"level": ""
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"amqp": {
|
||||
"1": {
|
||||
"enable": true,
|
||||
"url": "",
|
||||
"exchange": "",
|
||||
"routingKey": "",
|
||||
"exchangeType": "",
|
||||
"mandatory": false,
|
||||
"immediate": false,
|
||||
"durable": false,
|
||||
"internal": false,
|
||||
"noWait": false,
|
||||
"autoDeleted": false
|
||||
}
|
||||
},
|
||||
"nats": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"subject": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"token": "",
|
||||
"secure": false,
|
||||
"pingInterval": 0,
|
||||
"streaming": {
|
||||
"enable": false,
|
||||
"clusterID": "",
|
||||
"clientID": "",
|
||||
"async": false,
|
||||
"maxPubAcksInflight": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"elasticsearch": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"index": ""
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"password": "",
|
||||
"key": ""
|
||||
}
|
||||
},
|
||||
"postgresql": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"connectionString": "",
|
||||
"table": "",
|
||||
"host": "",
|
||||
"port": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"database": ""
|
||||
}
|
||||
},
|
||||
"kafka": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"brokers": null,
|
||||
"topic": ""
|
||||
}
|
||||
},
|
||||
"webhook": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"endpoint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
)
|
||||
|
||||
// TestGetValidServerConfig - test for getValidServerConfig.
|
||||
func TestGetValidServerConfig(t *testing.T) {
|
||||
var c1, c2 serverConfigV13
|
||||
err := json.Unmarshal(config1, &c1)
|
||||
if err != nil {
|
||||
t.Fatalf("json unmarshal of %s failed: %v", string(config1), err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(config2, &c2)
|
||||
if err != nil {
|
||||
t.Fatalf("json unmarshal of %s failed: %v", string(config2), err)
|
||||
}
|
||||
|
||||
// Valid config.
|
||||
noErrs := []error{nil, nil, nil, nil}
|
||||
serverConfigs := []serverConfigV13{c1, c2, c1, c1}
|
||||
validConfig, err := getValidServerConfig(serverConfigs, noErrs)
|
||||
if err != nil {
|
||||
t.Errorf("Expected a valid config but received %v instead", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(validConfig, c1) {
|
||||
t.Errorf("Expected valid config to be %v but received %v", config1, validConfig)
|
||||
}
|
||||
|
||||
// Invalid config - no quorum.
|
||||
serverConfigs = []serverConfigV13{c1, c2, c2, c1}
|
||||
_, err = getValidServerConfig(serverConfigs, noErrs)
|
||||
if err != errXLWriteQuorum {
|
||||
t.Errorf("Expected to fail due to lack of quorum but received %v", err)
|
||||
}
|
||||
|
||||
// All errors
|
||||
allErrs := []error{errDiskNotFound, errDiskNotFound, errDiskNotFound, errDiskNotFound}
|
||||
serverConfigs = []serverConfigV13{{}, {}, {}, {}}
|
||||
_, err = getValidServerConfig(serverConfigs, allErrs)
|
||||
if err != errXLWriteQuorum {
|
||||
t.Errorf("Expected to fail due to lack of quorum but received %v", err)
|
||||
}
|
||||
}
|
||||
+114
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -17,8 +17,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/rpc"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
router "github.com/gorilla/mux"
|
||||
@@ -48,10 +52,16 @@ type ListLocksReply struct {
|
||||
volLocks []VolumeLockInfo
|
||||
}
|
||||
|
||||
// UptimeReply - wraps the uptime response over RPC.
|
||||
type UptimeReply struct {
|
||||
// ServerInfoDataReply - wraps the server info response over RPC.
|
||||
type ServerInfoDataReply struct {
|
||||
AuthRPCReply
|
||||
Uptime time.Duration
|
||||
ServerInfoData ServerInfoData
|
||||
}
|
||||
|
||||
// ConfigReply - wraps the server config response over RPC.
|
||||
type ConfigReply struct {
|
||||
AuthRPCReply
|
||||
Config []byte // json-marshalled bytes of serverConfigV13
|
||||
}
|
||||
|
||||
// Restart - Restart this instance of minio server.
|
||||
@@ -111,8 +121,8 @@ func (s *adminCmd) ReInitDisks(args *AuthRPCArgs, reply *AuthRPCReply) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Uptime - returns the time when object layer was initialized on this server.
|
||||
func (s *adminCmd) Uptime(args *AuthRPCArgs, reply *UptimeReply) error {
|
||||
// ServerInfo - returns the server info when object layer was initialized on this server.
|
||||
func (s *adminCmd) ServerInfoData(args *AuthRPCArgs, reply *ServerInfoDataReply) error {
|
||||
if err := args.IsAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -121,27 +131,116 @@ func (s *adminCmd) Uptime(args *AuthRPCArgs, reply *UptimeReply) error {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
// N B The uptime is computed assuming that the system time is
|
||||
// monotonic. This is not the case in time pkg in Go, see
|
||||
// https://github.com/golang/go/issues/12914. This is expected
|
||||
// to be fixed by go1.9.
|
||||
*reply = UptimeReply{
|
||||
Uptime: time.Now().UTC().Sub(globalBootTime),
|
||||
// Build storage info
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
storageInfo := objLayer.StorageInfo()
|
||||
|
||||
var arns []string
|
||||
for queueArn := range globalEventNotifier.GetAllExternalTargets() {
|
||||
arns = append(arns, queueArn)
|
||||
}
|
||||
|
||||
reply.ServerInfoData = ServerInfoData{
|
||||
Properties: ServerProperties{
|
||||
Uptime: UTCNow().Sub(globalBootTime),
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
Region: serverConfig.GetRegion(),
|
||||
SQSARN: arns,
|
||||
},
|
||||
StorageInfo: storageInfo,
|
||||
ConnStats: globalConnStats.toServerConnStats(),
|
||||
HTTPStats: globalHTTPStats.toServerHTTPStats(),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig - returns the config.json of this server.
|
||||
func (s *adminCmd) GetConfig(args *AuthRPCArgs, reply *ConfigReply) error {
|
||||
if err := args.IsAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if serverConfig == nil {
|
||||
return errors.New("config not present")
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(serverConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reply.Config = jsonBytes
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteConfigArgs - wraps the bytes to be written and temporary file name.
|
||||
type WriteConfigArgs struct {
|
||||
AuthRPCArgs
|
||||
TmpFileName string
|
||||
Buf []byte
|
||||
}
|
||||
|
||||
// WriteConfigReply - wraps the result of a writing config into a temporary file.
|
||||
// the remote node.
|
||||
type WriteConfigReply struct {
|
||||
AuthRPCReply
|
||||
}
|
||||
|
||||
func writeTmpConfigCommon(tmpFileName string, configBytes []byte) error {
|
||||
tmpConfigFile := filepath.Join(getConfigDir(), tmpFileName)
|
||||
err := ioutil.WriteFile(tmpConfigFile, configBytes, 0666)
|
||||
errorIf(err, fmt.Sprintf("Failed to write to temporary config file %s", tmpConfigFile))
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteTmpConfig - writes the supplied config contents onto the
|
||||
// supplied temporary file.
|
||||
func (s *adminCmd) WriteTmpConfig(wArgs *WriteConfigArgs, wReply *WriteConfigReply) error {
|
||||
if err := wArgs.IsAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeTmpConfigCommon(wArgs.TmpFileName, wArgs.Buf)
|
||||
}
|
||||
|
||||
// CommitConfigArgs - wraps the config file name that needs to be
|
||||
// committed into config.json on this node.
|
||||
type CommitConfigArgs struct {
|
||||
AuthRPCArgs
|
||||
FileName string
|
||||
}
|
||||
|
||||
// CommitConfigReply - represents response to commit of config file on
|
||||
// this node.
|
||||
type CommitConfigReply struct {
|
||||
AuthRPCReply
|
||||
}
|
||||
|
||||
// CommitConfig - Renames the temporary file into config.json on this node.
|
||||
func (s *adminCmd) CommitConfig(cArgs *CommitConfigArgs, cReply *CommitConfigReply) error {
|
||||
configFile := getConfigFile()
|
||||
tmpConfigFile := filepath.Join(getConfigDir(), cArgs.FileName)
|
||||
|
||||
err := os.Rename(tmpConfigFile, configFile)
|
||||
errorIf(err, fmt.Sprintf("Failed to rename %s to %s", tmpConfigFile, configFile))
|
||||
return err
|
||||
}
|
||||
|
||||
// registerAdminRPCRouter - registers RPC methods for service status,
|
||||
// stop and restart commands.
|
||||
func registerAdminRPCRouter(mux *router.Router) error {
|
||||
adminRPCHandler := &adminCmd{}
|
||||
adminRPCServer := rpc.NewServer()
|
||||
adminRPCServer := newRPCServer()
|
||||
err := adminRPCServer.RegisterName("Admin", adminRPCHandler)
|
||||
if err != nil {
|
||||
return traceError(err)
|
||||
}
|
||||
adminRouter := mux.NewRoute().PathPrefix(reservedBucket).Subrouter()
|
||||
adminRouter := mux.NewRoute().PathPrefix(minioReservedBucketPath).Subrouter()
|
||||
adminRouter.Path(adminPath).Handler(adminRPCServer)
|
||||
return nil
|
||||
}
|
||||
|
||||
+145
-28
@@ -17,9 +17,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func testAdminCmd(cmd cmdType, t *testing.T) {
|
||||
@@ -31,18 +31,21 @@ func testAdminCmd(cmd cmdType, t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test config - %v", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
creds := serverConfig.GetCredential()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
adminServer := adminCmd{}
|
||||
creds := serverConfig.GetCredential()
|
||||
args := LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: time.Now().UTC(),
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
reply := LoginRPCReply{}
|
||||
err = adminServer.Login(&args, &reply)
|
||||
err = adminServer.Login(&args, &LoginRPCReply{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login to admin server - %v", err)
|
||||
}
|
||||
@@ -52,7 +55,7 @@ func testAdminCmd(cmd cmdType, t *testing.T) {
|
||||
<-globalServiceSignalCh
|
||||
}()
|
||||
|
||||
ga := AuthRPCArgs{AuthToken: reply.AuthToken, RequestTime: time.Now().UTC()}
|
||||
ga := AuthRPCArgs{AuthToken: token}
|
||||
genReply := AuthRPCReply{}
|
||||
switch cmd {
|
||||
case restartCmd:
|
||||
@@ -76,7 +79,7 @@ func TestReInitDisks(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize server config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// Initializing objectLayer for HealFormatHandler.
|
||||
_, xlDirs, xlErr := initTestXLObjLayer()
|
||||
@@ -86,29 +89,30 @@ func TestReInitDisks(t *testing.T) {
|
||||
defer removeRoots(xlDirs)
|
||||
|
||||
// Set globalEndpoints for a single node XL setup.
|
||||
for _, xlDir := range xlDirs {
|
||||
globalEndpoints = append(globalEndpoints, &url.URL{Path: xlDir})
|
||||
}
|
||||
globalEndpoints = mustGetNewEndpointList(xlDirs...)
|
||||
|
||||
// Setup admin rpc server for an XL backend.
|
||||
globalIsXL = true
|
||||
adminServer := adminCmd{}
|
||||
|
||||
creds := serverConfig.GetCredential()
|
||||
args := LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
RequestTime: time.Now().UTC(),
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reply := LoginRPCReply{}
|
||||
err = adminServer.Login(&args, &reply)
|
||||
|
||||
args := LoginRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
err = adminServer.Login(&args, &LoginRPCReply{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login to admin server - %v", err)
|
||||
}
|
||||
|
||||
authArgs := AuthRPCArgs{
|
||||
AuthToken: reply.AuthToken,
|
||||
RequestTime: time.Now().UTC(),
|
||||
AuthToken: token,
|
||||
}
|
||||
authReply := AuthRPCReply{}
|
||||
|
||||
@@ -117,14 +121,17 @@ func TestReInitDisks(t *testing.T) {
|
||||
t.Errorf("Expected to pass, but failed with %v", err)
|
||||
}
|
||||
|
||||
token, err = authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Negative test case with admin rpc server setup for FS.
|
||||
globalIsXL = false
|
||||
fsAdminServer := adminCmd{}
|
||||
fsArgs := LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: time.Now().UTC(),
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
fsReply := LoginRPCReply{}
|
||||
err = fsAdminServer.Login(&fsArgs, &fsReply)
|
||||
@@ -133,8 +140,7 @@ func TestReInitDisks(t *testing.T) {
|
||||
}
|
||||
|
||||
authArgs = AuthRPCArgs{
|
||||
AuthToken: fsReply.AuthToken,
|
||||
RequestTime: time.Now().UTC(),
|
||||
AuthToken: token,
|
||||
}
|
||||
authReply = AuthRPCReply{}
|
||||
// Attempt ReInitDisks service on a FS backend.
|
||||
@@ -144,3 +150,114 @@ func TestReInitDisks(t *testing.T) {
|
||||
errUnsupportedBackend, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetConfig - Test for GetConfig admin RPC.
|
||||
func TestGetConfig(t *testing.T) {
|
||||
// Reset global variables to start afresh.
|
||||
resetTestGlobals()
|
||||
|
||||
rootPath, err := newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize server config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
adminServer := adminCmd{}
|
||||
creds := serverConfig.GetCredential()
|
||||
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
args := LoginRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
reply := LoginRPCReply{}
|
||||
err = adminServer.Login(&args, &reply)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login to admin server - %v", err)
|
||||
}
|
||||
|
||||
authArgs := AuthRPCArgs{
|
||||
AuthToken: token,
|
||||
}
|
||||
|
||||
configReply := ConfigReply{}
|
||||
|
||||
err = adminServer.GetConfig(&authArgs, &configReply)
|
||||
if err != nil {
|
||||
t.Errorf("Expected GetConfig to pass but failed with %v", err)
|
||||
}
|
||||
|
||||
var config serverConfigV13
|
||||
err = json.Unmarshal(configReply.Config, &config)
|
||||
if err != nil {
|
||||
t.Errorf("Expected json unmarshal to pass but failed with %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteAndCommitConfig - test for WriteTmpConfig and CommitConfig
|
||||
// RPC handler.
|
||||
func TestWriteAndCommitConfig(t *testing.T) {
|
||||
// Reset global variables to start afresh.
|
||||
resetTestGlobals()
|
||||
|
||||
rootPath, err := newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize server config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
adminServer := adminCmd{}
|
||||
creds := serverConfig.GetCredential()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args := LoginRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
reply := LoginRPCReply{}
|
||||
err = adminServer.Login(&args, &reply)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login to admin server - %v", err)
|
||||
}
|
||||
|
||||
// Write temporary config.
|
||||
buf := []byte("hello")
|
||||
tmpFileName := mustGetUUID()
|
||||
wArgs := WriteConfigArgs{
|
||||
AuthRPCArgs: AuthRPCArgs{
|
||||
AuthToken: token,
|
||||
},
|
||||
TmpFileName: tmpFileName,
|
||||
Buf: buf,
|
||||
}
|
||||
|
||||
err = adminServer.WriteTmpConfig(&wArgs, &WriteConfigReply{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write temporary config %v", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected to succeed but failed %v", err)
|
||||
}
|
||||
|
||||
cArgs := CommitConfigArgs{
|
||||
AuthRPCArgs: AuthRPCArgs{
|
||||
AuthToken: token,
|
||||
},
|
||||
FileName: tmpFileName,
|
||||
}
|
||||
cReply := CommitConfigReply{}
|
||||
|
||||
err = adminServer.CommitConfig(&cArgs, &cReply)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to commit config file %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
+196
-10
@@ -19,6 +19,9 @@ package cmd
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
// APIError structure
|
||||
@@ -56,6 +59,8 @@ const (
|
||||
ErrInvalidBucketName
|
||||
ErrInvalidDigest
|
||||
ErrInvalidRange
|
||||
ErrInvalidCopyPartRange
|
||||
ErrInvalidCopyPartRangeSource
|
||||
ErrInvalidMaxKeys
|
||||
ErrInvalidMaxUploads
|
||||
ErrInvalidMaxParts
|
||||
@@ -112,8 +117,23 @@ const (
|
||||
ErrInvalidQueryParams
|
||||
ErrBucketAlreadyOwnedByYou
|
||||
ErrInvalidDuration
|
||||
ErrBucketAlreadyExists
|
||||
ErrMetadataTooLarge
|
||||
ErrUnsupportedMetadata
|
||||
ErrMaximumExpires
|
||||
// Add new error codes here.
|
||||
|
||||
// Server-Side-Encryption (with Customer provided key) related API errors.
|
||||
|
||||
ErrInsecureSSECustomerRequest
|
||||
ErrSSEEncryptedObject
|
||||
ErrInvalidEncryptionParameters
|
||||
ErrInvalidSSECustomerAlgorithm
|
||||
ErrInvalidSSECustomerKey
|
||||
ErrMissingSSECustomerKey
|
||||
ErrMissingSSECustomerKeyMD5
|
||||
ErrSSECustomerKeyMD5Mismatch
|
||||
|
||||
// Bucket notification related errors.
|
||||
ErrEventNotification
|
||||
ErrARNNotification
|
||||
@@ -124,6 +144,7 @@ const (
|
||||
ErrFilterNameSuffix
|
||||
ErrFilterValueInvalid
|
||||
ErrOverlappingConfigs
|
||||
ErrUnsupportedNotification
|
||||
|
||||
// S3 extended errors.
|
||||
ErrContentSHA256Mismatch
|
||||
@@ -137,13 +158,21 @@ const (
|
||||
ErrObjectExistsAsDirectory
|
||||
ErrPolicyNesting
|
||||
ErrInvalidObjectName
|
||||
ErrInvalidResourceName
|
||||
ErrServerNotInitialized
|
||||
ErrOperationTimedOut
|
||||
ErrPartsSizeUnequal
|
||||
ErrInvalidRequest
|
||||
// Add new extended error codes here.
|
||||
// Please open a https://github.com/minio/minio/issues before adding
|
||||
// new error codes here.
|
||||
|
||||
ErrAdminInvalidAccessKey
|
||||
ErrAdminInvalidSecretKey
|
||||
ErrAdminConfigNoQuorum
|
||||
ErrAdminCredentialsMismatch
|
||||
ErrInsecureClientRequest
|
||||
ErrObjectTampered
|
||||
)
|
||||
|
||||
// error code to APIError structure, these fields carry respective
|
||||
@@ -225,7 +254,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
ErrInvalidAccessKeyID: {
|
||||
Code: "InvalidAccessKeyID",
|
||||
Code: "InvalidAccessKeyId",
|
||||
Description: "The access key ID you provided does not exist in our records.",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
@@ -311,7 +340,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
},
|
||||
ErrInvalidPart: {
|
||||
Code: "InvalidPart",
|
||||
Description: "One or more of the specified parts could not be found.",
|
||||
Description: "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidPartOrder: {
|
||||
@@ -349,6 +378,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "The bucket you tried to delete is not empty",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrBucketAlreadyExists: {
|
||||
Code: "BucketAlreadyExists",
|
||||
Description: "The requested bucket name is not available. The bucket namespace is shared by all users of the system. Please select a different name and try again.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrAllAccessDisabled: {
|
||||
Code: "AllAccessDisabled",
|
||||
Description: "All access to this bucket has been disabled.",
|
||||
@@ -539,6 +573,66 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Configurations overlap. Configurations on the same bucket cannot share a common event type.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrUnsupportedNotification: {
|
||||
Code: "UnsupportedNotification",
|
||||
Description: "Minio server does not support Topic or Cloud Function based notifications.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidCopyPartRange: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "The x-amz-copy-source-range value must be of the form bytes=first-last where first and last are the zero-based offsets of the first and last bytes to copy",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidCopyPartRangeSource: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Range specified is not valid for source object",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMetadataTooLarge: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Your metadata headers exceed the maximum allowed metadata size.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInsecureSSECustomerRequest: {
|
||||
Code: "InvalidRequest",
|
||||
Description: errInsecureSSERequest.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSSEEncryptedObject: {
|
||||
Code: "InvalidRequest",
|
||||
Description: errEncryptedObject.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidEncryptionParameters: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "The encryption parameters are not applicable to this object.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidSSECustomerAlgorithm: {
|
||||
Code: "InvalidArgument",
|
||||
Description: errInvalidSSEAlgorithm.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidSSECustomerKey: {
|
||||
Code: "InvalidArgument",
|
||||
Description: errInvalidSSEKey.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingSSECustomerKey: {
|
||||
Code: "InvalidArgument",
|
||||
Description: errMissingSSEKey.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingSSECustomerKeyMD5: {
|
||||
Code: "InvalidArgument",
|
||||
Description: errMissingSSEKeyMD5.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSSECustomerKeyMD5Mismatch: {
|
||||
Code: "InvalidArgument",
|
||||
Description: errSSEKeyMD5Mismatch.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
/// S3 extensions.
|
||||
ErrContentSHA256Mismatch: {
|
||||
@@ -550,7 +644,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
/// Minio extensions.
|
||||
ErrStorageFull: {
|
||||
Code: "XMinioStorageFull",
|
||||
Description: "Storage backend has reached its minimum free disk threshold. Please delete few objects to proceed.",
|
||||
Description: "Storage backend has reached its minimum free disk threshold. Please delete a few objects to proceed.",
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
ErrObjectExistsAsDirectory: {
|
||||
@@ -575,7 +669,12 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
},
|
||||
ErrInvalidObjectName: {
|
||||
Code: "XMinioInvalidObjectName",
|
||||
Description: "Object name contains unsupported characters. Unsupported characters are `^*|\\\"",
|
||||
Description: "Object name contains unsupported characters.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidResourceName: {
|
||||
Code: "XMinioInvalidResourceName",
|
||||
Description: "Resource name contains bad components such as \"..\" or \".\".",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrServerNotInitialized: {
|
||||
@@ -593,6 +692,54 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "The secret key is invalid.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminConfigNoQuorum: {
|
||||
Code: "XMinioAdminConfigNoQuorum",
|
||||
Description: "Configuration update failed because server quorum was not met",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrAdminCredentialsMismatch: {
|
||||
Code: "XMinioAdminCredentialsMismatch",
|
||||
Description: "Credentials in config mismatch with server environment variables",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrInsecureClientRequest: {
|
||||
Code: "XMinioInsecureClientRequest",
|
||||
Description: "Cannot respond to plain-text request from TLS-encrypted server",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrOperationTimedOut: {
|
||||
Code: "XMinioServerTimedOut",
|
||||
Description: "A timeout occurred while trying to lock a resource",
|
||||
HTTPStatusCode: http.StatusRequestTimeout,
|
||||
},
|
||||
ErrUnsupportedMetadata: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Your metadata headers are not supported.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrPartsSizeUnequal: {
|
||||
Code: "XMinioPartsSizeUnequal",
|
||||
Description: "All parts except the last part should be of the same size.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrObjectTampered: {
|
||||
Code: "XMinioObjectTampered",
|
||||
Description: errObjectTampered.Error(),
|
||||
HTTPStatusCode: http.StatusPartialContent,
|
||||
},
|
||||
ErrMaximumExpires: {
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
// Generic Invalid-Request error. Should be used for response errors only for unlikely
|
||||
// corner case errors for which introducing new APIErrorCode is not worth it. errorIf()
|
||||
// should be used to log the error at the source of the error for debugging purposes.
|
||||
ErrInvalidRequest: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "Invalid Request",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
// Add your error structure here.
|
||||
}
|
||||
@@ -610,15 +757,13 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
switch err {
|
||||
case errSignatureMismatch:
|
||||
apiErr = ErrSignatureDoesNotMatch
|
||||
case errContentSHA256Mismatch:
|
||||
apiErr = ErrContentSHA256Mismatch
|
||||
case errDataTooLarge:
|
||||
apiErr = ErrEntityTooLarge
|
||||
case errDataTooSmall:
|
||||
apiErr = ErrEntityTooSmall
|
||||
case errInvalidAccessKeyLength:
|
||||
case auth.ErrInvalidAccessKeyLength:
|
||||
apiErr = ErrAdminInvalidAccessKey
|
||||
case errInvalidSecretKeyLength:
|
||||
case auth.ErrInvalidSecretKeyLength:
|
||||
apiErr = ErrAdminInvalidSecretKey
|
||||
}
|
||||
|
||||
@@ -627,11 +772,34 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
return apiErr
|
||||
}
|
||||
|
||||
switch err { // SSE errors
|
||||
case errInsecureSSERequest:
|
||||
return ErrInsecureSSECustomerRequest
|
||||
case errInvalidSSEAlgorithm:
|
||||
return ErrInvalidSSECustomerAlgorithm
|
||||
case errInvalidSSEKey:
|
||||
return ErrInvalidSSECustomerKey
|
||||
case errMissingSSEKey:
|
||||
return ErrMissingSSECustomerKey
|
||||
case errMissingSSEKeyMD5:
|
||||
return ErrMissingSSECustomerKeyMD5
|
||||
case errSSEKeyMD5Mismatch:
|
||||
return ErrSSECustomerKeyMD5Mismatch
|
||||
case errObjectTampered:
|
||||
return ErrObjectTampered
|
||||
case errEncryptedObject:
|
||||
return ErrSSEEncryptedObject
|
||||
case errSSEKeyMismatch:
|
||||
return ErrAccessDenied // no access without correct key
|
||||
}
|
||||
|
||||
switch err.(type) {
|
||||
case StorageFull:
|
||||
apiErr = ErrStorageFull
|
||||
case BadDigest:
|
||||
case hash.BadDigest:
|
||||
apiErr = ErrBadDigest
|
||||
case AllAccessDisabled:
|
||||
apiErr = ErrAllAccessDisabled
|
||||
case IncompleteBody:
|
||||
apiErr = ErrIncompleteBody
|
||||
case ObjectExistsAsDirectory:
|
||||
@@ -642,8 +810,12 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrInvalidBucketName
|
||||
case BucketNotFound:
|
||||
apiErr = ErrNoSuchBucket
|
||||
case BucketAlreadyOwnedByYou:
|
||||
apiErr = ErrBucketAlreadyOwnedByYou
|
||||
case BucketNotEmpty:
|
||||
apiErr = ErrBucketNotEmpty
|
||||
case BucketAlreadyExists:
|
||||
apiErr = ErrBucketAlreadyExists
|
||||
case BucketExists:
|
||||
apiErr = ErrBucketAlreadyOwnedByYou
|
||||
case ObjectNotFound:
|
||||
@@ -668,12 +840,26 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrNoSuchUpload
|
||||
case PartTooSmall:
|
||||
apiErr = ErrEntityTooSmall
|
||||
case SHA256Mismatch:
|
||||
case SignatureDoesNotMatch:
|
||||
apiErr = ErrSignatureDoesNotMatch
|
||||
case hash.SHA256Mismatch:
|
||||
apiErr = ErrContentSHA256Mismatch
|
||||
case ObjectTooLarge:
|
||||
apiErr = ErrEntityTooLarge
|
||||
case ObjectTooSmall:
|
||||
apiErr = ErrEntityTooSmall
|
||||
case NotImplemented:
|
||||
apiErr = ErrNotImplemented
|
||||
case PolicyNotFound:
|
||||
apiErr = ErrNoSuchBucketPolicy
|
||||
case PartTooBig:
|
||||
apiErr = ErrEntityTooLarge
|
||||
case UnsupportedMetadata:
|
||||
apiErr = ErrUnsupportedMetadata
|
||||
case PartsSizeUnequal:
|
||||
apiErr = ErrPartsSizeUnequal
|
||||
case BucketPolicyNotFound:
|
||||
apiErr = ErrNoSuchBucketPolicy
|
||||
default:
|
||||
apiErr = ErrInternalError
|
||||
}
|
||||
|
||||
+43
-105
@@ -19,114 +19,52 @@ package cmd
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
var toAPIErrorCodeTests = []struct {
|
||||
err error
|
||||
errCode APIErrorCode
|
||||
}{
|
||||
{err: hash.BadDigest{}, errCode: ErrBadDigest},
|
||||
{err: hash.SHA256Mismatch{}, errCode: ErrContentSHA256Mismatch},
|
||||
{err: IncompleteBody{}, errCode: ErrIncompleteBody},
|
||||
{err: ObjectExistsAsDirectory{}, errCode: ErrObjectExistsAsDirectory},
|
||||
{err: BucketNameInvalid{}, errCode: ErrInvalidBucketName},
|
||||
{err: BucketExists{}, errCode: ErrBucketAlreadyOwnedByYou},
|
||||
{err: ObjectNotFound{}, errCode: ErrNoSuchKey},
|
||||
{err: ObjectNameInvalid{}, errCode: ErrInvalidObjectName},
|
||||
{err: InvalidUploadID{}, errCode: ErrNoSuchUpload},
|
||||
{err: InvalidPart{}, errCode: ErrInvalidPart},
|
||||
{err: InsufficientReadQuorum{}, errCode: ErrReadQuorum},
|
||||
{err: InsufficientWriteQuorum{}, errCode: ErrWriteQuorum},
|
||||
{err: UnsupportedDelimiter{}, errCode: ErrNotImplemented},
|
||||
{err: InvalidMarkerPrefixCombination{}, errCode: ErrNotImplemented},
|
||||
{err: InvalidUploadIDKeyCombination{}, errCode: ErrNotImplemented},
|
||||
{err: MalformedUploadID{}, errCode: ErrNoSuchUpload},
|
||||
{err: PartTooSmall{}, errCode: ErrEntityTooSmall},
|
||||
{err: BucketNotEmpty{}, errCode: ErrBucketNotEmpty},
|
||||
{err: BucketNotFound{}, errCode: ErrNoSuchBucket},
|
||||
{err: StorageFull{}, errCode: ErrStorageFull},
|
||||
{err: NotImplemented{}, errCode: ErrNotImplemented},
|
||||
{err: errSignatureMismatch, errCode: ErrSignatureDoesNotMatch},
|
||||
|
||||
// SSE-C errors
|
||||
{err: errInsecureSSERequest, errCode: ErrInsecureSSECustomerRequest},
|
||||
{err: errInvalidSSEAlgorithm, errCode: ErrInvalidSSECustomerAlgorithm},
|
||||
{err: errMissingSSEKey, errCode: ErrMissingSSECustomerKey},
|
||||
{err: errInvalidSSEKey, errCode: ErrInvalidSSECustomerKey},
|
||||
{err: errMissingSSEKeyMD5, errCode: ErrMissingSSECustomerKeyMD5},
|
||||
{err: errSSEKeyMD5Mismatch, errCode: ErrSSECustomerKeyMD5Mismatch},
|
||||
{err: errObjectTampered, errCode: ErrObjectTampered},
|
||||
|
||||
{err: nil, errCode: ErrNone},
|
||||
{err: errors.New("Custom error"), errCode: ErrInternalError}, // Case where err type is unknown.
|
||||
}
|
||||
|
||||
func TestAPIErrCode(t *testing.T) {
|
||||
testCases := []struct {
|
||||
err error
|
||||
errCode APIErrorCode
|
||||
}{
|
||||
// Valid cases.
|
||||
{
|
||||
BadDigest{},
|
||||
ErrBadDigest,
|
||||
},
|
||||
{
|
||||
IncompleteBody{},
|
||||
ErrIncompleteBody,
|
||||
},
|
||||
{
|
||||
ObjectExistsAsDirectory{},
|
||||
ErrObjectExistsAsDirectory,
|
||||
},
|
||||
{
|
||||
BucketNameInvalid{},
|
||||
ErrInvalidBucketName,
|
||||
},
|
||||
{
|
||||
BucketExists{},
|
||||
ErrBucketAlreadyOwnedByYou,
|
||||
},
|
||||
{
|
||||
ObjectNotFound{},
|
||||
ErrNoSuchKey,
|
||||
},
|
||||
{
|
||||
ObjectNameInvalid{},
|
||||
ErrInvalidObjectName,
|
||||
},
|
||||
{
|
||||
InvalidUploadID{},
|
||||
ErrNoSuchUpload,
|
||||
},
|
||||
{
|
||||
InvalidPart{},
|
||||
ErrInvalidPart,
|
||||
},
|
||||
{
|
||||
InsufficientReadQuorum{},
|
||||
ErrReadQuorum,
|
||||
},
|
||||
{
|
||||
InsufficientWriteQuorum{},
|
||||
ErrWriteQuorum,
|
||||
},
|
||||
{
|
||||
UnsupportedDelimiter{},
|
||||
ErrNotImplemented,
|
||||
},
|
||||
{
|
||||
InvalidMarkerPrefixCombination{},
|
||||
ErrNotImplemented,
|
||||
},
|
||||
{
|
||||
InvalidUploadIDKeyCombination{},
|
||||
ErrNotImplemented,
|
||||
},
|
||||
{
|
||||
MalformedUploadID{},
|
||||
ErrNoSuchUpload,
|
||||
},
|
||||
{
|
||||
PartTooSmall{},
|
||||
ErrEntityTooSmall,
|
||||
},
|
||||
{
|
||||
BucketNotEmpty{},
|
||||
ErrBucketNotEmpty,
|
||||
},
|
||||
{
|
||||
BucketNotFound{},
|
||||
ErrNoSuchBucket,
|
||||
},
|
||||
{
|
||||
StorageFull{},
|
||||
ErrStorageFull,
|
||||
},
|
||||
{
|
||||
errSignatureMismatch,
|
||||
ErrSignatureDoesNotMatch,
|
||||
},
|
||||
{
|
||||
errContentSHA256Mismatch,
|
||||
ErrContentSHA256Mismatch,
|
||||
}, // End of all valid cases.
|
||||
|
||||
// Case where err is nil.
|
||||
{
|
||||
nil,
|
||||
ErrNone,
|
||||
},
|
||||
|
||||
// Case where err type is unknown.
|
||||
{
|
||||
errors.New("Custom error"),
|
||||
ErrInternalError,
|
||||
},
|
||||
}
|
||||
|
||||
// Validate all the errors with their API error codes.
|
||||
for i, testCase := range testCases {
|
||||
for i, testCase := range toAPIErrorCodeTests {
|
||||
errCode := toAPIErrorCode(testCase.err)
|
||||
if errCode != testCase.errCode {
|
||||
t.Errorf("Test %d: Expected error code %d, got %d", i+1, testCase.errCode, errCode)
|
||||
|
||||
+17
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015,2016 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2015, 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.
|
||||
@@ -34,8 +34,13 @@ func mustGetRequestID(t time.Time) string {
|
||||
// Write http common headers
|
||||
func setCommonHeaders(w http.ResponseWriter) {
|
||||
// Set unique request ID for each reply.
|
||||
w.Header().Set(responseRequestIDKey, mustGetRequestID(time.Now().UTC()))
|
||||
w.Header().Set(responseRequestIDKey, mustGetRequestID(UTCNow()))
|
||||
w.Header().Set("Server", globalServerUserAgent)
|
||||
// Set `x-amz-bucket-region` only if region is set on the server
|
||||
// by default minio uses an empty region.
|
||||
if region := serverConfig.GetRegion(); region != "" {
|
||||
w.Header().Set("X-Amz-Bucket-Region", region)
|
||||
}
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
}
|
||||
|
||||
@@ -61,8 +66,16 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *h
|
||||
w.Header().Set("Last-Modified", lastModified)
|
||||
|
||||
// Set Etag if available.
|
||||
if objInfo.MD5Sum != "" {
|
||||
w.Header().Set("ETag", "\""+objInfo.MD5Sum+"\"")
|
||||
if objInfo.ETag != "" {
|
||||
w.Header().Set("ETag", "\""+objInfo.ETag+"\"")
|
||||
}
|
||||
|
||||
if objInfo.ContentType != "" {
|
||||
w.Header().Set("Content-Type", objInfo.ContentType)
|
||||
}
|
||||
|
||||
if objInfo.ContentEncoding != "" {
|
||||
w.Header().Set("Content-Encoding", objInfo.ContentEncoding)
|
||||
}
|
||||
|
||||
// Set all other user defined metadata.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2015, 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.
|
||||
@@ -18,12 +18,11 @@ package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewRequestID(t *testing.T) {
|
||||
// Ensure that it returns an alphanumeric result of length 16.
|
||||
var id = mustGetRequestID(time.Now().UTC())
|
||||
var id = mustGetRequestID(UTCNow())
|
||||
|
||||
if len(id) != 16 {
|
||||
t.Fail()
|
||||
|
||||
+40
-29
@@ -166,12 +166,13 @@ type ListBucketsResponse struct {
|
||||
|
||||
// Upload container for in progress multipart upload
|
||||
type Upload struct {
|
||||
Key string
|
||||
UploadID string `xml:"UploadId"`
|
||||
Initiator Initiator
|
||||
Owner Owner
|
||||
StorageClass string
|
||||
Initiated string
|
||||
Key string
|
||||
UploadID string `xml:"UploadId"`
|
||||
Initiator Initiator
|
||||
Owner Owner
|
||||
StorageClass string
|
||||
Initiated string
|
||||
HealUploadInfo *HealObjectInfo `xml:"HealObjectInfo,omitempty"`
|
||||
}
|
||||
|
||||
// CommonPrefix container for prefix response in ListObjectsResponse
|
||||
@@ -274,9 +275,25 @@ func getLocation(r *http.Request) string {
|
||||
return path.Clean(r.URL.Path) // Clean any trailing slashes.
|
||||
}
|
||||
|
||||
// getObjectLocation gets the relative URL for an object
|
||||
func getObjectLocation(bucketName string, key string) string {
|
||||
return "/" + bucketName + "/" + key
|
||||
// returns "https" if the tls boolean is true, "http" otherwise.
|
||||
func getURLScheme(tls bool) string {
|
||||
if tls {
|
||||
return httpsScheme
|
||||
}
|
||||
return httpScheme
|
||||
}
|
||||
|
||||
// getObjectLocation gets the fully qualified URL of an object.
|
||||
func getObjectLocation(host, proto, bucket, object string) string {
|
||||
if proto == "" {
|
||||
proto = getURLScheme(globalIsSSL)
|
||||
}
|
||||
u := url.URL{
|
||||
Host: host,
|
||||
Path: path.Join(slashSeparator, bucket, object),
|
||||
Scheme: proto,
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// generates ListBucketsResponse from array of BucketInfo which can be
|
||||
@@ -287,12 +304,10 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
|
||||
var owner = Owner{}
|
||||
|
||||
owner.ID = globalMinioDefaultOwnerID
|
||||
owner.DisplayName = globalMinioDefaultOwnerID
|
||||
|
||||
for _, bucket := range buckets {
|
||||
var listbucket = Bucket{}
|
||||
listbucket.Name = bucket.Name
|
||||
listbucket.CreationDate = bucket.Created.Format(timeFormatAMZLong)
|
||||
listbucket.CreationDate = bucket.Created.UTC().Format(timeFormatAMZLong)
|
||||
listbucket.HealBucketInfo = bucket.HealBucketInfo
|
||||
listbuckets = append(listbuckets, listbucket)
|
||||
}
|
||||
@@ -311,8 +326,6 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max
|
||||
var data = ListObjectsResponse{}
|
||||
|
||||
owner.ID = globalMinioDefaultOwnerID
|
||||
owner.DisplayName = globalMinioDefaultOwnerID
|
||||
|
||||
for _, object := range resp.Objects {
|
||||
var content = Object{}
|
||||
if object.Name == "" {
|
||||
@@ -320,8 +333,8 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max
|
||||
}
|
||||
content.Key = object.Name
|
||||
content.LastModified = object.ModTime.UTC().Format(timeFormatAMZLong)
|
||||
if object.MD5Sum != "" {
|
||||
content.ETag = "\"" + object.MD5Sum + "\""
|
||||
if object.ETag != "" {
|
||||
content.ETag = "\"" + object.ETag + "\""
|
||||
}
|
||||
content.Size = object.Size
|
||||
content.StorageClass = globalMinioDefaultStorageClass
|
||||
@@ -351,26 +364,25 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max
|
||||
}
|
||||
|
||||
// generates an ListObjectsV2 response for the said bucket with other enumerated options.
|
||||
func generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter string, fetchOwner bool, maxKeys int, resp ListObjectsInfo) ListObjectsV2Response {
|
||||
func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, delimiter string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string) ListObjectsV2Response {
|
||||
var contents []Object
|
||||
var prefixes []CommonPrefix
|
||||
var commonPrefixes []CommonPrefix
|
||||
var owner = Owner{}
|
||||
var data = ListObjectsV2Response{}
|
||||
|
||||
if fetchOwner {
|
||||
owner.ID = globalMinioDefaultOwnerID
|
||||
owner.DisplayName = globalMinioDefaultOwnerID
|
||||
}
|
||||
|
||||
for _, object := range resp.Objects {
|
||||
for _, object := range objects {
|
||||
var content = Object{}
|
||||
if object.Name == "" {
|
||||
continue
|
||||
}
|
||||
content.Key = object.Name
|
||||
content.LastModified = object.ModTime.UTC().Format(timeFormatAMZLong)
|
||||
if object.MD5Sum != "" {
|
||||
content.ETag = "\"" + object.MD5Sum + "\""
|
||||
if object.ETag != "" {
|
||||
content.ETag = "\"" + object.ETag + "\""
|
||||
}
|
||||
content.Size = object.Size
|
||||
content.StorageClass = globalMinioDefaultStorageClass
|
||||
@@ -386,14 +398,14 @@ func generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter
|
||||
data.Prefix = prefix
|
||||
data.MaxKeys = maxKeys
|
||||
data.ContinuationToken = token
|
||||
data.NextContinuationToken = resp.NextMarker
|
||||
data.IsTruncated = resp.IsTruncated
|
||||
for _, prefix := range resp.Prefixes {
|
||||
data.NextContinuationToken = nextToken
|
||||
data.IsTruncated = isTruncated
|
||||
for _, prefix := range prefixes {
|
||||
var prefixItem = CommonPrefix{}
|
||||
prefixItem.Prefix = prefix
|
||||
prefixes = append(prefixes, prefixItem)
|
||||
commonPrefixes = append(commonPrefixes, prefixItem)
|
||||
}
|
||||
data.CommonPrefixes = prefixes
|
||||
data.CommonPrefixes = commonPrefixes
|
||||
data.KeyCount = len(data.Contents) + len(data.CommonPrefixes)
|
||||
return data
|
||||
}
|
||||
@@ -442,9 +454,7 @@ func generateListPartsResponse(partsInfo ListPartsInfo) ListPartsResponse {
|
||||
listPartsResponse.UploadID = partsInfo.UploadID
|
||||
listPartsResponse.StorageClass = globalMinioDefaultStorageClass
|
||||
listPartsResponse.Initiator.ID = globalMinioDefaultOwnerID
|
||||
listPartsResponse.Initiator.DisplayName = globalMinioDefaultOwnerID
|
||||
listPartsResponse.Owner.ID = globalMinioDefaultOwnerID
|
||||
listPartsResponse.Owner.DisplayName = globalMinioDefaultOwnerID
|
||||
|
||||
listPartsResponse.MaxParts = partsInfo.MaxParts
|
||||
listPartsResponse.PartNumberMarker = partsInfo.PartNumberMarker
|
||||
@@ -488,6 +498,7 @@ func generateListMultipartUploadsResponse(bucket string, multipartsInfo ListMult
|
||||
newUpload.UploadID = upload.UploadID
|
||||
newUpload.Key = upload.Object
|
||||
newUpload.Initiated = upload.Initiated.UTC().Format(timeFormatAMZLong)
|
||||
newUpload.HealUploadInfo = upload.HealUploadInfo
|
||||
listMultipartUploadsResponse.Uploads[index] = newUpload
|
||||
}
|
||||
return listMultipartUploadsResponse
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests object location.
|
||||
func TestObjectLocation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
host, proto, bucket, object string
|
||||
expectedLocation string
|
||||
}{
|
||||
// Server binding to localhost IP with https.
|
||||
{
|
||||
host: "127.0.0.1:9000",
|
||||
proto: httpsScheme,
|
||||
bucket: "testbucket1",
|
||||
object: "test/1.txt",
|
||||
expectedLocation: "https://127.0.0.1:9000/testbucket1/test/1.txt",
|
||||
},
|
||||
// Server binding to fqdn.
|
||||
{
|
||||
host: "s3.mybucket.org",
|
||||
proto: httpScheme,
|
||||
bucket: "mybucket",
|
||||
object: "test/1.txt",
|
||||
expectedLocation: "http://s3.mybucket.org/mybucket/test/1.txt",
|
||||
},
|
||||
// Server binding to fqdn.
|
||||
{
|
||||
host: "mys3.mybucket.org",
|
||||
proto: "",
|
||||
bucket: "mybucket",
|
||||
object: "test/1.txt",
|
||||
expectedLocation: "http://mys3.mybucket.org/mybucket/test/1.txt",
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
gotLocation := getObjectLocation(testCase.host, testCase.proto, testCase.bucket, testCase.object)
|
||||
if testCase.expectedLocation != gotLocation {
|
||||
t.Errorf("Test %d: expected %s, got %s", i+1, testCase.expectedLocation, gotLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests getURLScheme function behavior.
|
||||
func TestGetURLScheme(t *testing.T) {
|
||||
tls := false
|
||||
gotScheme := getURLScheme(tls)
|
||||
if gotScheme != httpScheme {
|
||||
t.Errorf("Expected %s, got %s", httpScheme, gotScheme)
|
||||
}
|
||||
tls = true
|
||||
gotScheme = getURLScheme(tls)
|
||||
if gotScheme != httpsScheme {
|
||||
t.Errorf("Expected %s, got %s", httpsScheme, gotScheme)
|
||||
}
|
||||
}
|
||||
+66
-60
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import router "github.com/gorilla/mux"
|
||||
import "net/http"
|
||||
|
||||
// objectAPIHandler implements and provides http handlers for S3 API.
|
||||
type objectAPIHandlers struct {
|
||||
@@ -32,70 +33,75 @@ func registerAPIRouter(mux *router.Router) {
|
||||
|
||||
// API Router
|
||||
apiRouter := mux.NewRoute().PathPrefix("/").Subrouter()
|
||||
var routers []*router.Router
|
||||
if globalDomainName != "" {
|
||||
routers = append(routers, apiRouter.Host("{bucket:.+}."+globalDomainName).Subrouter())
|
||||
}
|
||||
routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter())
|
||||
|
||||
// Bucket router
|
||||
bucket := apiRouter.PathPrefix("/{bucket}").Subrouter()
|
||||
for _, bucket := range routers {
|
||||
// Object operations
|
||||
// HeadObject
|
||||
bucket.Methods("HEAD").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.HeadObjectHandler))
|
||||
// CopyObjectPart
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(httpTraceAll(api.CopyObjectPartHandler)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||
// PutObjectPart
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.PutObjectPartHandler)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||
// ListObjectPxarts
|
||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.ListObjectPartsHandler)).Queries("uploadId", "{uploadId:.*}")
|
||||
// CompleteMultipartUpload
|
||||
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.CompleteMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}")
|
||||
// NewMultipartUpload
|
||||
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.NewMultipartUploadHandler)).Queries("uploads", "")
|
||||
// AbortMultipartUpload
|
||||
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.AbortMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}")
|
||||
// GetObject
|
||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectHandler))
|
||||
// CopyObject
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(httpTraceAll(api.CopyObjectHandler))
|
||||
// PutObject
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.PutObjectHandler))
|
||||
// DeleteObject
|
||||
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.DeleteObjectHandler))
|
||||
|
||||
/// Object operations
|
||||
|
||||
// HeadObject
|
||||
bucket.Methods("HEAD").Path("/{object:.+}").HandlerFunc(api.HeadObjectHandler)
|
||||
// CopyObjectPart
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(api.CopyObjectPartHandler).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||
// 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("events", "{events:.*}")
|
||||
// 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)
|
||||
/// Bucket operations
|
||||
// GetBucketLocation
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketLocationHandler)).Queries("location", "")
|
||||
// GetBucketPolicy
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketPolicyHandler)).Queries("policy", "")
|
||||
// GetBucketNotification
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketNotificationHandler)).Queries("notification", "")
|
||||
// ListenBucketNotification
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListenBucketNotificationHandler)).Queries("events", "{events:.*}")
|
||||
// ListMultipartUploads
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListMultipartUploadsHandler)).Queries("uploads", "")
|
||||
// ListObjectsV2
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListObjectsV2Handler)).Queries("list-type", "2")
|
||||
// ListObjectsV1 (Legacy)
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListObjectsV1Handler))
|
||||
// PutBucketPolicy
|
||||
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketPolicyHandler)).Queries("policy", "")
|
||||
// PutBucketNotification
|
||||
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketNotificationHandler)).Queries("notification", "")
|
||||
// PutBucket
|
||||
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketHandler))
|
||||
// HeadBucket
|
||||
bucket.Methods("HEAD").HandlerFunc(httpTraceAll(api.HeadBucketHandler))
|
||||
// PostPolicy
|
||||
bucket.Methods("POST").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(httpTraceAll(api.PostPolicyBucketHandler))
|
||||
// DeleteMultipleObjects
|
||||
bucket.Methods("POST").HandlerFunc(httpTraceAll(api.DeleteMultipleObjectsHandler)).Queries("delete", "")
|
||||
// DeleteBucketPolicy
|
||||
bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketPolicyHandler)).Queries("policy", "")
|
||||
// DeleteBucket
|
||||
bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketHandler))
|
||||
}
|
||||
|
||||
/// Root operation
|
||||
|
||||
// ListBuckets
|
||||
apiRouter.Methods("GET").HandlerFunc(api.ListBucketsHandler)
|
||||
apiRouter.Methods("GET").Path("/").HandlerFunc(httpTraceAll(api.ListBucketsHandler))
|
||||
|
||||
// If none of the routes match.
|
||||
apiRouter.NotFoundHandler = http.HandlerFunc(httpTraceAll(notFoundHandler))
|
||||
}
|
||||
|
||||
+39
-29
@@ -63,7 +63,8 @@ func isRequestPostPolicySignatureV4(r *http.Request) bool {
|
||||
|
||||
// Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation.
|
||||
func isRequestSignStreamingV4(r *http.Request) bool {
|
||||
return r.Header.Get("x-amz-content-sha256") == streamingContentSHA256 && r.Method == httpPUT
|
||||
return r.Header.Get("x-amz-content-sha256") == streamingContentSHA256 &&
|
||||
r.Method == httpPUT
|
||||
}
|
||||
|
||||
// Authorization type.
|
||||
@@ -112,21 +113,26 @@ func checkRequestAuthType(r *http.Request, bucket, policyAction, region string)
|
||||
// Signature V2 validation.
|
||||
s3Error := isReqAuthenticatedV2(r)
|
||||
if s3Error != ErrNone {
|
||||
errorIf(errSignatureMismatch, dumpRequest(r))
|
||||
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
||||
}
|
||||
return s3Error
|
||||
case authTypeSigned, authTypePresigned:
|
||||
s3Error := isReqAuthenticated(r, region)
|
||||
if s3Error != ErrNone {
|
||||
errorIf(errSignatureMismatch, dumpRequest(r))
|
||||
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
||||
}
|
||||
return s3Error
|
||||
}
|
||||
|
||||
if reqAuthType == authTypeAnonymous && policyAction != "" {
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
||||
return enforceBucketPolicy(bucket, policyAction, r.URL.Path,
|
||||
r.Referer(), r.URL.Query())
|
||||
sourceIP := getSourceIPAddress(r)
|
||||
resource, err := getResource(r.URL.Path, r.Host, globalDomainName)
|
||||
if err != nil {
|
||||
return ErrInternalError
|
||||
}
|
||||
return enforceBucketPolicy(bucket, policyAction, resource,
|
||||
r.Referer(), sourceIP, r.URL.Query())
|
||||
}
|
||||
|
||||
// By default return ErrAccessDenied
|
||||
@@ -141,19 +147,16 @@ func isReqAuthenticatedV2(r *http.Request) (s3Error APIErrorCode) {
|
||||
return doesPresignV2SignatureMatch(r)
|
||||
}
|
||||
|
||||
func reqSignatureV4Verify(r *http.Request) (s3Error APIErrorCode) {
|
||||
sha256sum := r.Header.Get("X-Amz-Content-Sha256")
|
||||
// Skips calculating sha256 on the payload on server,
|
||||
// if client requested for it.
|
||||
if skipContentSha256Cksum(r) {
|
||||
sha256sum = unsignedPayload
|
||||
func reqSignatureV4Verify(r *http.Request, region string) (s3Error APIErrorCode) {
|
||||
sha256sum := getContentSha256Cksum(r)
|
||||
switch {
|
||||
case isRequestSignatureV4(r):
|
||||
return doesSignatureMatch(sha256sum, r, region)
|
||||
case isRequestPresignedSignatureV4(r):
|
||||
return doesPresignedSignatureMatch(sha256sum, r, region)
|
||||
default:
|
||||
return ErrAccessDenied
|
||||
}
|
||||
if isRequestSignatureV4(r) {
|
||||
return doesSignatureMatch(sha256sum, r, serverConfig.GetRegion())
|
||||
} else if isRequestPresignedSignatureV4(r) {
|
||||
return doesPresignedSignatureMatch(sha256sum, r, serverConfig.GetRegion())
|
||||
}
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
// Verify if request has valid AWS Signature Version '4'.
|
||||
@@ -161,32 +164,39 @@ func isReqAuthenticated(r *http.Request, region string) (s3Error APIErrorCode) {
|
||||
if r == nil {
|
||||
return ErrInternalError
|
||||
}
|
||||
if errCode := reqSignatureV4Verify(r, region); errCode != ErrNone {
|
||||
return errCode
|
||||
}
|
||||
payload, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to read request body for signature verification")
|
||||
return ErrInternalError
|
||||
}
|
||||
|
||||
// Populate back the payload.
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
||||
|
||||
// Verify Content-Md5, if payload is set.
|
||||
if r.Header.Get("Content-Md5") != "" {
|
||||
if r.Header.Get("Content-Md5") != getMD5HashBase64(payload) {
|
||||
return ErrBadDigest
|
||||
}
|
||||
}
|
||||
// Populate back the payload.
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
||||
// Skips calculating sha256 on the payload on server, if client requested for it.
|
||||
var sha256sum string
|
||||
|
||||
if skipContentSha256Cksum(r) {
|
||||
sha256sum = unsignedPayload
|
||||
} else {
|
||||
sha256sum = getSHA256Hash(payload)
|
||||
return ErrNone
|
||||
}
|
||||
if isRequestSignatureV4(r) {
|
||||
return doesSignatureMatch(sha256sum, r, region)
|
||||
} else if isRequestPresignedSignatureV4(r) {
|
||||
return doesPresignedSignatureMatch(sha256sum, r, region)
|
||||
|
||||
// Verify that X-Amz-Content-Sha256 Header == sha256(payload)
|
||||
// If X-Amz-Content-Sha256 header is not sent then we don't calculate/verify sha256(payload)
|
||||
sum := r.Header.Get("X-Amz-Content-Sha256")
|
||||
if isRequestPresignedSignatureV4(r) {
|
||||
sum = r.URL.Query().Get("X-Amz-Content-Sha256")
|
||||
}
|
||||
return ErrAccessDenied
|
||||
if sum != "" && sum != getSHA256Hash(payload) {
|
||||
return ErrContentSHA256Mismatch
|
||||
}
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// authHandler - handles all the incoming authorization headers and validates them if possible.
|
||||
|
||||
@@ -21,7 +21,10 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Test get request auth type.
|
||||
@@ -43,6 +46,7 @@ func TestGetRequestAuthType(t *testing.T) {
|
||||
Header: http.Header{
|
||||
"Authorization": []string{"AWS4-HMAC-SHA256 <cred_string>"},
|
||||
"X-Amz-Content-Sha256": []string{streamingContentSHA256},
|
||||
"Content-Encoding": []string{streamingContentEncoding},
|
||||
},
|
||||
Method: "PUT",
|
||||
},
|
||||
@@ -307,15 +311,29 @@ func mustNewSignedRequest(method string, urlStr string, contentLength int64, bod
|
||||
return req
|
||||
}
|
||||
|
||||
func mustNewSignedBadMD5Request(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
|
||||
req := mustNewRequest(method, urlStr, contentLength, body, t)
|
||||
req.Header.Set("Content-Md5", "YWFhYWFhYWFhYWFhYWFhCg==")
|
||||
cred := serverConfig.GetCredential()
|
||||
if err := signRequestV4(req, cred.AccessKey, cred.SecretKey); err != nil {
|
||||
t.Fatalf("Unable to initialized new signed http request %s", err)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
// Tests is requested authenticated function, tests replies for s3 errors.
|
||||
func TestIsReqAuthenticated(t *testing.T) {
|
||||
path, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer removeAll(path)
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
||||
if err != nil {
|
||||
t.Fatalf("unable create credential, %s", err)
|
||||
}
|
||||
|
||||
creds := newCredentialWithKeys("myuser", "mypassword")
|
||||
serverConfig.SetCredential(creds)
|
||||
|
||||
// List of test cases for validating http request authentication.
|
||||
@@ -328,16 +346,13 @@ func TestIsReqAuthenticated(t *testing.T) {
|
||||
// When request is unsigned, access denied is returned.
|
||||
{mustNewRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrAccessDenied},
|
||||
// When request is properly signed, but has bad Content-MD5 header.
|
||||
{mustNewSignedRequest("PUT", "http://127.0.0.1:9000", 5, bytes.NewReader([]byte("hello")), t), ErrBadDigest},
|
||||
{mustNewSignedBadMD5Request("PUT", "http://127.0.0.1:9000/", 5, bytes.NewReader([]byte("hello")), t), ErrBadDigest},
|
||||
// When request is properly signed, error is none.
|
||||
{mustNewSignedRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrNone},
|
||||
}
|
||||
|
||||
// Validates all testcases.
|
||||
for _, testCase := range testCases {
|
||||
if testCase.s3Error == ErrBadDigest {
|
||||
testCase.req.Header.Set("Content-Md5", "garbage")
|
||||
}
|
||||
if s3Error := isReqAuthenticated(testCase.req, serverConfig.GetRegion()); s3Error != testCase.s3Error {
|
||||
t.Fatalf("Unexpected s3error returned wanted %d, got %d", testCase.s3Error, s3Error)
|
||||
}
|
||||
|
||||
+48
-38
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -52,10 +52,10 @@ type authConfig struct {
|
||||
|
||||
// AuthRPCClient is a authenticated RPC client which does authentication before doing Call().
|
||||
type AuthRPCClient struct {
|
||||
sync.Mutex // Mutex to lock this object.
|
||||
rpcClient *RPCClient // Reconnectable RPC client to make any RPC call.
|
||||
config authConfig // Authentication configuration information.
|
||||
authToken string // Authentication token.
|
||||
sync.RWMutex // Mutex to lock this object.
|
||||
rpcClient *RPCClient // Reconnectable RPC client to make any RPC call.
|
||||
config authConfig // Authentication configuration information.
|
||||
authToken string // Authentication token.
|
||||
}
|
||||
|
||||
// newAuthRPCClient - returns a JWT based authenticated (go) rpc client, which does automatic reconnect.
|
||||
@@ -78,57 +78,67 @@ func newAuthRPCClient(config authConfig) *AuthRPCClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Login - a jwt based authentication is performed with rpc server.
|
||||
// Login a JWT based authentication is performed with rpc server.
|
||||
func (authClient *AuthRPCClient) Login() (err error) {
|
||||
// Login should be attempted one at a time.
|
||||
//
|
||||
// The reason for large region lock here is
|
||||
// to avoid two simultaneous login attempts
|
||||
// racing over each other.
|
||||
//
|
||||
// #1 Login() gets the lock proceeds to login.
|
||||
// #2 Login() waits for the unlock to happen
|
||||
// after login in #1.
|
||||
// #1 Successfully completes login saves the
|
||||
// newly acquired token.
|
||||
// #2 Successfully gets the lock and proceeds,
|
||||
// but since we have acquired the token
|
||||
// already the call quickly returns.
|
||||
authClient.Lock()
|
||||
defer authClient.Unlock()
|
||||
|
||||
// Return if already logged in.
|
||||
if authClient.authToken != "" {
|
||||
return nil
|
||||
// Attempt to login if not logged in already.
|
||||
if authClient.authToken == "" {
|
||||
authClient.authToken, err = authenticateNode(authClient.config.accessKey, authClient.config.secretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Login to authenticate your token.
|
||||
var (
|
||||
loginMethod = authClient.config.serviceName + loginMethodName
|
||||
loginArgs = LoginRPCArgs{
|
||||
AuthToken: authClient.authToken,
|
||||
Version: Version,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
)
|
||||
if err = authClient.rpcClient.Call(loginMethod, &loginArgs, &LoginRPCReply{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Call login.
|
||||
args := LoginRPCArgs{
|
||||
Username: authClient.config.accessKey,
|
||||
Password: authClient.config.secretKey,
|
||||
Version: Version,
|
||||
RequestTime: time.Now().UTC(),
|
||||
}
|
||||
|
||||
reply := LoginRPCReply{}
|
||||
serviceMethod := authClient.config.serviceName + loginMethodName
|
||||
if err = authClient.rpcClient.Call(serviceMethod, &args, &reply); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Logged in successfully.
|
||||
authClient.authToken = reply.AuthToken
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// call makes a RPC call after logs into the server.
|
||||
func (authClient *AuthRPCClient) call(serviceMethod string, args interface {
|
||||
SetAuthToken(authToken string)
|
||||
SetRequestTime(requestTime time.Time)
|
||||
}, reply interface{}) (err error) {
|
||||
// On successful login, execute RPC call.
|
||||
if err = authClient.Login(); err == nil {
|
||||
// Set token and timestamp before the rpc call.
|
||||
args.SetAuthToken(authClient.authToken)
|
||||
args.SetRequestTime(time.Now().UTC())
|
||||
if err = authClient.Login(); err != nil {
|
||||
return err
|
||||
} // On successful login, execute RPC call.
|
||||
|
||||
// Do RPC call.
|
||||
err = authClient.rpcClient.Call(serviceMethod, args, reply)
|
||||
}
|
||||
return err
|
||||
authClient.RLock()
|
||||
// Set token before the rpc call.
|
||||
args.SetAuthToken(authClient.authToken)
|
||||
authClient.RUnlock()
|
||||
|
||||
// Do an RPC call.
|
||||
return authClient.rpcClient.Call(serviceMethod, args, reply)
|
||||
}
|
||||
|
||||
// Call executes RPC call till success or globalAuthRPCRetryThreshold on ErrShutdown.
|
||||
func (authClient *AuthRPCClient) Call(serviceMethod string, args interface {
|
||||
SetAuthToken(authToken string)
|
||||
SetRequestTime(requestTime time.Time)
|
||||
}, reply interface{}) (err error) {
|
||||
|
||||
// Done channel is used to close any lingering retry routine, as soon
|
||||
|
||||
@@ -20,8 +20,7 @@ package cmd
|
||||
const loginMethodName = ".Login"
|
||||
|
||||
// AuthRPCServer RPC server authenticates using JWT.
|
||||
type AuthRPCServer struct {
|
||||
}
|
||||
type AuthRPCServer struct{}
|
||||
|
||||
// Login - Handles JWT based RPC login.
|
||||
func (b AuthRPCServer) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
|
||||
@@ -30,14 +29,10 @@ func (b AuthRPCServer) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Authenticate using JWT.
|
||||
token, err := authenticateNode(args.Username, args.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
// Return an error if token is not valid.
|
||||
if !isAuthTokenValid(args.AuthToken) {
|
||||
return errAuthentication
|
||||
}
|
||||
|
||||
// Return the token.
|
||||
reply.AuthToken = token
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+16
-45
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -26,8 +27,12 @@ func TestLogin(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test config - %v", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
creds := serverConfig.GetCredential()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ls := AuthRPCServer{}
|
||||
testCases := []struct {
|
||||
args LoginRPCArgs
|
||||
@@ -37,9 +42,8 @@ func TestLogin(t *testing.T) {
|
||||
// Valid case.
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: nil,
|
||||
@@ -47,9 +51,8 @@ func TestLogin(t *testing.T) {
|
||||
// Valid username, password and request time, not version.
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: "INVALID-" + Version,
|
||||
AuthToken: token,
|
||||
Version: "INVALID-" + Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: errServerVersionMismatch,
|
||||
@@ -57,49 +60,17 @@ func TestLogin(t *testing.T) {
|
||||
// Valid username, password and version, not request time
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 20 * time.Minute,
|
||||
expectedErr: errServerTimeMismatch,
|
||||
},
|
||||
// Invalid username length
|
||||
// Invalid token, fails with authentication error
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: "aaa",
|
||||
Password: "minio123",
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: errInvalidAccessKeyLength,
|
||||
},
|
||||
// Invalid password length
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: globalMinioDefaultOwnerID,
|
||||
Password: "aaa",
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: errInvalidSecretKeyLength,
|
||||
},
|
||||
// Invalid username
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: "aaaaa",
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: errInvalidAccessKeyID,
|
||||
},
|
||||
// Invalid password
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: "aaaaaaaa",
|
||||
Version: Version,
|
||||
AuthToken: "",
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: errAuthentication,
|
||||
@@ -107,7 +78,7 @@ func TestLogin(t *testing.T) {
|
||||
}
|
||||
for i, test := range testCases {
|
||||
reply := LoginRPCReply{}
|
||||
test.args.RequestTime = time.Now().Add(test.skewTime).UTC()
|
||||
test.args.RequestTime = UTCNow().Add(test.skewTime)
|
||||
err := ls.Login(&test.args, &reply)
|
||||
if err != test.expectedErr {
|
||||
t.Errorf("Test %d: Expected error %v but received %v",
|
||||
|
||||
+73
-75
@@ -21,32 +21,16 @@ import (
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
// Prepare benchmark backend
|
||||
// Prepare XL/FS backend for benchmark.
|
||||
func prepareBenchmarkBackend(instanceType string) (ObjectLayer, []string, error) {
|
||||
switch instanceType {
|
||||
// Total number of disks for FS backend is set to 1.
|
||||
case FSTestStr:
|
||||
obj, disk, err := prepareFS()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return obj, []string{disk}, nil
|
||||
// Total number of disks for XL backend is set to 16.
|
||||
case XLTestStr:
|
||||
return prepareXL()
|
||||
}
|
||||
obj, disk, err := prepareFS()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return obj, []string{disk}, nil
|
||||
return prepareTestBackend(instanceType)
|
||||
}
|
||||
|
||||
// Benchmark utility functions for ObjectLayer.PutObject().
|
||||
@@ -56,7 +40,7 @@ func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucket(bucket)
|
||||
err = obj.MakeBucketWithLocation(bucket, "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -66,20 +50,23 @@ func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for PutObject.
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = getMD5Hash(textData)
|
||||
sha256sum := ""
|
||||
|
||||
md5hex := getMD5Hash(textData)
|
||||
sha256hex := ""
|
||||
|
||||
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
|
||||
b.ReportAllocs()
|
||||
// the actual benchmark for PutObject starts here. Reset the benchmark timer.
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// insert the object.
|
||||
objInfo, err := obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata, sha256sum)
|
||||
objInfo, err := obj.PutObject(bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if objInfo.MD5Sum != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.MD5Sum, metadata["md5Sum"])
|
||||
if objInfo.ETag != md5hex {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, md5hex)
|
||||
}
|
||||
}
|
||||
// Benchmark ends here. Stop timer.
|
||||
@@ -95,28 +82,29 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
||||
object := getRandomObjectName()
|
||||
|
||||
// create bucket.
|
||||
err = obj.MakeBucket(bucket)
|
||||
err = obj.MakeBucketWithLocation(bucket, "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
objSize := 128 * humanize.MiByte
|
||||
|
||||
// PutObjectPart returns md5Sum of the object inserted.
|
||||
// md5Sum variable is assigned with that value.
|
||||
var md5Sum, uploadID string
|
||||
// PutObjectPart returns etag of the object inserted.
|
||||
// etag variable is assigned with that value.
|
||||
var etag, uploadID string
|
||||
// get text data generated for number of bytes equal to object size.
|
||||
textData := generateBytesData(objSize)
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for NewMultipartUpload.
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = getMD5Hash(textData)
|
||||
sha256sum := ""
|
||||
uploadID, err = obj.NewMultipartUpload(bucket, object, metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
md5hex := getMD5Hash(textData)
|
||||
sha256hex := ""
|
||||
|
||||
var textPartData []byte
|
||||
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
|
||||
b.ReportAllocs()
|
||||
@@ -131,15 +119,15 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
||||
} else {
|
||||
textPartData = textData[j*partSize:]
|
||||
}
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = getMD5Hash([]byte(textPartData))
|
||||
md5hex = getMD5Hash([]byte(textPartData))
|
||||
var partInfo PartInfo
|
||||
partInfo, err = obj.PutObjectPart(bucket, object, uploadID, j, int64(len(textPartData)), bytes.NewBuffer(textPartData), metadata["md5Sum"], sha256sum)
|
||||
partInfo, err = obj.PutObjectPart(bucket, object, uploadID, j,
|
||||
mustGetHashReader(b, bytes.NewBuffer(textPartData), int64(len(textPartData)), md5hex, sha256hex))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if partInfo.ETag != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, md5Sum, metadata["md5Sum"])
|
||||
if partInfo.ETag != md5hex {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, etag, md5hex)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,7 +141,7 @@ func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -172,7 +160,7 @@ func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -191,7 +179,7 @@ func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -211,33 +199,37 @@ func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucket(bucket)
|
||||
err = obj.MakeBucketWithLocation(bucket, "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
sha256sum := ""
|
||||
textData := generateBytesData(objSize)
|
||||
|
||||
// generate etag for the generated data.
|
||||
// etag of the data to written is required as input for PutObject.
|
||||
// PutObject is the functions which writes the data onto the FS/XL backend.
|
||||
metadata := make(map[string]string)
|
||||
|
||||
// get text data generated for number of bytes equal to object size.
|
||||
md5hex := getMD5Hash(textData)
|
||||
sha256hex := ""
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
// get text data generated for number of bytes equal to object size.
|
||||
textData := generateBytesData(objSize)
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for PutObject.
|
||||
// PutObject is the functions which writes the data onto the FS/XL backend.
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = getMD5Hash(textData)
|
||||
// insert the object.
|
||||
var objInfo ObjectInfo
|
||||
objInfo, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata, sha256sum)
|
||||
objInfo, err = obj.PutObject(bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if objInfo.MD5Sum != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.MD5Sum, metadata["md5Sum"])
|
||||
if objInfo.ETag != md5hex {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, md5hex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +253,7 @@ func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
func getRandomByte() []byte {
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
// seeding the random number generator.
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
rand.Seed(UTCNow().UnixNano())
|
||||
var b byte
|
||||
// pick a character randomly.
|
||||
b = letterBytes[rand.Intn(len(letterBytes))]
|
||||
@@ -280,7 +272,7 @@ func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -299,7 +291,7 @@ func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -319,12 +311,12 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucket(bucket)
|
||||
err = obj.MakeBucketWithLocation(bucket, "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -334,8 +326,10 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for PutObject.
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = getMD5Hash([]byte(textData))
|
||||
sha256sum := ""
|
||||
|
||||
md5hex := getMD5Hash([]byte(textData))
|
||||
sha256hex := ""
|
||||
|
||||
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
|
||||
b.ReportAllocs()
|
||||
// the actual benchmark for PutObject starts here. Reset the benchmark timer.
|
||||
@@ -345,12 +339,13 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
// insert the object.
|
||||
objInfo, err := obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata, sha256sum)
|
||||
objInfo, err := obj.PutObject(bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if objInfo.MD5Sum != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", objInfo.MD5Sum, metadata["md5Sum"])
|
||||
if objInfo.ETag != md5hex {
|
||||
b.Fatalf("Write no: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", objInfo.ETag, md5hex)
|
||||
}
|
||||
i++
|
||||
}
|
||||
@@ -367,33 +362,36 @@ func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucket(bucket)
|
||||
err = obj.MakeBucketWithLocation(bucket, "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// get text data generated for number of bytes equal to object size.
|
||||
textData := generateBytesData(objSize)
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for PutObject.
|
||||
// PutObject is the functions which writes the data onto the FS/XL backend.
|
||||
metadata := make(map[string]string)
|
||||
|
||||
md5hex := getMD5Hash([]byte(textData))
|
||||
sha256hex := ""
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
// get text data generated for number of bytes equal to object size.
|
||||
textData := generateBytesData(objSize)
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for PutObject.
|
||||
// PutObject is the functions which writes the data onto the FS/XL backend.
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = getMD5Hash([]byte(textData))
|
||||
sha256sum := ""
|
||||
// insert the object.
|
||||
var objInfo ObjectInfo
|
||||
objInfo, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata, sha256sum)
|
||||
objInfo, err = obj.PutObject(bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if objInfo.MD5Sum != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.MD5Sum, metadata["md5Sum"])
|
||||
if objInfo.ETag != md5hex {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, md5hex)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// BrowserFlag - wrapper bool type.
|
||||
type BrowserFlag bool
|
||||
|
||||
// String - returns string of BrowserFlag.
|
||||
func (bf BrowserFlag) String() string {
|
||||
if bf {
|
||||
return "on"
|
||||
}
|
||||
|
||||
return "off"
|
||||
}
|
||||
|
||||
// MarshalJSON - converts BrowserFlag into JSON data.
|
||||
func (bf BrowserFlag) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(bf.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON - parses given data into BrowserFlag.
|
||||
func (bf *BrowserFlag) UnmarshalJSON(data []byte) (err error) {
|
||||
var s string
|
||||
if err = json.Unmarshal(data, &s); err == nil {
|
||||
b := BrowserFlag(true)
|
||||
if s == "" {
|
||||
// Empty string is treated as valid.
|
||||
*bf = b
|
||||
} else if b, err = ParseBrowserFlag(s); err == nil {
|
||||
*bf = b
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ParseBrowserFlag - parses string into BrowserFlag.
|
||||
func ParseBrowserFlag(s string) (bf BrowserFlag, err error) {
|
||||
if s == "on" {
|
||||
bf = true
|
||||
} else if s == "off" {
|
||||
bf = false
|
||||
} else {
|
||||
err = fmt.Errorf("invalid value ‘%s’ for BrowserFlag", s)
|
||||
}
|
||||
|
||||
return bf, err
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test BrowserFlag.String()
|
||||
func TestBrowserFlagString(t *testing.T) {
|
||||
var bf BrowserFlag
|
||||
|
||||
testCases := []struct {
|
||||
flag BrowserFlag
|
||||
expectedResult string
|
||||
}{
|
||||
{bf, "off"},
|
||||
{BrowserFlag(true), "on"},
|
||||
{BrowserFlag(false), "off"},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
str := testCase.flag.String()
|
||||
if testCase.expectedResult != str {
|
||||
t.Fatalf("expected: %v, got: %v", testCase.expectedResult, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test BrowserFlag.MarshalJSON()
|
||||
func TestBrowserFlagMarshalJSON(t *testing.T) {
|
||||
var bf BrowserFlag
|
||||
|
||||
testCases := []struct {
|
||||
flag BrowserFlag
|
||||
expectedResult string
|
||||
}{
|
||||
{bf, `"off"`},
|
||||
{BrowserFlag(true), `"on"`},
|
||||
{BrowserFlag(false), `"off"`},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
data, _ := testCase.flag.MarshalJSON()
|
||||
if testCase.expectedResult != string(data) {
|
||||
t.Fatalf("expected: %v, got: %v", testCase.expectedResult, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test BrowserFlag.UnmarshalJSON()
|
||||
func TestBrowserFlagUnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult BrowserFlag
|
||||
expectedErr error
|
||||
}{
|
||||
{[]byte(`{}`), BrowserFlag(false), errors.New("json: cannot unmarshal object into Go value of type string")},
|
||||
{[]byte(`["on"]`), BrowserFlag(false), errors.New("json: cannot unmarshal array into Go value of type string")},
|
||||
{[]byte(`"junk"`), BrowserFlag(false), errors.New("invalid value ‘junk’ for BrowserFlag")},
|
||||
{[]byte(`"true"`), BrowserFlag(false), errors.New("invalid value ‘true’ for BrowserFlag")},
|
||||
{[]byte(`"false"`), BrowserFlag(false), errors.New("invalid value ‘false’ for BrowserFlag")},
|
||||
{[]byte(`"ON"`), BrowserFlag(false), errors.New("invalid value ‘ON’ for BrowserFlag")},
|
||||
{[]byte(`"OFF"`), BrowserFlag(false), errors.New("invalid value ‘OFF’ for BrowserFlag")},
|
||||
{[]byte(`""`), BrowserFlag(true), nil},
|
||||
{[]byte(`"on"`), BrowserFlag(true), nil},
|
||||
{[]byte(`"off"`), BrowserFlag(false), nil},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
var flag BrowserFlag
|
||||
err := (&flag).UnmarshalJSON(testCase.data)
|
||||
if testCase.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
} else if testCase.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
|
||||
if err == nil && testCase.expectedResult != flag {
|
||||
t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test ParseBrowserFlag()
|
||||
func TestParseBrowserFlag(t *testing.T) {
|
||||
testCases := []struct {
|
||||
flagStr string
|
||||
expectedResult BrowserFlag
|
||||
expectedErr error
|
||||
}{
|
||||
{"", BrowserFlag(false), errors.New("invalid value ‘’ for BrowserFlag")},
|
||||
{"junk", BrowserFlag(false), errors.New("invalid value ‘junk’ for BrowserFlag")},
|
||||
{"true", BrowserFlag(false), errors.New("invalid value ‘true’ for BrowserFlag")},
|
||||
{"false", BrowserFlag(false), errors.New("invalid value ‘false’ for BrowserFlag")},
|
||||
{"ON", BrowserFlag(false), errors.New("invalid value ‘ON’ for BrowserFlag")},
|
||||
{"OFF", BrowserFlag(false), errors.New("invalid value ‘OFF’ for BrowserFlag")},
|
||||
{"on", BrowserFlag(true), nil},
|
||||
{"off", BrowserFlag(false), nil},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
bf, err := ParseBrowserFlag(testCase.flagStr)
|
||||
if testCase.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
} else if testCase.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
|
||||
if err == nil && testCase.expectedResult != bf {
|
||||
t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, bf)
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-28
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -17,38 +17,21 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Login handler implements JWT login token generator, which upon login request
|
||||
// along with username and password is generated.
|
||||
func (br *browserPeerAPIHandlers) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
|
||||
// Validate LoginRPCArgs
|
||||
if err := args.IsValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Authenticate using JWT.
|
||||
token, err := authenticateWeb(args.Username, args.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Return the token.
|
||||
reply.AuthToken = token
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAuthPeerArgs - Arguments collection for SetAuth RPC call
|
||||
type SetAuthPeerArgs struct {
|
||||
// For Auth
|
||||
AuthRPCArgs
|
||||
|
||||
// New credentials that receiving peer should update to.
|
||||
Creds credential
|
||||
Creds auth.Credentials
|
||||
}
|
||||
|
||||
// SetAuthPeer - Update to new credentials sent from a peer Minio
|
||||
@@ -63,16 +46,19 @@ func (br *browserPeerAPIHandlers) SetAuthPeer(args SetAuthPeerArgs, reply *AuthR
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateAuthKeys(args.Creds.AccessKey, args.Creds.SecretKey); err != nil {
|
||||
return err
|
||||
if !args.Creds.IsValid() {
|
||||
return fmt.Errorf("Invalid credential passed")
|
||||
}
|
||||
|
||||
// Update credentials in memory
|
||||
serverConfig.SetCredential(args.Creds)
|
||||
prevCred := serverConfig.SetCredential(args.Creds)
|
||||
|
||||
// Save credentials to config file
|
||||
if err := serverConfig.Save(); err != nil {
|
||||
errorIf(err, "Error updating config file with new credentials sent from browser RPC.")
|
||||
// Save the current creds when failed to update.
|
||||
serverConfig.SetCredential(prevCred)
|
||||
|
||||
errorIf(err, "Unable to update the config with new credentials sent from browser RPC.")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -80,7 +66,7 @@ func (br *browserPeerAPIHandlers) SetAuthPeer(args SetAuthPeerArgs, reply *AuthR
|
||||
}
|
||||
|
||||
// Sends SetAuthPeer RPCs to all peers in the Minio cluster
|
||||
func updateCredsOnPeers(creds credential) map[string]error {
|
||||
func updateCredsOnPeers(creds auth.Credentials) map[string]error {
|
||||
// Get list of peer addresses (from globalS3Peers)
|
||||
peers := []string{}
|
||||
for _, p := range globalS3Peers {
|
||||
@@ -111,7 +97,7 @@ func updateCredsOnPeers(creds credential) map[string]error {
|
||||
secretKey: serverCred.SecretKey,
|
||||
serverAddr: peers[ix],
|
||||
secureConn: globalIsSSL,
|
||||
serviceEndpoint: path.Join(reservedBucket, browserPeerPath),
|
||||
serviceEndpoint: path.Join(minioReservedBucketPath, browserPeerPath),
|
||||
serviceName: "BrowserPeer",
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -19,7 +19,8 @@ package cmd
|
||||
import (
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// API suite container common to both FS and XL.
|
||||
@@ -30,21 +31,20 @@ type TestRPCBrowserPeerSuite struct {
|
||||
}
|
||||
|
||||
// Setting up the test suite and starting the Test server.
|
||||
func (s *TestRPCBrowserPeerSuite) SetUpSuite(c *testing.T) {
|
||||
s.testServer = StartTestBrowserPeerRPCServer(c, s.serverType)
|
||||
func (s *TestRPCBrowserPeerSuite) SetUpSuite(t *testing.T) {
|
||||
s.testServer = StartTestBrowserPeerRPCServer(t, s.serverType)
|
||||
s.testAuthConf = authConfig{
|
||||
serverAddr: s.testServer.Server.Listener.Addr().String(),
|
||||
accessKey: s.testServer.AccessKey,
|
||||
secretKey: s.testServer.SecretKey,
|
||||
serviceEndpoint: path.Join(reservedBucket, browserPeerPath),
|
||||
serviceEndpoint: path.Join(minioReservedBucketPath, browserPeerPath),
|
||||
serviceName: "BrowserPeer",
|
||||
}
|
||||
}
|
||||
|
||||
// No longer used with gocheck, but used in explicit teardown code in
|
||||
// each test function. // Called implicitly by "gopkg.in/check.v1"
|
||||
// after all tests are run.
|
||||
func (s *TestRPCBrowserPeerSuite) TearDownSuite(c *testing.T) {
|
||||
// TeatDownSuite - called implicitly by after all tests are run in
|
||||
// browser peer rpc suite.
|
||||
func (s *TestRPCBrowserPeerSuite) TearDownSuite(t *testing.T) {
|
||||
s.testServer.Stop()
|
||||
}
|
||||
|
||||
@@ -63,9 +63,9 @@ func TestBrowserPeerRPC(t *testing.T) {
|
||||
// Tests for browser peer rpc.
|
||||
func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
|
||||
// Construct RPC call arguments.
|
||||
creds := credential{
|
||||
AccessKey: "abcd1",
|
||||
SecretKey: "abcd1234",
|
||||
creds, err := auth.CreateCredentials("abcd1", "abcd1234")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create credential. %v", err)
|
||||
}
|
||||
|
||||
// Validate for invalid token.
|
||||
@@ -73,7 +73,7 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
|
||||
args.AuthToken = "garbage"
|
||||
rclient := newRPCClient(s.testAuthConf.serverAddr, s.testAuthConf.serviceEndpoint, false)
|
||||
defer rclient.Close()
|
||||
err := rclient.Call("BrowserPeer.SetAuthPeer", &args, &AuthRPCReply{})
|
||||
err = rclient.Call("BrowserPeer.SetAuthPeer", &args, &AuthRPCReply{})
|
||||
if err != nil {
|
||||
if err.Error() != errInvalidToken.Error() {
|
||||
t.Fatal(err)
|
||||
@@ -92,11 +92,14 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
|
||||
// Validate for failure in login handler with previous credentials.
|
||||
rclient = newRPCClient(s.testAuthConf.serverAddr, s.testAuthConf.serviceEndpoint, false)
|
||||
defer rclient.Close()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rargs := &LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: time.Now().UTC(),
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
rreply := &LoginRPCReply{}
|
||||
err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply)
|
||||
@@ -106,20 +109,18 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate for success in loing handled with valid credetnails.
|
||||
rargs = &LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
RequestTime: time.Now().UTC(),
|
||||
}
|
||||
rreply = &LoginRPCReply{}
|
||||
err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply)
|
||||
token, err = authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Validate all the replied fields after successful login.
|
||||
if rreply.AuthToken == "" {
|
||||
t.Fatalf("Generated token cannot be empty %s", errInvalidToken)
|
||||
// Validate for success in loing handled with valid credetnails.
|
||||
rargs = &LoginRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
rreply = &LoginRPCReply{}
|
||||
if err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/rpc"
|
||||
|
||||
router "github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@@ -37,15 +35,15 @@ type browserPeerAPIHandlers struct {
|
||||
|
||||
// Register RPC router
|
||||
func registerBrowserPeerRPCRouter(mux *router.Router) error {
|
||||
bpHandlers := &browserPeerAPIHandlers{}
|
||||
bpHandlers := &browserPeerAPIHandlers{AuthRPCServer{}}
|
||||
|
||||
bpRPCServer := rpc.NewServer()
|
||||
bpRPCServer := newRPCServer()
|
||||
err := bpRPCServer.RegisterName("BrowserPeer", bpHandlers)
|
||||
if err != nil {
|
||||
return traceError(err)
|
||||
}
|
||||
|
||||
bpRouter := mux.NewRoute().PathPrefix(reservedBucket).Subrouter()
|
||||
bpRouter := mux.NewRoute().PathPrefix(minioReservedBucketPath).Subrouter()
|
||||
bpRouter.Path(browserPeerPath).Handler(bpRPCServer)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -51,6 +51,26 @@ func validateListObjectsArgs(prefix, marker, delimiter string, maxKeys int) APIE
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Validate all the ListObjectsV2 query arguments, returns an APIErrorCode
|
||||
// if one of the args do not meet the required conditions.
|
||||
// Special conditions required by Minio server are as below
|
||||
// - delimiter if set should be equal to '/', otherwise the request is rejected.
|
||||
func validateGatewayListObjectsV2Args(prefix, marker, delimiter string, maxKeys int) APIErrorCode {
|
||||
// Max keys cannot be negative.
|
||||
if maxKeys < 0 {
|
||||
return ErrInvalidMaxKeys
|
||||
}
|
||||
|
||||
/// Minio special conditions for ListObjects.
|
||||
|
||||
// Verify if delimiter is anything other than '/', which we do not support.
|
||||
if delimiter != "" && delimiter != "/" {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// ListObjectsV2Handler - GET Bucket (List Objects) Version 2.
|
||||
// --------------------------
|
||||
// This implementation of the GET operation returns some or all (up to 1000)
|
||||
@@ -100,7 +120,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
|
||||
response := generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter, fetchOwner, maxKeys, listObjectsInfo)
|
||||
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsInfo.NextMarker, startAfter, delimiter, fetchOwner, listObjectsInfo.IsTruncated, maxKeys, listObjectsInfo.Objects, listObjectsInfo.Prefixes)
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, encodeResponse(response))
|
||||
|
||||
+180
-83
@@ -19,21 +19,25 @@ package cmd
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
mux "github.com/gorilla/mux"
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
// 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, action, resource, referer string, queryParams url.Values) (s3Error APIErrorCode) {
|
||||
func enforceBucketPolicy(bucket, action, resource, referer, sourceIP string, queryParams url.Values) (s3Error APIErrorCode) {
|
||||
// Verify if bucket actually exists
|
||||
if err := checkBucketExist(bucket, newObjectLayerFn()); err != nil {
|
||||
err = errorCause(err)
|
||||
@@ -50,9 +54,13 @@ func enforceBucketPolicy(bucket, action, resource, referer string, queryParams u
|
||||
return ErrInternalError
|
||||
}
|
||||
|
||||
if globalBucketPolicies == nil {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
// Fetch bucket policy, if policy is not set return access denied.
|
||||
policy := globalBucketPolicies.GetBucketPolicy(bucket)
|
||||
if policy == nil {
|
||||
p := globalBucketPolicies.GetBucketPolicy(bucket)
|
||||
if reflect.DeepEqual(p, emptyBucketPolicy) {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
@@ -60,7 +68,7 @@ func enforceBucketPolicy(bucket, action, resource, referer string, queryParams u
|
||||
arn := bucketARNPrefix + strings.TrimSuffix(strings.TrimPrefix(resource, "/"), "/")
|
||||
|
||||
// Get conditions for policy verification.
|
||||
conditionKeyMap := make(map[string]set.StringSet)
|
||||
conditionKeyMap := make(policy.ConditionKeyMap)
|
||||
for queryParam := range queryParams {
|
||||
conditionKeyMap[queryParam] = set.CreateStringSet(queryParams.Get(queryParam))
|
||||
}
|
||||
@@ -69,9 +77,11 @@ func enforceBucketPolicy(bucket, action, resource, referer string, queryParams u
|
||||
if referer != "" {
|
||||
conditionKeyMap["referer"] = set.CreateStringSet(referer)
|
||||
}
|
||||
// Add request source Ip to conditionKeyMap.
|
||||
conditionKeyMap["ip"] = set.CreateStringSet(sourceIP)
|
||||
|
||||
// Validate action, resource and conditions with current policy statements.
|
||||
if !bucketPolicyEvalStatements(action, arn, conditionKeyMap, policy.Statements) {
|
||||
if !bucketPolicyEvalStatements(action, arn, conditionKeyMap, p.Statements) {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
return ErrNone
|
||||
@@ -79,17 +89,18 @@ func enforceBucketPolicy(bucket, action, resource, referer string, queryParams u
|
||||
|
||||
// Check if the action is allowed on the bucket/prefix.
|
||||
func isBucketActionAllowed(action, bucket, prefix string) bool {
|
||||
policy := globalBucketPolicies.GetBucketPolicy(bucket)
|
||||
if policy == nil {
|
||||
if globalBucketPolicies == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
bp := globalBucketPolicies.GetBucketPolicy(bucket)
|
||||
if reflect.DeepEqual(bp, emptyBucketPolicy) {
|
||||
return false
|
||||
}
|
||||
resource := bucketARNPrefix + path.Join(bucket, prefix)
|
||||
var conditionKeyMap map[string]set.StringSet
|
||||
// Validate action, resource and conditions with current policy statements.
|
||||
if !bucketPolicyEvalStatements(action, resource, conditionKeyMap, policy.Statements) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return bucketPolicyEvalStatements(action, resource, conditionKeyMap, bp.Statements)
|
||||
}
|
||||
|
||||
// GetBucketLocationHandler - GET Bucket location.
|
||||
@@ -233,9 +244,14 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(r, bucket, "s3:DeleteObject", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
var authError APIErrorCode
|
||||
if authError = checkRequestAuthType(r, bucket, "s3:DeleteObject", serverConfig.GetRegion()); authError != ErrNone {
|
||||
// In the event access is denied, a 200 response should still be returned
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||
if authError != ErrAccessDenied {
|
||||
writeErrorResponse(w, authError, r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Content-Length is required and should be non-zero
|
||||
@@ -278,9 +294,25 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
wg.Add(1)
|
||||
go func(i int, obj ObjectIdentifier) {
|
||||
defer wg.Done()
|
||||
dErr := objectAPI.DeleteObject(bucket, obj.ObjectName)
|
||||
if dErr != nil {
|
||||
dErrs[i] = dErr
|
||||
// If the request is denied access, each item
|
||||
// should be marked as 'AccessDenied'
|
||||
if authError == ErrAccessDenied {
|
||||
dErrs[i] = PrefixAccessDenied{
|
||||
Bucket: bucket,
|
||||
Object: obj.ObjectName,
|
||||
}
|
||||
return
|
||||
}
|
||||
objectLock := globalNSMutex.NewNSLock(bucket, obj.ObjectName)
|
||||
if timedOutErr := objectLock.GetLock(globalObjectTimeout); timedOutErr != nil {
|
||||
dErrs[i] = timedOutErr
|
||||
} else {
|
||||
defer objectLock.Unlock()
|
||||
|
||||
dErr := objectAPI.DeleteObject(bucket, obj.ObjectName)
|
||||
if dErr != nil {
|
||||
dErrs[i] = dErr
|
||||
}
|
||||
}
|
||||
}(index, object)
|
||||
}
|
||||
@@ -318,6 +350,13 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, encodedSuccessResponse)
|
||||
|
||||
// Get host and port from Request.RemoteAddr failing which
|
||||
// fill them with empty strings.
|
||||
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
host, port = "", ""
|
||||
}
|
||||
|
||||
// Notify deleted event for objects.
|
||||
for _, dobj := range deletedObjects {
|
||||
eventNotify(eventData{
|
||||
@@ -326,9 +365,10 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
ObjInfo: ObjectInfo{
|
||||
Name: dobj.ObjectName,
|
||||
},
|
||||
ReqParams: map[string]string{
|
||||
"sourceIPAddress": r.RemoteAddr,
|
||||
},
|
||||
ReqParams: extractReqParams(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
Host: host,
|
||||
Port: port,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -344,11 +384,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
// PutBucket does not have any bucket action.
|
||||
s3Error := checkRequestAuthType(r, "", "", globalMinioDefaultRegion)
|
||||
if s3Error == ErrInvalidRegion {
|
||||
// Clients like boto3 send putBucket() call signed with region that is configured.
|
||||
s3Error = checkRequestAuthType(r, "", "", serverConfig.GetRegion())
|
||||
}
|
||||
s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion())
|
||||
if s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
@@ -357,19 +393,29 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
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 {
|
||||
// Parse incoming location constraint.
|
||||
location, s3Error := parseLocationConstraint(r)
|
||||
if s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate if location sent by the client is valid, reject
|
||||
// requests which do not follow valid region requirements.
|
||||
if !isValidLocation(location) {
|
||||
writeErrorResponse(w, ErrInvalidRegion, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.Lock()
|
||||
if bucketLock.GetLock(globalObjectTimeout) != nil {
|
||||
writeErrorResponse(w, ErrOperationTimedOut, r.URL)
|
||||
return
|
||||
}
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
// Proceed to creating a bucket.
|
||||
err := objectAPI.MakeBucket(bucket)
|
||||
err := objectAPI.MakeBucketWithLocation(bucket, "")
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to create a bucket.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
@@ -393,12 +439,24 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
bucket := mux.Vars(r)["bucket"]
|
||||
|
||||
// Require Content-Length to be set in the request
|
||||
size := r.ContentLength
|
||||
if size < 0 {
|
||||
writeErrorResponse(w, ErrMissingContentLength, r.URL)
|
||||
return
|
||||
}
|
||||
resource, err := getResource(r.URL.Path, r.Host, globalDomainName)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, ErrInvalidRequest, r.URL)
|
||||
return
|
||||
}
|
||||
// Make sure that the URL does not contain object name.
|
||||
if bucket != filepath.Clean(resource[1:]) {
|
||||
writeErrorResponse(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Here the parameter is the size of the form data that should
|
||||
// be loaded in memory, the remaining being put in temporary files.
|
||||
@@ -437,15 +495,25 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
// Close multipart file
|
||||
defer fileBody.Close()
|
||||
|
||||
bucket := mux.Vars(r)["bucket"]
|
||||
formValues["Bucket"] = bucket
|
||||
formValues.Set("Bucket", bucket)
|
||||
|
||||
if fileName != "" && strings.Contains(formValues["Key"], "${filename}") {
|
||||
if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") {
|
||||
// S3 feature to replace ${filename} found in Key form field
|
||||
// by the filename attribute passed in multipart
|
||||
formValues["Key"] = strings.Replace(formValues["Key"], "${filename}", fileName, -1)
|
||||
formValues.Set("Key", strings.Replace(formValues.Get("Key"), "${filename}", fileName, -1))
|
||||
}
|
||||
object := formValues.Get("Key")
|
||||
|
||||
successRedirect := formValues.Get("success_action_redirect")
|
||||
successStatus := formValues.Get("success_action_status")
|
||||
var redirectURL *url.URL
|
||||
if successRedirect != "" {
|
||||
redirectURL, err = url.Parse(successRedirect)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
object := formValues["Key"]
|
||||
|
||||
// Verify policy signature.
|
||||
apiErr := doesPolicySignatureMatch(formValues)
|
||||
@@ -454,7 +522,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
policyBytes, err := base64.StdEncoding.DecodeString(formValues["Policy"])
|
||||
policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy"))
|
||||
if err != nil {
|
||||
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL)
|
||||
return
|
||||
@@ -482,7 +550,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
if fileSize > lengthRange.Max || fileSize > maxObjectSize {
|
||||
if fileSize > lengthRange.Max || isMaxObjectSize(fileSize) {
|
||||
errorIf(err, "Unable to create object.")
|
||||
writeErrorResponse(w, toAPIErrorCode(errDataTooLarge), r.URL)
|
||||
return
|
||||
@@ -490,64 +558,78 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
}
|
||||
|
||||
// Extract metadata to be saved from received Form.
|
||||
metadata := extractMetadataFromForm(formValues)
|
||||
|
||||
sha256sum := ""
|
||||
metadata, err := extractMetadataFromHeader(formValues)
|
||||
if err != nil {
|
||||
errorIf(err, "found invalid http request header")
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
objectLock := globalNSMutex.NewNSLock(bucket, object)
|
||||
objectLock.Lock()
|
||||
if objectLock.GetLock(globalObjectTimeout) != nil {
|
||||
writeErrorResponse(w, ErrOperationTimedOut, r.URL)
|
||||
return
|
||||
}
|
||||
defer objectLock.Unlock()
|
||||
|
||||
objInfo, err := objectAPI.PutObject(bucket, object, fileSize, fileBody, metadata, sha256sum)
|
||||
hashReader, err := hash.NewReader(fileBody, fileSize, "", "")
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to initialize hashReader.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
objInfo, err := objectAPI.PutObject(bucket, object, hashReader, metadata)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to create object.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
w.Header().Set("ETag", "\""+objInfo.MD5Sum+"\"")
|
||||
w.Header().Set("Location", getObjectLocation(bucket, object))
|
||||
|
||||
successRedirect := formValues[http.CanonicalHeaderKey("success_action_redirect")]
|
||||
successStatus := formValues[http.CanonicalHeaderKey("success_action_status")]
|
||||
port := r.Header.Get("X-Forward-Proto")
|
||||
location := getObjectLocation(r.Host, port, bucket, object)
|
||||
w.Header().Set("ETag", `"`+objInfo.ETag+`"`)
|
||||
w.Header().Set("Location", location)
|
||||
|
||||
if successStatus == "" && successRedirect == "" {
|
||||
writeSuccessNoContent(w)
|
||||
} else {
|
||||
if successRedirect != "" {
|
||||
redirectURL := successRedirect + "?" + fmt.Sprintf("bucket=%s&key=%s&etag=%s",
|
||||
bucket,
|
||||
getURLEncodedName(object),
|
||||
getURLEncodedName("\""+objInfo.MD5Sum+"\""))
|
||||
|
||||
writeRedirectSeeOther(w, redirectURL)
|
||||
} else {
|
||||
// Decide what http response to send depending on success_action_status parameter
|
||||
switch successStatus {
|
||||
case "201":
|
||||
resp := encodeResponse(PostResponse{
|
||||
Bucket: bucket,
|
||||
Key: object,
|
||||
ETag: "\"" + objInfo.MD5Sum + "\"",
|
||||
Location: getObjectLocation(bucket, object),
|
||||
})
|
||||
writeResponse(w, http.StatusCreated, resp, "application/xml")
|
||||
case "200":
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
default:
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
}
|
||||
// Get host and port from Request.RemoteAddr.
|
||||
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
host, port = "", ""
|
||||
}
|
||||
|
||||
// Notify object created event.
|
||||
eventNotify(eventData{
|
||||
Type: ObjectCreatedPost,
|
||||
Bucket: bucket,
|
||||
ObjInfo: objInfo,
|
||||
ReqParams: map[string]string{
|
||||
"sourceIPAddress": r.RemoteAddr,
|
||||
},
|
||||
defer eventNotify(eventData{
|
||||
Type: ObjectCreatedPost,
|
||||
Bucket: objInfo.Bucket,
|
||||
ObjInfo: objInfo,
|
||||
ReqParams: extractReqParams(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
Host: host,
|
||||
Port: port,
|
||||
})
|
||||
|
||||
if successRedirect != "" {
|
||||
// Replace raw query params..
|
||||
redirectURL.RawQuery = getRedirectPostRawQuery(objInfo)
|
||||
writeRedirectSeeOther(w, redirectURL.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Decide what http response to send depending on success_action_status parameter
|
||||
switch successStatus {
|
||||
case "201":
|
||||
resp := encodeResponse(PostResponse{
|
||||
Bucket: objInfo.Bucket,
|
||||
Key: objInfo.Name,
|
||||
ETag: `"` + objInfo.ETag + `"`,
|
||||
Location: location,
|
||||
})
|
||||
writeResponse(w, http.StatusCreated, resp, "application/xml")
|
||||
case "200":
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
default:
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
}
|
||||
|
||||
// HeadBucketHandler - HEAD Bucket
|
||||
@@ -572,7 +654,10 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.RLock()
|
||||
if bucketLock.GetRLock(globalObjectTimeout) != nil {
|
||||
writeErrorResponseHeadersOnly(w, ErrOperationTimedOut)
|
||||
return
|
||||
}
|
||||
defer bucketLock.RUnlock()
|
||||
|
||||
if _, err := objectAPI.GetBucketInfo(bucket); err != nil {
|
||||
@@ -602,7 +687,10 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
bucket := vars["bucket"]
|
||||
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.Lock()
|
||||
if bucketLock.GetLock(globalObjectTimeout) != nil {
|
||||
writeErrorResponse(w, ErrOperationTimedOut, r.URL)
|
||||
return
|
||||
}
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
// Attempt to delete bucket.
|
||||
@@ -615,12 +703,21 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
// Delete bucket access policy, if present - ignore any errors.
|
||||
_ = removeBucketPolicy(bucket, objectAPI)
|
||||
|
||||
// Notify all peers (including self) to update in-memory state
|
||||
S3PeersUpdateBucketPolicy(bucket, policyChange{true, policy.BucketAccessPolicy{}})
|
||||
|
||||
// Delete notification config, if present - ignore any errors.
|
||||
_ = removeNotificationConfig(bucket, objectAPI)
|
||||
|
||||
// Notify all peers (including self) to update in-memory state
|
||||
S3PeersUpdateBucketNotification(bucket, nil)
|
||||
|
||||
// Delete listener config, if present - ignore any errors.
|
||||
_ = removeListenerConfig(bucket, objectAPI)
|
||||
|
||||
// Notify all peers (including self) to update in-memory state
|
||||
S3PeersUpdateBucketListener(bucket, []listenerConfig{})
|
||||
|
||||
// Write success response.
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
+77
-14
@@ -24,6 +24,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Wrapper for calling GetBucketPolicy HTTP handler tests for both XL multiple disks and single node setup.
|
||||
@@ -32,7 +34,7 @@ func TestGetBucketLocationHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// test cases with sample input and expected output.
|
||||
@@ -68,7 +70,7 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
locationResponse: []byte(""),
|
||||
errorResponse: APIErrorResponse{
|
||||
Resource: "/" + bucketName + "/",
|
||||
Code: "InvalidAccessKeyID",
|
||||
Code: "InvalidAccessKeyId",
|
||||
Message: "The access key ID you provided does not exist in our records.",
|
||||
},
|
||||
shouldPass: false,
|
||||
@@ -177,7 +179,7 @@ func TestHeadBucketHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// test cases with sample input and expected output.
|
||||
@@ -284,7 +286,7 @@ func TestListMultipartUploadsHandler(t *testing.T) {
|
||||
|
||||
// testListMultipartUploadsHandler - Tests validate listing of multipart uploads.
|
||||
func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// Collection of non-exhaustive ListMultipartUploads test cases, valid errors
|
||||
@@ -455,7 +457,6 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
|
||||
|
||||
// verify response for V2 signed HTTP request.
|
||||
reqV2, err := newTestSignedRequestV2("GET", u, 0, nil, testCase.accessKey, testCase.secretKey)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||
}
|
||||
@@ -523,7 +524,7 @@ func TestListBucketsHandler(t *testing.T) {
|
||||
|
||||
// testListBucketsHandler - Tests validate listing of buckets.
|
||||
func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
@@ -616,7 +617,7 @@ func TestAPIDeleteMultipleObjectsHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
var err error
|
||||
@@ -632,8 +633,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
for i := 0; i < 10; i++ {
|
||||
objectName := "test-object-" + strconv.Itoa(i)
|
||||
// uploading the object.
|
||||
_, err = obj.PutObject(bucketName, objectName, int64(len(contentBytes)), bytes.NewBuffer(contentBytes),
|
||||
make(map[string]string), sha256sum)
|
||||
_, err = obj.PutObject(bucketName, objectName, mustGetHashReader(t, bytes.NewBuffer(contentBytes), int64(len(contentBytes)), "", sha256sum), nil)
|
||||
// if object upload fails stop the test.
|
||||
if err != nil {
|
||||
t.Fatalf("Put Object %d: Error uploading object: <ERROR> %v", i, err)
|
||||
@@ -650,6 +650,17 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
|
||||
return objectIdentifierList
|
||||
}
|
||||
getDeleteErrorList := func(objects []ObjectIdentifier) (deleteErrorList []DeleteError) {
|
||||
for _, obj := range objects {
|
||||
deleteErrorList = append(deleteErrorList, DeleteError{
|
||||
Code: errorCodeResponse[ErrAccessDenied].Code,
|
||||
Message: errorCodeResponse[ErrAccessDenied].Description,
|
||||
Key: obj.ObjectName,
|
||||
})
|
||||
}
|
||||
|
||||
return deleteErrorList
|
||||
}
|
||||
|
||||
requestList := []DeleteObjectsRequest{
|
||||
{Quiet: false, Objects: getObjectIdentifierList(objectNames[:5])},
|
||||
@@ -670,6 +681,10 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
errorResponse := generateMultiDeleteResponse(requestList[1].Quiet, requestList[1].Objects, nil)
|
||||
encodedErrorResponse := encodeResponse(errorResponse)
|
||||
|
||||
anonRequest := encodeResponse(requestList[0])
|
||||
anonResponse := generateMultiDeleteResponse(requestList[0].Quiet, nil, getDeleteErrorList(requestList[0].Objects))
|
||||
encodedAnonResponse := encodeResponse(anonResponse)
|
||||
|
||||
testCases := []struct {
|
||||
bucket string
|
||||
objects []byte
|
||||
@@ -718,15 +733,32 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
expectedContent: encodedErrorResponse,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 5.
|
||||
// Anonymous user access denied response
|
||||
// Currently anonymous users cannot delete multiple objects in Minio server
|
||||
{
|
||||
bucket: bucketName,
|
||||
objects: anonRequest,
|
||||
accessKey: "",
|
||||
secretKey: "",
|
||||
expectedContent: encodedAnonResponse,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var req *http.Request
|
||||
var actualContent []byte
|
||||
|
||||
// Indicating that all parts are uploaded and initiating completeMultipartUpload.
|
||||
req, err = newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", bucketName),
|
||||
int64(len(testCase.objects)), bytes.NewReader(testCase.objects), testCase.accessKey, testCase.secretKey)
|
||||
// Generate a signed or anonymous request based on the testCase
|
||||
if testCase.accessKey != "" {
|
||||
req, err = newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", bucketName),
|
||||
int64(len(testCase.objects)), bytes.NewReader(testCase.objects), testCase.accessKey, testCase.secretKey)
|
||||
} else {
|
||||
req, err = newTestRequest("POST", getDeleteMultipleObjectsURL("", bucketName),
|
||||
int64(len(testCase.objects)), bytes.NewReader(testCase.objects))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request for DeleteMultipleObjects: <ERROR> %v", err)
|
||||
}
|
||||
@@ -753,8 +785,6 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
}
|
||||
}
|
||||
|
||||
// Currently anonymous user cannot delete multiple objects in Minio server, hence no test case is required.
|
||||
|
||||
// HTTP request to test the case of `objectLayer` being set to `nil`.
|
||||
// There is no need to use an existing bucket or valid input for creating the request,
|
||||
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
|
||||
@@ -771,3 +801,36 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
// `ExecObjectLayerAPINilTest` manages the operation.
|
||||
ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq)
|
||||
}
|
||||
|
||||
func TestIsBucketActionAllowed(t *testing.T) {
|
||||
ExecObjectLayerAPITest(t, testIsBucketActionAllowedHandler, []string{"BucketLocation"})
|
||||
}
|
||||
|
||||
func testIsBucketActionAllowedHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
// input.
|
||||
action string
|
||||
bucket string
|
||||
prefix string
|
||||
isGlobalPoliciesNil bool
|
||||
// flag indicating whether the test should pass.
|
||||
shouldPass bool
|
||||
}{
|
||||
{"s3:GetBucketLocation", "mybucket", "abc", true, false},
|
||||
{"s3:ListObject", "mybucket", "abc", false, false},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
if testCase.isGlobalPoliciesNil {
|
||||
globalBucketPolicies = nil
|
||||
} else {
|
||||
initBucketPolicies(obj)
|
||||
}
|
||||
isAllowed := isBucketActionAllowed(testCase.action, testCase.bucket, testCase.prefix)
|
||||
if isAllowed != testCase.shouldPass {
|
||||
t.Errorf("Case %d: Expected the response status to be `%t`, but instead found `%t`", i+1, testCase.shouldPass, isAllowed)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ type notificationConfig struct {
|
||||
XMLName xml.Name `xml:"NotificationConfiguration"`
|
||||
QueueConfigs []queueConfig `xml:"QueueConfiguration"`
|
||||
LambdaConfigs []lambdaConfig `xml:"CloudFunctionConfiguration"`
|
||||
TopicConfigs []topicConfig `xml:"TopicConfiguration"`
|
||||
}
|
||||
|
||||
// listenerConfig structure represents run-time notification
|
||||
@@ -94,6 +95,10 @@ const (
|
||||
ObjectCreatedCompleteMultipartUpload
|
||||
// ObjectRemovedDelete is s3:ObjectRemoved:Delete
|
||||
ObjectRemovedDelete
|
||||
// ObjectAccessedGet is s3:ObjectAccessed:Get
|
||||
ObjectAccessedGet
|
||||
// ObjectAccessedHead is s3:ObjectAccessed:Head
|
||||
ObjectAccessedHead
|
||||
)
|
||||
|
||||
// Stringer interface for event name.
|
||||
@@ -109,6 +114,10 @@ func (eventName EventName) String() string {
|
||||
return "s3:ObjectCreated:CompleteMultipartUpload"
|
||||
case ObjectRemovedDelete:
|
||||
return "s3:ObjectRemoved:Delete"
|
||||
case ObjectAccessedGet:
|
||||
return "s3:ObjectAccessed:Get"
|
||||
case ObjectAccessedHead:
|
||||
return "s3:ObjectAccessed:Head"
|
||||
default:
|
||||
return "s3:Unknown"
|
||||
}
|
||||
@@ -128,11 +137,13 @@ type bucketMeta struct {
|
||||
|
||||
// Notification event object metadata.
|
||||
type objectMeta struct {
|
||||
Key string `json:"key"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
ETag string `json:"eTag,omitempty"`
|
||||
VersionID string `json:"versionId,omitempty"`
|
||||
Sequencer string `json:"sequencer"`
|
||||
Key string `json:"key"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
ETag string `json:"eTag,omitempty"`
|
||||
ContentType string `json:"contentType,omitempty"`
|
||||
UserMetadata map[string]string `json:"userMetadata,omitempty"`
|
||||
VersionID string `json:"versionId,omitempty"`
|
||||
Sequencer string `json:"sequencer"`
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -168,6 +179,14 @@ const (
|
||||
eventVersion = "2.0"
|
||||
)
|
||||
|
||||
// sourceInfo represents information on the client that triggered the
|
||||
// event notification.
|
||||
type sourceInfo struct {
|
||||
Host string `json:"host"`
|
||||
Port string `json:"port"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
}
|
||||
|
||||
// NotificationEvent represents an Amazon an S3 bucket notification event.
|
||||
type NotificationEvent struct {
|
||||
EventVersion string `json:"eventVersion"`
|
||||
@@ -179,6 +198,7 @@ type NotificationEvent struct {
|
||||
RequestParameters map[string]string `json:"requestParameters"`
|
||||
ResponseElements map[string]string `json:"responseElements"`
|
||||
S3 eventMeta `json:"s3"`
|
||||
Source sourceInfo `json:"source"`
|
||||
}
|
||||
|
||||
// Represents the minio sqs type and account id's.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
* 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.
|
||||
@@ -22,7 +22,9 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -63,13 +65,13 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
||||
|
||||
// Attempt to successfully load notification config.
|
||||
nConfig, err := loadNotificationConfig(bucket, objAPI)
|
||||
if err != nil && err != errNoSuchNotifications {
|
||||
if err != nil && errorCause(err) != errNoSuchNotifications {
|
||||
errorIf(err, "Unable to read notification configuration.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
// For no notifications we write a dummy XML.
|
||||
if err == errNoSuchNotifications {
|
||||
if errorCause(err) == errNoSuchNotifications {
|
||||
// Complies with the s3 behavior in this regard.
|
||||
nConfig = ¬ificationConfig{}
|
||||
}
|
||||
@@ -150,6 +152,12 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
// Convert the incoming ARNs properly to the GetRegion().
|
||||
for i, queueConfig := range notificationCfg.QueueConfigs {
|
||||
queueConfig.QueueARN = unmarshalSqsARN(queueConfig.QueueARN).String()
|
||||
notificationCfg.QueueConfigs[i] = queueConfig
|
||||
}
|
||||
|
||||
// Put bucket notification config.
|
||||
err = PutBucketNotificationConfig(bucket, ¬ificationCfg, objectAPI)
|
||||
if err != nil {
|
||||
@@ -173,7 +181,9 @@ func PutBucketNotificationConfig(bucket string, ncfg *notificationConfig, objAPI
|
||||
// Acquire a write lock on bucket before modifying its
|
||||
// configuration.
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.Lock()
|
||||
if err := bucketLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
// Release lock after notifying peers
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
@@ -204,39 +214,73 @@ func writeNotification(w http.ResponseWriter, notification map[string][]Notifica
|
||||
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
|
||||
return err
|
||||
}
|
||||
|
||||
// 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.
|
||||
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.
|
||||
// listenChan A `listenChan` provides a data channel to send event
|
||||
// notifications on and `doneCh` to signal that events are no longer
|
||||
// being received. It also sends empty events (whitespace) to keep the
|
||||
// underlying connection alive.
|
||||
type listenChan struct {
|
||||
doneCh chan struct{}
|
||||
dataCh chan []NotificationEvent
|
||||
}
|
||||
|
||||
// newListenChan returns a listenChan with properly initialized
|
||||
// unbuffered channels.
|
||||
func newListenChan() *listenChan {
|
||||
return &listenChan{
|
||||
doneCh: make(chan struct{}),
|
||||
dataCh: make(chan []NotificationEvent),
|
||||
}
|
||||
}
|
||||
|
||||
// sendNotificationEvent sends notification events on the data channel
|
||||
// unless doneCh is not closed
|
||||
func (l *listenChan) sendNotificationEvent(events []NotificationEvent) {
|
||||
select {
|
||||
// Returns immediately if receiver has quit.
|
||||
case <-l.doneCh:
|
||||
// Blocks until receiver is available.
|
||||
case l.dataCh <- events:
|
||||
}
|
||||
}
|
||||
|
||||
// waitForListener writes event notification OR whitespaces on
|
||||
// ResponseWriter until client closes connection
|
||||
func (l *listenChan) waitForListener(w http.ResponseWriter) {
|
||||
|
||||
// Logs errors other than EPIPE and ECONNRESET.
|
||||
// EPIPE and ECONNRESET indicate that the client stopped
|
||||
// listening to notification events.
|
||||
logClientError := func(err error, msg string) {
|
||||
if oe, ok := err.(*net.OpError); ok && (oe.Err == syscall.EPIPE || oe.Err ==
|
||||
syscall.ECONNRESET) {
|
||||
errorIf(err, msg)
|
||||
}
|
||||
}
|
||||
|
||||
emptyEvent := map[string][]NotificationEvent{"Records": nil}
|
||||
defer close(l.doneCh)
|
||||
for {
|
||||
select {
|
||||
case events := <-arnListenerCh:
|
||||
case events := <-l.dataCh:
|
||||
if err := writeNotification(w, map[string][]NotificationEvent{"Records": events}); err != nil {
|
||||
errorIf(err, "Unable to write notification to client.")
|
||||
logClientError(err, "Unable to write notification")
|
||||
return
|
||||
}
|
||||
case <-time.After(globalSNSConnAlive): // Wait for global conn active seconds.
|
||||
if err := writeNotification(w, dummyEvents); err != nil {
|
||||
// FIXME - do not log for all errors.
|
||||
errorIf(err, "Unable to write notification to client.")
|
||||
case <-time.After(globalSNSConnAlive):
|
||||
if err := writeNotification(w, emptyEvent); err != nil {
|
||||
logClientError(err, "Unable to write empty notification")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -288,15 +332,17 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
return
|
||||
}
|
||||
|
||||
accountID := fmt.Sprintf("%d", time.Now().UTC().UnixNano())
|
||||
targetServer := GetLocalPeer(globalEndpoints)
|
||||
accountID := fmt.Sprintf("%d", UTCNow().UnixNano())
|
||||
accountARN := fmt.Sprintf(
|
||||
"%s:%s:%s:%s-%s",
|
||||
minioTopic,
|
||||
serverConfig.GetRegion(),
|
||||
accountID,
|
||||
snsTypeMinio,
|
||||
globalMinioAddr,
|
||||
targetServer,
|
||||
)
|
||||
|
||||
var filterRules []filterRule
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
@@ -330,12 +376,11 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
},
|
||||
}
|
||||
|
||||
// Setup a listening channel that will receive notifications
|
||||
// from the RPC handler.
|
||||
nEventCh := make(chan []NotificationEvent)
|
||||
defer close(nEventCh)
|
||||
// Setup a listen channel to receive notifications like
|
||||
// s3:ObjectCreated, s3:ObjectDeleted etc.
|
||||
nListenCh := newListenChan()
|
||||
// Add channel for listener events
|
||||
if err = globalEventNotifier.AddListenerChan(accountARN, nEventCh); err != nil {
|
||||
if err = globalEventNotifier.AddListenerChan(accountARN, nListenCh); err != nil {
|
||||
errorIf(err, "Error adding a listener!")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -345,11 +390,11 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
defer globalEventNotifier.RemoveListenerChan(accountARN)
|
||||
|
||||
// Update topic config to bucket config and persist - as soon
|
||||
// as this call compelets, events may start appearing in
|
||||
// nEventCh
|
||||
// as this call completes, events may start appearing in
|
||||
// nListenCh
|
||||
lc := listenerConfig{
|
||||
TopicConfig: *topicCfg,
|
||||
TargetServer: globalMinioAddr,
|
||||
TargetServer: targetServer,
|
||||
}
|
||||
|
||||
err = AddBucketListenerConfig(bucket, &lc, objAPI)
|
||||
@@ -362,8 +407,16 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
// Add all common headers.
|
||||
setCommonHeaders(w)
|
||||
|
||||
// Start sending bucket notifications.
|
||||
sendBucketNotification(w, nEventCh)
|
||||
// https://github.com/containous/traefik/issues/560
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
|
||||
//
|
||||
// Proxies might buffer the connection to avoid this we
|
||||
// need the proper MIME type before writing to client.
|
||||
// This MIME header tells the proxies to avoid buffering
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
|
||||
// Start writing bucket notifications to ResponseWriter.
|
||||
nListenCh.waitForListener(w)
|
||||
}
|
||||
|
||||
// AddBucketListenerConfig - Updates on disk state of listeners, and
|
||||
@@ -380,7 +433,9 @@ func AddBucketListenerConfig(bucket string, lcfg *listenerConfig, objAPI ObjectL
|
||||
// Acquire a write lock on bucket before modifying its
|
||||
// configuration.
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.Lock()
|
||||
if err := bucketLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
// Release lock after notifying peers
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
@@ -421,7 +476,9 @@ func RemoveBucketListenerConfig(bucket string, lcfg *listenerConfig, objAPI Obje
|
||||
// Acquire a write lock on bucket before modifying its
|
||||
// configuration.
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.Lock()
|
||||
if bucketLock.GetLock(globalOperationTimeout) != nil {
|
||||
return
|
||||
}
|
||||
// Release lock after notifying peers
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
|
||||
@@ -21,12 +21,19 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Implement a dummy flush writer.
|
||||
@@ -51,7 +58,7 @@ func TestWriteNotification(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize test config %s", err)
|
||||
}
|
||||
defer removeAll(root)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
// Collection of test cases for each event writer.
|
||||
@@ -110,26 +117,56 @@ func TestWriteNotification(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendBucketNotification(t *testing.T) {
|
||||
// testResponseWriter implements `http.ResponseWriter` that buffers
|
||||
// response body in a `bytes.Buffer` and returns error after `failCount`
|
||||
// calls to `Write` method
|
||||
type testResponseWriter struct {
|
||||
mu sync.Mutex
|
||||
failCount int
|
||||
buf *bytes.Buffer
|
||||
m http.Header
|
||||
}
|
||||
|
||||
func newTestResponseWriter(failAt int) *testResponseWriter {
|
||||
return &testResponseWriter{
|
||||
buf: new(bytes.Buffer),
|
||||
m: make(http.Header),
|
||||
failCount: failAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (trw *testResponseWriter) Flush() {
|
||||
}
|
||||
|
||||
func (trw *testResponseWriter) Write(p []byte) (int, error) {
|
||||
trw.mu.Lock()
|
||||
defer trw.mu.Unlock()
|
||||
|
||||
if trw.failCount == 0 {
|
||||
return 0, errors.New("Custom error")
|
||||
}
|
||||
trw.failCount--
|
||||
|
||||
return trw.buf.Write(p)
|
||||
}
|
||||
|
||||
func (trw *testResponseWriter) Header() http.Header {
|
||||
return trw.m
|
||||
}
|
||||
|
||||
func (trw *testResponseWriter) WriteHeader(i int) {
|
||||
}
|
||||
|
||||
func TestListenChan(t *testing.T) {
|
||||
// Initialize a new test config.
|
||||
root, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize test config %s", err)
|
||||
}
|
||||
defer removeAll(root)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
eventCh := make(chan []NotificationEvent)
|
||||
|
||||
// Create a Pipe with FlushWriter on the write-side and bufio.Scanner
|
||||
// on the reader-side to receive notification over the listen channel in a
|
||||
// synchronized manner.
|
||||
pr, pw := io.Pipe()
|
||||
fw := newFlushWriter(pw)
|
||||
scanner := bufio.NewScanner(pr)
|
||||
// Start a go-routine to wait for notification events.
|
||||
go func(listenerCh <-chan []NotificationEvent) {
|
||||
sendBucketNotification(fw, listenerCh)
|
||||
}(eventCh)
|
||||
// Create a listen channel to manage notifications
|
||||
nListenCh := newListenChan()
|
||||
|
||||
// Construct notification events to be passed on the events channel.
|
||||
var events []NotificationEvent
|
||||
@@ -139,37 +176,68 @@ func TestSendBucketNotification(t *testing.T) {
|
||||
ObjectCreatedCopy,
|
||||
ObjectCreatedCompleteMultipartUpload,
|
||||
}
|
||||
|
||||
for _, evType := range evTypes {
|
||||
events = append(events, newNotificationEvent(eventData{
|
||||
Type: evType,
|
||||
}))
|
||||
}
|
||||
// Send notification events to the channel on which sendBucketNotification
|
||||
// is waiting on.
|
||||
eventCh <- events
|
||||
|
||||
// Read from the pipe connected to the ResponseWriter.
|
||||
scanner.Scan()
|
||||
notificationBytes := scanner.Bytes()
|
||||
|
||||
// Close the read-end and send an empty notification event on the channel
|
||||
// to signal sendBucketNotification to terminate.
|
||||
pr.Close()
|
||||
eventCh <- []NotificationEvent{}
|
||||
close(eventCh)
|
||||
|
||||
// Checking if the notification are the same as those sent over the channel.
|
||||
var notifications map[string][]NotificationEvent
|
||||
err = json.Unmarshal(notificationBytes, ¬ifications)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to Unmarshal notification")
|
||||
}
|
||||
records := notifications["Records"]
|
||||
for i, rec := range records {
|
||||
if rec.EventName == evTypes[i].String() {
|
||||
continue
|
||||
// Send notification events one-by-one
|
||||
go func() {
|
||||
for _, event := range events {
|
||||
nListenCh.sendNotificationEvent([]NotificationEvent{event})
|
||||
}
|
||||
t.Errorf("Failed to receive %d event %s", i, evTypes[i].String())
|
||||
}()
|
||||
|
||||
// Create a http.ResponseWriter that fails after len(events)
|
||||
// number of times
|
||||
trw := newTestResponseWriter(len(events))
|
||||
|
||||
// Wait for all (4) notification events to be received
|
||||
nListenCh.waitForListener(trw)
|
||||
|
||||
// Used to read JSON-formatted event stream line-by-line
|
||||
scanner := bufio.NewScanner(trw.buf)
|
||||
var records map[string][]NotificationEvent
|
||||
for i := 0; scanner.Scan(); i++ {
|
||||
err = json.Unmarshal(scanner.Bytes(), &records)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal json %v", err)
|
||||
}
|
||||
|
||||
nEvent := records["Records"][0]
|
||||
if nEvent.EventName != evTypes[i].String() {
|
||||
t.Errorf("notification event name mismatch, expected %s but got %s", evTypes[i], nEvent.EventName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendNotificationEvent(t *testing.T) {
|
||||
// This test verifies that sendNotificationEvent function
|
||||
// returns once listenChan.doneCh is closed
|
||||
|
||||
l := newListenChan()
|
||||
testCh := make(chan struct{})
|
||||
timeout := 5 * time.Second
|
||||
|
||||
go func() {
|
||||
// Send one empty notification event on listenChan
|
||||
events := []NotificationEvent{NotificationEvent{}}
|
||||
l.sendNotificationEvent(events)
|
||||
testCh <- struct{}{}
|
||||
}()
|
||||
|
||||
// close l.doneCh to signal client exiting from
|
||||
// ListenBucketNotification API call
|
||||
close(l.doneCh)
|
||||
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
t.Fatalf("sendNotificationEvent didn't return after %v seconds", timeout)
|
||||
case <-testCh:
|
||||
// If we reach this case, sendNotificationEvent
|
||||
// returned on closing l.doneCh
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,12 +248,12 @@ func TestGetBucketNotificationHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testGetBucketNotificationHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
// declare sample configs
|
||||
filterRules := []filterRule{
|
||||
{
|
||||
Name: "prefix",
|
||||
Value: globalMinioDefaultOwnerID,
|
||||
Value: "minio",
|
||||
},
|
||||
{
|
||||
Name: "suffix",
|
||||
@@ -246,6 +314,95 @@ func testGetBucketNotificationHandler(obj ObjectLayer, instanceType, bucketName
|
||||
}
|
||||
}
|
||||
|
||||
func TestPutBucketNotificationHandler(t *testing.T) {
|
||||
ExecObjectLayerAPITest(t, testPutBucketNotificationHandler, []string{
|
||||
"PutBucketNotification",
|
||||
})
|
||||
}
|
||||
|
||||
func testPutBucketNotificationHandler(obj ObjectLayer, instanceType,
|
||||
bucketName string, apiRouter http.Handler, credentials auth.Credentials,
|
||||
t *testing.T) {
|
||||
|
||||
// declare sample configs
|
||||
filterRules := []filterRule{
|
||||
{
|
||||
Name: "prefix",
|
||||
Value: "minio",
|
||||
},
|
||||
{
|
||||
Name: "suffix",
|
||||
Value: "*.jpg",
|
||||
},
|
||||
}
|
||||
sampleSvcCfg := ServiceConfig{
|
||||
[]string{"s3:ObjectRemoved:*", "s3:ObjectCreated:*"},
|
||||
filterStruct{
|
||||
keyFilter{filterRules},
|
||||
},
|
||||
"1",
|
||||
}
|
||||
sampleNotifCfg := notificationConfig{
|
||||
QueueConfigs: []queueConfig{
|
||||
{
|
||||
ServiceConfig: sampleSvcCfg,
|
||||
QueueARN: "testqARN",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
{
|
||||
sampleNotifCfg.LambdaConfigs = []lambdaConfig{
|
||||
{
|
||||
sampleSvcCfg, "testLARN",
|
||||
},
|
||||
}
|
||||
xmlBytes, err := xml.Marshal(sampleNotifCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Unexpected err: %#v", instanceType, err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
req, err := newTestSignedRequestV4("PUT",
|
||||
getPutBucketNotificationURL("", bucketName),
|
||||
int64(len(xmlBytes)), bytes.NewReader(xmlBytes),
|
||||
credentials.AccessKey, credentials.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Failed to create HTTP testRequest for PutBucketNotification: <ERROR> %v",
|
||||
instanceType, err)
|
||||
}
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("Unexpected http response %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
sampleNotifCfg.LambdaConfigs = nil
|
||||
sampleNotifCfg.TopicConfigs = []topicConfig{
|
||||
{
|
||||
sampleSvcCfg, "testTARN",
|
||||
},
|
||||
}
|
||||
xmlBytes, err := xml.Marshal(sampleNotifCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Unexpected err: %#v", instanceType, err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
req, err := newTestSignedRequestV4("PUT",
|
||||
getPutBucketNotificationURL("", bucketName),
|
||||
int64(len(xmlBytes)), bytes.NewReader(xmlBytes),
|
||||
credentials.AccessKey, credentials.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Failed to create HTTP testRequest for PutBucketNotification: <ERROR> %v",
|
||||
instanceType, err)
|
||||
}
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("Unexpected http response %d", rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenBucketNotificationNilHandler(t *testing.T) {
|
||||
ExecObjectLayerAPITest(t, testListenBucketNotificationNilHandler, []string{
|
||||
"ListenBucketNotification",
|
||||
@@ -254,7 +411,7 @@ func TestListenBucketNotificationNilHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testListenBucketNotificationNilHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
// get random bucket name.
|
||||
randBucket := getRandomBucketName()
|
||||
|
||||
@@ -268,6 +425,7 @@ func testListenBucketNotificationNilHandler(obj ObjectLayer, instanceType, bucke
|
||||
[]string{"*.jpg"}, []string{
|
||||
"s3:ObjectCreated:*",
|
||||
"s3:ObjectRemoved:*",
|
||||
"s3:ObjectAccessed:*",
|
||||
}), 0, nil, credentials.AccessKey, credentials.SecretKey)
|
||||
if tErr != nil {
|
||||
t.Fatalf("%s: Failed to create HTTP testRequest for ListenBucketNotification: <ERROR> %v", instanceType, tErr)
|
||||
@@ -279,26 +437,28 @@ func testListenBucketNotificationNilHandler(obj ObjectLayer, instanceType, bucke
|
||||
}
|
||||
}
|
||||
|
||||
func testRemoveNotificationConfig(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
func testRemoveNotificationConfig(obj ObjectLayer, instanceType,
|
||||
bucketName string, apiRouter http.Handler, credentials auth.Credentials,
|
||||
t *testing.T) {
|
||||
|
||||
invalidBucket := "Invalid\\Bucket"
|
||||
// get random bucket name.
|
||||
randBucket := bucketName
|
||||
|
||||
sampleNotificationBytes := []byte("<NotificationConfiguration><TopicConfiguration>" +
|
||||
"<Event>s3:ObjectCreated:*</Event><Event>s3:ObjectRemoved:*</Event><Filter>" +
|
||||
"<S3Key></S3Key></Filter><Id></Id><Topic>arn:minio:sns:us-east-1:1474332374:listen</Topic>" +
|
||||
"</TopicConfiguration></NotificationConfiguration>")
|
||||
|
||||
// Set sample bucket notification on randBucket.
|
||||
testRec := httptest.NewRecorder()
|
||||
testReq, tErr := newTestSignedRequestV4("PUT", getPutBucketNotificationURL("", randBucket),
|
||||
int64(len(sampleNotificationBytes)), bytes.NewReader(sampleNotificationBytes),
|
||||
credentials.AccessKey, credentials.SecretKey)
|
||||
if tErr != nil {
|
||||
t.Fatalf("%s: Failed to create HTTP testRequest for PutBucketNotification: <ERROR> %v", instanceType, tErr)
|
||||
nCfg := notificationConfig{
|
||||
QueueConfigs: []queueConfig{
|
||||
{
|
||||
ServiceConfig: ServiceConfig{
|
||||
Events: []string{"s3:ObjectRemoved:*",
|
||||
"s3:ObjectCreated:*"},
|
||||
},
|
||||
QueueARN: "testqARN",
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := persistNotificationConfig(randBucket, &nCfg, obj); err != nil {
|
||||
t.Fatalf("Unexpected error: %#v", err)
|
||||
}
|
||||
apiRouter.ServeHTTP(testRec, testReq)
|
||||
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
|
||||
package cmd
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
)
|
||||
|
||||
// List of valid event types.
|
||||
var suppportedEventTypes = map[string]struct{}{
|
||||
@@ -29,6 +34,9 @@ var suppportedEventTypes = map[string]struct{}{
|
||||
// Object removed event types.
|
||||
"s3:ObjectRemoved:*": {},
|
||||
"s3:ObjectRemoved:Delete": {},
|
||||
"s3:ObjectAccessed:Get": {},
|
||||
"s3:ObjectAccessed:Head": {},
|
||||
"s3:ObjectAccessed:*": {},
|
||||
}
|
||||
|
||||
// checkEvent - checks if an event is supported.
|
||||
@@ -104,20 +112,25 @@ func checkARN(arn, arnType string) APIErrorCode {
|
||||
if !strings.HasPrefix(arn, arnType) {
|
||||
return ErrARNNotification
|
||||
}
|
||||
if !strings.HasPrefix(arn, arnType+serverConfig.GetRegion()+":") {
|
||||
return ErrRegionNotification
|
||||
}
|
||||
account := strings.SplitN(strings.TrimPrefix(arn, arnType+serverConfig.GetRegion()+":"), ":", 2)
|
||||
switch len(account) {
|
||||
case 1:
|
||||
// This means ARN is malformed, account should have min of 2elements.
|
||||
strs := strings.SplitN(arn, ":", -1)
|
||||
if len(strs) != 6 {
|
||||
return ErrARNNotification
|
||||
case 2:
|
||||
// Account topic id or topic name cannot be empty.
|
||||
if account[0] == "" || account[1] == "" {
|
||||
return ErrARNNotification
|
||||
}
|
||||
|
||||
// Server region is allowed to be empty by default,
|
||||
// in such a scenario ARN region is not validating
|
||||
// allowing all regions.
|
||||
if sregion := serverConfig.GetRegion(); sregion != "" {
|
||||
region := strs[3]
|
||||
if region != sregion {
|
||||
return ErrRegionNotification
|
||||
}
|
||||
}
|
||||
accountID := strs[4]
|
||||
resource := strs[5]
|
||||
if accountID == "" || resource == "" {
|
||||
return ErrARNNotification
|
||||
}
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
@@ -135,6 +148,9 @@ func isValidQueueID(queueARN string) bool {
|
||||
if isAMQPQueue(sqsARN) { // AMQP eueue.
|
||||
amqpN := serverConfig.Notify.GetAMQPByID(sqsARN.AccountID)
|
||||
return amqpN.Enable && amqpN.URL != ""
|
||||
} else if isMQTTQueue(sqsARN) {
|
||||
mqttN := serverConfig.Notify.GetMQTTByID(sqsARN.AccountID)
|
||||
return mqttN.Enable && mqttN.Broker != ""
|
||||
} else if isNATSQueue(sqsARN) {
|
||||
natsN := serverConfig.Notify.GetNATSByID(sqsARN.AccountID)
|
||||
return natsN.Enable && natsN.Address != ""
|
||||
@@ -148,6 +164,10 @@ func isValidQueueID(queueARN string) bool {
|
||||
pgN := serverConfig.Notify.GetPostgreSQLByID(sqsARN.AccountID)
|
||||
// Postgres can work with only default conn. info.
|
||||
return pgN.Enable
|
||||
} else if isMySQLQueue(sqsARN) {
|
||||
msqlN := serverConfig.Notify.GetMySQLByID(sqsARN.AccountID)
|
||||
// Mysql can work with only default conn. info.
|
||||
return msqlN.Enable
|
||||
} else if isKafkaQueue(sqsARN) {
|
||||
kafkaN := serverConfig.Notify.GetKafkaByID(sqsARN.AccountID)
|
||||
return (kafkaN.Enable && len(kafkaN.Brokers) > 0 &&
|
||||
@@ -200,16 +220,14 @@ func validateQueueConfigs(queueConfigs []queueConfig) APIErrorCode {
|
||||
|
||||
// Check all the queue configs for any duplicates.
|
||||
func checkDuplicateQueueConfigs(configs []queueConfig) APIErrorCode {
|
||||
var queueConfigARNS []string
|
||||
queueConfigARNS := set.NewStringSet()
|
||||
|
||||
// Navigate through each configs and count the entries.
|
||||
for _, config := range configs {
|
||||
queueConfigARNS = append(queueConfigARNS, config.QueueARN)
|
||||
queueConfigARNS.Add(config.QueueARN)
|
||||
}
|
||||
|
||||
// Check if there are any duplicate counts.
|
||||
if err := checkDuplicateStrings(queueConfigARNS); err != nil {
|
||||
errorIf(err, "Invalid queue configs found.")
|
||||
if len(queueConfigARNS) != len(configs) {
|
||||
return ErrOverlappingConfigs
|
||||
}
|
||||
|
||||
@@ -221,6 +239,12 @@ func checkDuplicateQueueConfigs(configs []queueConfig) APIErrorCode {
|
||||
// if one of the config is malformed or has invalid data it is rejected.
|
||||
// Configuration is never applied partially.
|
||||
func validateNotificationConfig(nConfig notificationConfig) APIErrorCode {
|
||||
// Minio server does not support lambda/topic configurations
|
||||
// currently. Such configuration is rejected.
|
||||
if len(nConfig.LambdaConfigs) > 0 || len(nConfig.TopicConfigs) > 0 {
|
||||
return ErrUnsupportedNotification
|
||||
}
|
||||
|
||||
// Validate all queue configs.
|
||||
if s3Error := validateQueueConfigs(nConfig.QueueConfigs); s3Error != ErrNone {
|
||||
return s3Error
|
||||
@@ -240,34 +264,54 @@ func validateNotificationConfig(nConfig notificationConfig) APIErrorCode {
|
||||
// Unmarshals input value of AWS ARN format into minioSqs object.
|
||||
// Returned value represents minio sqs types, currently supported are
|
||||
// - amqp
|
||||
// - mqtt
|
||||
// - nats
|
||||
// - elasticsearch
|
||||
// - redis
|
||||
// - postgresql
|
||||
// - mysql
|
||||
// - kafka
|
||||
// - webhook
|
||||
func unmarshalSqsARN(queueARN string) (mSqs arnSQS) {
|
||||
mSqs = arnSQS{}
|
||||
if !strings.HasPrefix(queueARN, minioSqs+serverConfig.GetRegion()+":") {
|
||||
return mSqs
|
||||
strs := strings.SplitN(queueARN, ":", -1)
|
||||
if len(strs) != 6 {
|
||||
return
|
||||
}
|
||||
sqsType := strings.TrimPrefix(queueARN, minioSqs+serverConfig.GetRegion()+":")
|
||||
switch {
|
||||
case strings.HasSuffix(sqsType, queueTypeAMQP):
|
||||
|
||||
// Server region is allowed to be empty by default,
|
||||
// in such a scenario ARN region is not validating
|
||||
// allowing all regions.
|
||||
if sregion := serverConfig.GetRegion(); sregion != "" {
|
||||
region := strs[3]
|
||||
if region != sregion {
|
||||
return
|
||||
}
|
||||
}
|
||||
sqsType := strs[5]
|
||||
switch sqsType {
|
||||
case queueTypeAMQP:
|
||||
mSqs.Type = queueTypeAMQP
|
||||
case strings.HasSuffix(sqsType, queueTypeNATS):
|
||||
case queueTypeMQTT:
|
||||
mSqs.Type = queueTypeMQTT
|
||||
case queueTypeNATS:
|
||||
mSqs.Type = queueTypeNATS
|
||||
case strings.HasSuffix(sqsType, queueTypeElastic):
|
||||
case queueTypeElastic:
|
||||
mSqs.Type = queueTypeElastic
|
||||
case strings.HasSuffix(sqsType, queueTypeRedis):
|
||||
case queueTypeRedis:
|
||||
mSqs.Type = queueTypeRedis
|
||||
case strings.HasSuffix(sqsType, queueTypePostgreSQL):
|
||||
case queueTypePostgreSQL:
|
||||
mSqs.Type = queueTypePostgreSQL
|
||||
case strings.HasSuffix(sqsType, queueTypeKafka):
|
||||
case queueTypeMySQL:
|
||||
mSqs.Type = queueTypeMySQL
|
||||
case queueTypeKafka:
|
||||
mSqs.Type = queueTypeKafka
|
||||
case strings.HasSuffix(sqsType, queueTypeWebhook):
|
||||
case queueTypeWebhook:
|
||||
mSqs.Type = queueTypeWebhook
|
||||
default:
|
||||
errorIf(errors.New("invalid SQS type"), "SQS type: %s", sqsType)
|
||||
} // Add more queues here.
|
||||
mSqs.AccountID = strings.TrimSuffix(sqsType, ":"+mSqs.Type)
|
||||
return mSqs
|
||||
|
||||
mSqs.AccountID = strs[4]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -222,7 +223,7 @@ func TestQueueARN(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
testCases := []struct {
|
||||
queueARN string
|
||||
@@ -259,11 +260,6 @@ func TestQueueARN(t *testing.T) {
|
||||
queueARN: "arn:minio:sns:us-east-1:1:listen",
|
||||
errCode: ErrARNNotification,
|
||||
},
|
||||
// Invalid region 'us-west-1' in queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-west-1:1:redis",
|
||||
errCode: ErrRegionNotification,
|
||||
},
|
||||
// Invalid queue name empty in queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:",
|
||||
@@ -298,6 +294,37 @@ func TestQueueARN(t *testing.T) {
|
||||
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Test when server region is set.
|
||||
rootPath, err = newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
testCases = []struct {
|
||||
queueARN string
|
||||
errCode APIErrorCode
|
||||
}{
|
||||
// Incorrect region should produce error.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-west-1:1:webhook",
|
||||
errCode: ErrRegionNotification,
|
||||
},
|
||||
// Correct region should not produce error.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:webhook",
|
||||
errCode: ErrNone,
|
||||
},
|
||||
}
|
||||
|
||||
// Validate all tests for queue arn.
|
||||
for i, testCase := range testCases {
|
||||
errCode := checkQueueARN(testCase.queueARN)
|
||||
if testCase.errCode != errCode {
|
||||
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test unmarshal queue arn.
|
||||
@@ -306,7 +333,7 @@ func TestUnmarshalSQSARN(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
testCases := []struct {
|
||||
queueARN string
|
||||
@@ -332,16 +359,16 @@ func TestUnmarshalSQSARN(t *testing.T) {
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:amqp",
|
||||
Type: "amqp",
|
||||
},
|
||||
// Valid mqtt queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:mqtt",
|
||||
Type: "mqtt",
|
||||
},
|
||||
// Invalid empty queue arn.
|
||||
{
|
||||
queueARN: "",
|
||||
Type: "",
|
||||
},
|
||||
// Invalid region 'us-west-1' in queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-west-1:1:redis",
|
||||
Type: "",
|
||||
},
|
||||
// Partial queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:",
|
||||
@@ -361,4 +388,33 @@ func TestUnmarshalSQSARN(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test when the server region is set.
|
||||
rootPath, err = newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
testCases = []struct {
|
||||
queueARN string
|
||||
Type string
|
||||
}{
|
||||
// Incorrect region in ARN returns empty mSqs.Type
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-west-1:1:webhook",
|
||||
Type: "",
|
||||
},
|
||||
// Correct regionin ARN returns valid mSqs.Type
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:webhook",
|
||||
Type: "webhook",
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
mSqs := unmarshalSqsARN(testCase.queueARN)
|
||||
if testCase.Type != mSqs.Type {
|
||||
t.Errorf("Test %d: Expected \"%s\", got \"%s\"", i+1, testCase.Type, mSqs.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,17 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
mux "github.com/gorilla/mux"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,8 @@ const maxAccessPolicySize = 20 * humanize.KiByte
|
||||
|
||||
// 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]set.StringSet, statements []policyStatement) bool {
|
||||
func bucketPolicyEvalStatements(action string, resource string, conditions policy.ConditionKeyMap,
|
||||
statements []policy.Statement) bool {
|
||||
for _, statement := range statements {
|
||||
if bucketPolicyMatchStatement(action, resource, conditions, statement) {
|
||||
if statement.Effect == "Allow" {
|
||||
@@ -51,7 +53,8 @@ func bucketPolicyEvalStatements(action string, resource string, conditions map[s
|
||||
}
|
||||
|
||||
// Verify if action, resource and conditions match input policy statement.
|
||||
func bucketPolicyMatchStatement(action string, resource string, conditions map[string]set.StringSet, statement policyStatement) bool {
|
||||
func bucketPolicyMatchStatement(action string, resource string, conditions policy.ConditionKeyMap,
|
||||
statement policy.Statement) bool {
|
||||
// Verify if action, resource and condition match in given statement.
|
||||
return (bucketPolicyActionMatch(action, statement) &&
|
||||
bucketPolicyResourceMatch(resource, statement) &&
|
||||
@@ -59,7 +62,7 @@ func bucketPolicyMatchStatement(action string, resource string, conditions map[s
|
||||
}
|
||||
|
||||
// Verify if given action matches with policy statement.
|
||||
func bucketPolicyActionMatch(action string, statement policyStatement) bool {
|
||||
func bucketPolicyActionMatch(action string, statement policy.Statement) bool {
|
||||
return !statement.Actions.FuncMatch(actionMatch, action).IsEmpty()
|
||||
}
|
||||
|
||||
@@ -81,8 +84,20 @@ func refererMatch(pattern, referer string) bool {
|
||||
return wildcard.MatchSimple(pattern, referer)
|
||||
}
|
||||
|
||||
// isIPInCIDR - checks if a given a IP address is a member of the given subnet.
|
||||
func isIPInCIDR(cidr, ip string) bool {
|
||||
// AWS S3 spec says IPs must use standard CIDR notation.
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html#example-bucket-policies-use-case-3.
|
||||
_, cidrNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return false // If provided CIDR can't be parsed no IP will be in the subnet.
|
||||
}
|
||||
addr := net.ParseIP(ip)
|
||||
return cidrNet.Contains(addr)
|
||||
}
|
||||
|
||||
// Verify if given resource matches with policy statement.
|
||||
func bucketPolicyResourceMatch(resource string, statement policyStatement) bool {
|
||||
func bucketPolicyResourceMatch(resource string, statement policy.Statement) bool {
|
||||
// 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.
|
||||
@@ -91,17 +106,20 @@ func bucketPolicyResourceMatch(resource string, statement policyStatement) bool
|
||||
}
|
||||
|
||||
// Verify if given condition matches with policy statement.
|
||||
func bucketPolicyConditionMatch(conditions map[string]set.StringSet, statement policyStatement) bool {
|
||||
func bucketPolicyConditionMatch(conditions policy.ConditionKeyMap, statement policy.Statement) bool {
|
||||
// Supports following conditions.
|
||||
// - StringEquals
|
||||
// - StringNotEquals
|
||||
// - StringLike
|
||||
// - StringNotLike
|
||||
// - IpAddress
|
||||
// - NotIpAddress
|
||||
//
|
||||
// Supported applicable condition keys for each conditions.
|
||||
// - s3:prefix
|
||||
// - s3:max-keys
|
||||
// - s3:aws-Referer
|
||||
// - s3:aws-SourceIp
|
||||
|
||||
// The following loop evaluates the logical AND of all the
|
||||
// conditions in the statement. Note: we can break out of the
|
||||
@@ -159,6 +177,37 @@ func bucketPolicyConditionMatch(conditions map[string]set.StringSet, statement p
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else if condition == "IpAddress" {
|
||||
awsIps := conditionKeyVal["aws:SourceIp"]
|
||||
// Skip empty condition, it is trivially satisfied.
|
||||
if awsIps.IsEmpty() {
|
||||
continue
|
||||
}
|
||||
// wildcard match of ip if statement was not empty.
|
||||
// Find a valid ip.
|
||||
ipFound := false
|
||||
for ip := range conditions["ip"] {
|
||||
if !awsIps.FuncMatch(isIPInCIDR, ip).IsEmpty() {
|
||||
ipFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ipFound {
|
||||
return false
|
||||
}
|
||||
} else if condition == "NotIpAddress" {
|
||||
awsIps := conditionKeyVal["aws:SourceIp"]
|
||||
// Skip empty condition, it is trivially satisfied.
|
||||
if awsIps.IsEmpty() {
|
||||
continue
|
||||
}
|
||||
// wildcard match of ip if statement was not empty.
|
||||
// Find if nothing matches.
|
||||
for ip := range conditions["ip"] {
|
||||
if !awsIps.FuncMatch(isIPInCIDR, ip).IsEmpty() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,13 +302,11 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
||||
|
||||
// Delete bucket access policy, by passing an empty policy
|
||||
// struct.
|
||||
if err := persistAndNotifyBucketPolicyChange(bucket, policyChange{true, nil}, objAPI); err != nil {
|
||||
switch err.(type) {
|
||||
case BucketPolicyNotFound:
|
||||
writeErrorResponse(w, ErrNoSuchBucketPolicy, r.URL)
|
||||
default:
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
}
|
||||
err = persistAndNotifyBucketPolicyChange(bucket, policyChange{
|
||||
true, policy.BucketAccessPolicy{},
|
||||
}, objAPI)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -298,15 +345,17 @@ func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
policy, err := readBucketPolicy(bucket, objAPI)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to read bucket policy.")
|
||||
switch err.(type) {
|
||||
case BucketPolicyNotFound:
|
||||
writeErrorResponse(w, ErrNoSuchBucketPolicy, r.URL)
|
||||
default:
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
}
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
policyBytes, err := json.Marshal(&policy)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to marshal bucket policy.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write to client.
|
||||
fmt.Fprint(w, policy)
|
||||
w.Write(policyBytes)
|
||||
}
|
||||
|
||||
@@ -25,15 +25,17 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Tests validate Bucket policy resource matcher.
|
||||
func TestBucketPolicyResourceMatch(t *testing.T) {
|
||||
|
||||
// generates statement with given resource..
|
||||
generateStatement := func(resource string) policyStatement {
|
||||
statement := policyStatement{}
|
||||
generateStatement := func(resource string) policy.Statement {
|
||||
statement := policy.Statement{}
|
||||
statement.Resources = set.CreateStringSet([]string{resource}...)
|
||||
return statement
|
||||
}
|
||||
@@ -45,7 +47,7 @@ func TestBucketPolicyResourceMatch(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
resourceToMatch string
|
||||
statement policyStatement
|
||||
statement policy.Statement
|
||||
expectedResourceMatch bool
|
||||
}{
|
||||
// Test case 1-4.
|
||||
@@ -85,7 +87,7 @@ func TestBucketPolicyResourceMatch(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestBucketPolicyActionMatch - Test validates whether given action on the
|
||||
// bucket/object matches the allowed actions in policyStatement.
|
||||
// bucket/object matches the allowed actions in policy.Statement.
|
||||
// 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) {
|
||||
@@ -94,7 +96,7 @@ func TestBucketPolicyActionMatch(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
action string
|
||||
statement policyStatement
|
||||
statement policy.Statement
|
||||
expectedResult bool
|
||||
}{
|
||||
// s3:GetBucketLocation is the action necessary to be present in the bucket policy to allow
|
||||
@@ -247,11 +249,11 @@ func TestPutBucketPolicyHandler(t *testing.T) {
|
||||
|
||||
// testPutBucketPolicyHandler - Test for Bucket policy end point.
|
||||
func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
bucketName1 := fmt.Sprintf("%s-1", bucketName)
|
||||
if err := obj.MakeBucket(bucketName1); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(bucketName1, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -454,7 +456,7 @@ func TestGetBucketPolicyHandler(t *testing.T) {
|
||||
|
||||
// testGetBucketPolicyHandler - Test for end point which fetches the access policy json of the given bucket.
|
||||
func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
// initialize bucket policy.
|
||||
initBucketPolicies(obj)
|
||||
|
||||
@@ -643,7 +645,7 @@ func TestDeleteBucketPolicyHandler(t *testing.T) {
|
||||
|
||||
// testDeleteBucketPolicyHandler - Test for Delete bucket policy end point.
|
||||
func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
// initialize bucket policy.
|
||||
initBucketPolicies(obj)
|
||||
|
||||
@@ -843,28 +845,28 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
||||
|
||||
// TestBucketPolicyConditionMatch - Tests to validate whether bucket policy conditions match.
|
||||
func TestBucketPolicyConditionMatch(t *testing.T) {
|
||||
// obtain the inner map[string]set.StringSet for policyStatement.Conditions .
|
||||
// obtain the inner map[string]set.StringSet for policy.Statement.Conditions.
|
||||
getInnerMap := func(key2, value string) map[string]set.StringSet {
|
||||
innerMap := make(map[string]set.StringSet)
|
||||
innerMap[key2] = set.CreateStringSet(value)
|
||||
return innerMap
|
||||
}
|
||||
|
||||
// obtain policyStatement with Conditions set.
|
||||
getStatementWithCondition := func(key1, key2, value string) policyStatement {
|
||||
// obtain policy.Statement with Conditions set.
|
||||
getStatementWithCondition := func(key1, key2, value string) policy.Statement {
|
||||
innerMap := getInnerMap(key2, value)
|
||||
// to set policyStatment.Conditions .
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions[key1] = innerMap
|
||||
// new policy statement.
|
||||
statement := policyStatement{}
|
||||
statement := policy.Statement{}
|
||||
// set the condition.
|
||||
statement.Conditions = conditions
|
||||
return statement
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
statementCondition policyStatement
|
||||
statementCondition policy.Statement
|
||||
condition map[string]set.StringSet
|
||||
|
||||
expectedMatch bool
|
||||
@@ -970,6 +972,34 @@ func TestBucketPolicyConditionMatch(t *testing.T) {
|
||||
condition: getInnerMap("referer", "http://somethingelse.com/"),
|
||||
expectedMatch: true,
|
||||
},
|
||||
// Test case 13.
|
||||
// IpAddress condition evaluates to true.
|
||||
{
|
||||
statementCondition: getStatementWithCondition("IpAddress", "aws:SourceIp", "54.240.143.0/24"),
|
||||
condition: getInnerMap("ip", "54.240.143.2"),
|
||||
expectedMatch: true,
|
||||
},
|
||||
// Test case 14.
|
||||
// IpAddress condition evaluates to false.
|
||||
{
|
||||
statementCondition: getStatementWithCondition("IpAddress", "aws:SourceIp", "54.240.143.0/24"),
|
||||
condition: getInnerMap("ip", "127.240.143.224"),
|
||||
expectedMatch: false,
|
||||
},
|
||||
// Test case 15.
|
||||
// NotIpAddress condition evaluates to true.
|
||||
{
|
||||
statementCondition: getStatementWithCondition("NotIpAddress", "aws:SourceIp", "54.240.143.0/24"),
|
||||
condition: getInnerMap("ip", "54.240.144.188"),
|
||||
expectedMatch: true,
|
||||
},
|
||||
// Test case 16.
|
||||
// NotIpAddress condition evaluates to false.
|
||||
{
|
||||
statementCondition: getStatementWithCondition("NotIpAddress", "aws:SourceIp", "54.240.143.0/24"),
|
||||
condition: getInnerMap("ip", "54.240.143.243"),
|
||||
expectedMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
|
||||
+34
-97
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2015, 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.
|
||||
@@ -26,12 +26,16 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
)
|
||||
|
||||
var conditionKeyActionMap = map[string]set.StringSet{
|
||||
"s3:prefix": set.CreateStringSet("s3:ListBucket"),
|
||||
"s3:max-keys": set.CreateStringSet("s3:ListBucket"),
|
||||
var emptyBucketPolicy = policy.BucketAccessPolicy{}
|
||||
|
||||
var conditionKeyActionMap = policy.ConditionKeyMap{
|
||||
"s3:prefix": set.CreateStringSet("s3:ListBucket", "s3:ListBucketMultipartUploads"),
|
||||
"s3:max-keys": set.CreateStringSet("s3:ListBucket", "s3:ListBucketMultipartUploads",
|
||||
"s3:ListMultipartUploadParts"),
|
||||
}
|
||||
|
||||
// supportedActionMap - lists all the actions supported by minio.
|
||||
@@ -40,50 +44,25 @@ var supportedActionMap = set.CreateStringSet("*", "s3:*", "s3:GetObject",
|
||||
"s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts")
|
||||
|
||||
// supported Conditions type.
|
||||
var supportedConditionsType = set.CreateStringSet("StringEquals", "StringNotEquals", "StringLike", "StringNotLike")
|
||||
var supportedConditionsType = set.CreateStringSet("StringEquals", "StringNotEquals", "StringLike", "StringNotLike", "IpAddress", "NotIpAddress")
|
||||
|
||||
// Validate s3:prefix, s3:max-keys are present if not
|
||||
// supported keys for the conditions.
|
||||
var supportedConditionsKey = set.CreateStringSet("s3:prefix", "s3:max-keys", "aws:Referer")
|
||||
var supportedConditionsKey = set.CreateStringSet("s3:prefix", "s3:max-keys", "aws:Referer", "aws:SourceIp")
|
||||
|
||||
// supportedEffectMap - supported effects.
|
||||
var supportedEffectMap = set.CreateStringSet("Allow", "Deny")
|
||||
|
||||
// Statement - minio policy statement
|
||||
type policyStatement struct {
|
||||
Actions set.StringSet `json:"Action"`
|
||||
Conditions map[string]map[string]set.StringSet `json:"Condition,omitempty"`
|
||||
Effect string
|
||||
Principal interface{} `json:"Principal"`
|
||||
Resources set.StringSet `json:"Resource"`
|
||||
Sid string
|
||||
}
|
||||
|
||||
// bucketPolicy - collection of various bucket policy statements.
|
||||
type bucketPolicy struct {
|
||||
Version string // date in YYYY-MM-DD 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 marshal bucket policy into JSON %#v", b)
|
||||
return ""
|
||||
}
|
||||
return string(bbytes)
|
||||
}
|
||||
|
||||
// isValidActions - are actions valid.
|
||||
func isValidActions(actions set.StringSet) (err error) {
|
||||
// Statement actions cannot be empty.
|
||||
if len(actions) == 0 {
|
||||
if actions.IsEmpty() {
|
||||
err = errors.New("Action list cannot be empty")
|
||||
return err
|
||||
}
|
||||
if unsupportedActions := actions.Difference(supportedActionMap); !unsupportedActions.IsEmpty() {
|
||||
err = fmt.Errorf("Unsupported actions found: ‘%#v’, please validate your policy document", unsupportedActions)
|
||||
err = fmt.Errorf("Unsupported actions found: ‘%#v’, please validate your policy document",
|
||||
unsupportedActions)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -106,7 +85,7 @@ func isValidEffect(effect string) (err error) {
|
||||
// isValidResources - are valid resources.
|
||||
func isValidResources(resources set.StringSet) (err error) {
|
||||
// Statement resources cannot be empty.
|
||||
if len(resources) == 0 {
|
||||
if resources.IsEmpty() {
|
||||
err = errors.New("Resource list cannot be empty")
|
||||
return err
|
||||
}
|
||||
@@ -124,60 +103,17 @@ func isValidResources(resources set.StringSet) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse principals parses a incoming json. Handles cases for
|
||||
// these three combinations.
|
||||
// - "Principal": "*",
|
||||
// - "Principal": { "AWS" : "*" }
|
||||
// - "Principal": { "AWS" : [ "*" ]}
|
||||
func parsePrincipals(principal interface{}) set.StringSet {
|
||||
principals, ok := principal.(map[string]interface{})
|
||||
if !ok {
|
||||
var principalStr string
|
||||
principalStr, ok = principal.(string)
|
||||
if ok {
|
||||
return set.CreateStringSet(principalStr)
|
||||
}
|
||||
} // else {
|
||||
var principalStrs []string
|
||||
for _, p := range principals {
|
||||
principalStr, isStr := p.(string)
|
||||
if !isStr {
|
||||
principalsAdd, isInterface := p.([]interface{})
|
||||
if !isInterface {
|
||||
principalStrsAddr, isStrs := p.([]string)
|
||||
if !isStrs {
|
||||
continue
|
||||
}
|
||||
principalStrs = append(principalStrs, principalStrsAddr...)
|
||||
} else {
|
||||
for _, pa := range principalsAdd {
|
||||
var pstr string
|
||||
pstr, isStr = pa.(string)
|
||||
if !isStr {
|
||||
continue
|
||||
}
|
||||
principalStrs = append(principalStrs, pstr)
|
||||
}
|
||||
}
|
||||
continue
|
||||
} // else {
|
||||
principalStrs = append(principalStrs, principalStr)
|
||||
}
|
||||
return set.CreateStringSet(principalStrs...)
|
||||
}
|
||||
|
||||
// isValidPrincipals - are valid principals.
|
||||
func isValidPrincipals(principal interface{}) (err error) {
|
||||
principals := parsePrincipals(principal)
|
||||
// Statement principal should have a value.
|
||||
if len(principals) == 0 {
|
||||
err = errors.New("Principal cannot be empty")
|
||||
return err
|
||||
func isValidPrincipals(principal policy.User) (err error) {
|
||||
if principal.AWS.IsEmpty() {
|
||||
return errors.New("Principal cannot be empty")
|
||||
}
|
||||
if unsuppPrincipals := principals.Difference(set.CreateStringSet([]string{"*"}...)); !unsuppPrincipals.IsEmpty() {
|
||||
if diff := principal.AWS.Difference(set.CreateStringSet("*")); !diff.IsEmpty() {
|
||||
// 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
|
||||
err = fmt.Errorf("Unsupported principals found: ‘%#v’, please validate your policy document", unsuppPrincipals)
|
||||
// Amazon s3 doc on principal:
|
||||
// http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Principal
|
||||
err = fmt.Errorf("Unsupported principals found: ‘%#v’, please validate your policy document",
|
||||
diff)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -185,7 +121,7 @@ func isValidPrincipals(principal interface{}) (err error) {
|
||||
|
||||
// isValidConditions - returns nil if the given conditions valid and
|
||||
// corresponding error otherwise.
|
||||
func isValidConditions(actions set.StringSet, conditions map[string]map[string]set.StringSet) (err error) {
|
||||
func isValidConditions(actions set.StringSet, conditions policy.ConditionMap) (err error) {
|
||||
// Verify conditions should be valid. Validate if only
|
||||
// supported condition keys are present and return error
|
||||
// otherwise.
|
||||
@@ -239,7 +175,7 @@ func resourcePrefix(resource string) string {
|
||||
// checkBucketPolicyResources validates Resources in unmarshalled bucket policy structure.
|
||||
// - Resources are validated against the given set of Actions.
|
||||
// -
|
||||
func checkBucketPolicyResources(bucket string, bucketPolicy *bucketPolicy) APIErrorCode {
|
||||
func checkBucketPolicyResources(bucket string, bucketPolicy policy.BucketAccessPolicy) APIErrorCode {
|
||||
// Validate statements for special actions and collect resources
|
||||
// for others to validate nesting.
|
||||
var resourceMap = set.NewStringSet()
|
||||
@@ -294,27 +230,27 @@ func checkBucketPolicyResources(bucket string, bucketPolicy *bucketPolicy) APIEr
|
||||
|
||||
// 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) {
|
||||
func parseBucketPolicy(bucketPolicyReader io.Reader, bktPolicy *policy.BucketAccessPolicy) (err error) {
|
||||
// Parse bucket policy reader.
|
||||
decoder := json.NewDecoder(bucketPolicyReader)
|
||||
if err = decoder.Decode(&policy); err != nil {
|
||||
if err = decoder.Decode(bktPolicy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Policy version cannot be empty.
|
||||
if len(policy.Version) == 0 {
|
||||
if len(bktPolicy.Version) == 0 {
|
||||
err = errors.New("Policy version cannot be empty")
|
||||
return err
|
||||
}
|
||||
|
||||
// Policy statements cannot be empty.
|
||||
if len(policy.Statements) == 0 {
|
||||
if len(bktPolicy.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 {
|
||||
for _, statement := range bktPolicy.Statements {
|
||||
// Statement effect should be valid.
|
||||
if err := isValidEffect(statement.Effect); err != nil {
|
||||
return err
|
||||
@@ -339,19 +275,20 @@ func parseBucketPolicy(bucketPolicyReader io.Reader, policy *bucketPolicy) (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 {
|
||||
var denyStatements []policy.Statement
|
||||
var allowStatements []policy.Statement
|
||||
for _, statement := range bktPolicy.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...)
|
||||
bktPolicy.Statements = append(denyStatements, allowStatements...)
|
||||
|
||||
// Return successfully parsed policy structure.
|
||||
return nil
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
@@ -74,11 +75,11 @@ var (
|
||||
)
|
||||
|
||||
// Obtain bucket statement for read-write bucketPolicy.
|
||||
func getReadWriteObjectStatement(bucketName, objectPrefix string) policyStatement {
|
||||
objectResourceStatement := policyStatement{}
|
||||
func getReadWriteObjectStatement(bucketName, objectPrefix string) policy.Statement {
|
||||
objectResourceStatement := policy.Statement{}
|
||||
objectResourceStatement.Effect = "Allow"
|
||||
objectResourceStatement.Principal = map[string]interface{}{
|
||||
"AWS": "*",
|
||||
objectResourceStatement.Principal = policy.User{
|
||||
AWS: set.StringSet{"*": struct{}{}},
|
||||
}
|
||||
objectResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName+"/"+objectPrefix+"*")}...)
|
||||
objectResourceStatement.Actions = set.CreateStringSet(readWriteObjectActions...)
|
||||
@@ -86,11 +87,11 @@ func getReadWriteObjectStatement(bucketName, objectPrefix string) policyStatemen
|
||||
}
|
||||
|
||||
// Obtain object statement for read-write bucketPolicy.
|
||||
func getReadWriteBucketStatement(bucketName, objectPrefix string) policyStatement {
|
||||
bucketResourceStatement := policyStatement{}
|
||||
func getReadWriteBucketStatement(bucketName, objectPrefix string) policy.Statement {
|
||||
bucketResourceStatement := policy.Statement{}
|
||||
bucketResourceStatement.Effect = "Allow"
|
||||
bucketResourceStatement.Principal = map[string]interface{}{
|
||||
"AWS": "*",
|
||||
bucketResourceStatement.Principal = policy.User{
|
||||
AWS: set.StringSet{"*": struct{}{}},
|
||||
}
|
||||
bucketResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName)}...)
|
||||
bucketResourceStatement.Actions = set.CreateStringSet(readWriteBucketActions...)
|
||||
@@ -98,19 +99,19 @@ func getReadWriteBucketStatement(bucketName, objectPrefix string) policyStatemen
|
||||
}
|
||||
|
||||
// Obtain statements for read-write bucketPolicy.
|
||||
func getReadWriteStatement(bucketName, objectPrefix string) []policyStatement {
|
||||
statements := []policyStatement{}
|
||||
func getReadWriteStatement(bucketName, objectPrefix string) []policy.Statement {
|
||||
statements := []policy.Statement{}
|
||||
// Save the read write policy.
|
||||
statements = append(statements, getReadWriteBucketStatement(bucketName, objectPrefix), getReadWriteObjectStatement(bucketName, objectPrefix))
|
||||
return statements
|
||||
}
|
||||
|
||||
// Obtain bucket statement for read only bucketPolicy.
|
||||
func getReadOnlyBucketStatement(bucketName, objectPrefix string) policyStatement {
|
||||
bucketResourceStatement := policyStatement{}
|
||||
func getReadOnlyBucketStatement(bucketName, objectPrefix string) policy.Statement {
|
||||
bucketResourceStatement := policy.Statement{}
|
||||
bucketResourceStatement.Effect = "Allow"
|
||||
bucketResourceStatement.Principal = map[string]interface{}{
|
||||
"AWS": "*",
|
||||
bucketResourceStatement.Principal = policy.User{
|
||||
AWS: set.StringSet{"*": struct{}{}},
|
||||
}
|
||||
bucketResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName)}...)
|
||||
bucketResourceStatement.Actions = set.CreateStringSet(readOnlyBucketActions...)
|
||||
@@ -118,11 +119,11 @@ func getReadOnlyBucketStatement(bucketName, objectPrefix string) policyStatement
|
||||
}
|
||||
|
||||
// Obtain object statement for read only bucketPolicy.
|
||||
func getReadOnlyObjectStatement(bucketName, objectPrefix string) policyStatement {
|
||||
objectResourceStatement := policyStatement{}
|
||||
func getReadOnlyObjectStatement(bucketName, objectPrefix string) policy.Statement {
|
||||
objectResourceStatement := policy.Statement{}
|
||||
objectResourceStatement.Effect = "Allow"
|
||||
objectResourceStatement.Principal = map[string]interface{}{
|
||||
"AWS": "*",
|
||||
objectResourceStatement.Principal = policy.User{
|
||||
AWS: set.StringSet{"*": struct{}{}},
|
||||
}
|
||||
objectResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName+"/"+objectPrefix+"*")}...)
|
||||
objectResourceStatement.Actions = set.CreateStringSet(readOnlyObjectActions...)
|
||||
@@ -130,20 +131,20 @@ func getReadOnlyObjectStatement(bucketName, objectPrefix string) policyStatement
|
||||
}
|
||||
|
||||
// Obtain statements for read only bucketPolicy.
|
||||
func getReadOnlyStatement(bucketName, objectPrefix string) []policyStatement {
|
||||
statements := []policyStatement{}
|
||||
func getReadOnlyStatement(bucketName, objectPrefix string) []policy.Statement {
|
||||
statements := []policy.Statement{}
|
||||
// Save the read only policy.
|
||||
statements = append(statements, getReadOnlyBucketStatement(bucketName, objectPrefix), getReadOnlyObjectStatement(bucketName, objectPrefix))
|
||||
return statements
|
||||
}
|
||||
|
||||
// Obtain bucket statements for write only bucketPolicy.
|
||||
func getWriteOnlyBucketStatement(bucketName, objectPrefix string) policyStatement {
|
||||
func getWriteOnlyBucketStatement(bucketName, objectPrefix string) policy.Statement {
|
||||
|
||||
bucketResourceStatement := policyStatement{}
|
||||
bucketResourceStatement := policy.Statement{}
|
||||
bucketResourceStatement.Effect = "Allow"
|
||||
bucketResourceStatement.Principal = map[string]interface{}{
|
||||
"AWS": "*",
|
||||
bucketResourceStatement.Principal = policy.User{
|
||||
AWS: set.StringSet{"*": struct{}{}},
|
||||
}
|
||||
bucketResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName)}...)
|
||||
bucketResourceStatement.Actions = set.CreateStringSet(writeOnlyBucketActions...)
|
||||
@@ -151,11 +152,11 @@ func getWriteOnlyBucketStatement(bucketName, objectPrefix string) policyStatemen
|
||||
}
|
||||
|
||||
// Obtain object statements for write only bucketPolicy.
|
||||
func getWriteOnlyObjectStatement(bucketName, objectPrefix string) policyStatement {
|
||||
objectResourceStatement := policyStatement{}
|
||||
func getWriteOnlyObjectStatement(bucketName, objectPrefix string) policy.Statement {
|
||||
objectResourceStatement := policy.Statement{}
|
||||
objectResourceStatement.Effect = "Allow"
|
||||
objectResourceStatement.Principal = map[string]interface{}{
|
||||
"AWS": "*",
|
||||
objectResourceStatement.Principal = policy.User{
|
||||
AWS: set.StringSet{"*": struct{}{}},
|
||||
}
|
||||
objectResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName+"/"+objectPrefix+"*")}...)
|
||||
objectResourceStatement.Actions = set.CreateStringSet(writeOnlyObjectActions...)
|
||||
@@ -163,8 +164,8 @@ func getWriteOnlyObjectStatement(bucketName, objectPrefix string) policyStatemen
|
||||
}
|
||||
|
||||
// Obtain statements for write only bucketPolicy.
|
||||
func getWriteOnlyStatement(bucketName, objectPrefix string) []policyStatement {
|
||||
statements := []policyStatement{}
|
||||
func getWriteOnlyStatement(bucketName, objectPrefix string) []policy.Statement {
|
||||
statements := []policy.Statement{}
|
||||
// Write only policy.
|
||||
// Save the write only policy.
|
||||
statements = append(statements, getWriteOnlyBucketStatement(bucketName, objectPrefix), getWriteOnlyBucketStatement(bucketName, objectPrefix))
|
||||
@@ -325,9 +326,10 @@ func TestIsValidPrincipals(t *testing.T) {
|
||||
{[]string{"*"}, nil, true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
err := isValidPrincipals(map[string]interface{}{
|
||||
"AWS": testCase.principals,
|
||||
})
|
||||
u := policy.User{
|
||||
AWS: set.CreateStringSet(testCase.principals...),
|
||||
}
|
||||
err := isValidPrincipals(u)
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
|
||||
}
|
||||
@@ -343,80 +345,86 @@ func TestIsValidPrincipals(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// getEmptyConditionKeyMap - returns a function that generates a
|
||||
// getEmptyConditionMap - returns a function that generates a
|
||||
// condition key map for a given key.
|
||||
func getEmptyConditionKeyMap(conditionKey string) func() map[string]map[string]set.StringSet {
|
||||
emptyConditonGenerator := func() map[string]map[string]set.StringSet {
|
||||
emptyMap := make(map[string]set.StringSet)
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
func getEmptyConditionMap(conditionKey string) func() policy.ConditionMap {
|
||||
emptyConditonGenerator := func() policy.ConditionMap {
|
||||
emptyMap := make(policy.ConditionKeyMap)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions[conditionKey] = emptyMap
|
||||
return conditions
|
||||
}
|
||||
return emptyConditonGenerator
|
||||
}
|
||||
|
||||
// Tests validate policyStatement condition validator.
|
||||
// Tests validate policy.Statement condition validator.
|
||||
func TestIsValidConditions(t *testing.T) {
|
||||
// returns empty conditions map.
|
||||
setEmptyConditions := func() map[string]map[string]set.StringSet {
|
||||
return make(map[string]map[string]set.StringSet)
|
||||
setEmptyConditions := func() policy.ConditionMap {
|
||||
return make(policy.ConditionMap)
|
||||
}
|
||||
|
||||
// returns map with the "StringEquals" set to empty map.
|
||||
setEmptyStringEquals := getEmptyConditionKeyMap("StringEquals")
|
||||
setEmptyStringEquals := getEmptyConditionMap("StringEquals")
|
||||
|
||||
// returns map with the "StringNotEquals" set to empty map.
|
||||
setEmptyStringNotEquals := getEmptyConditionKeyMap("StringNotEquals")
|
||||
setEmptyStringNotEquals := getEmptyConditionMap("StringNotEquals")
|
||||
|
||||
// returns map with the "StringLike" set to empty map.
|
||||
setEmptyStringLike := getEmptyConditionKeyMap("StringLike")
|
||||
setEmptyStringLike := getEmptyConditionMap("StringLike")
|
||||
|
||||
// returns map with the "StringNotLike" set to empty map.
|
||||
setEmptyStringNotLike := getEmptyConditionKeyMap("StringNotLike")
|
||||
setEmptyStringNotLike := getEmptyConditionMap("StringNotLike")
|
||||
|
||||
// returns map with the "IpAddress" set to empty map.
|
||||
setEmptyIPAddress := getEmptyConditionMap("IpAddress")
|
||||
|
||||
// returns map with "NotIpAddress" set to empty map.
|
||||
setEmptyNotIPAddress := getEmptyConditionMap("NotIpAddress")
|
||||
|
||||
// Generate conditions.
|
||||
generateConditions := func(key1, key2, value string) map[string]map[string]set.StringSet {
|
||||
innerMap := make(map[string]set.StringSet)
|
||||
generateConditions := func(key1, key2, value string) policy.ConditionMap {
|
||||
innerMap := make(policy.ConditionKeyMap)
|
||||
innerMap[key2] = set.CreateStringSet(value)
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions[key1] = innerMap
|
||||
return conditions
|
||||
}
|
||||
|
||||
// generate ambigious conditions.
|
||||
generateAmbigiousConditions := func() map[string]map[string]set.StringSet {
|
||||
prefixMap := make(map[string]set.StringSet)
|
||||
generateAmbigiousConditions := func() policy.ConditionMap {
|
||||
prefixMap := make(policy.ConditionKeyMap)
|
||||
prefixMap["s3:prefix"] = set.CreateStringSet("Asia/")
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions["StringEquals"] = prefixMap
|
||||
conditions["StringNotEquals"] = prefixMap
|
||||
return conditions
|
||||
}
|
||||
|
||||
// generate valid and non valid type in the condition map.
|
||||
generateValidInvalidConditions := func() map[string]map[string]set.StringSet {
|
||||
innerMap := make(map[string]set.StringSet)
|
||||
generateValidInvalidConditions := func() policy.ConditionMap {
|
||||
innerMap := make(policy.ConditionKeyMap)
|
||||
innerMap["s3:prefix"] = set.CreateStringSet("Asia/")
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions["StringEquals"] = innerMap
|
||||
conditions["InvalidType"] = innerMap
|
||||
return conditions
|
||||
}
|
||||
|
||||
// generate valid and invalid keys for valid types in the same condition map.
|
||||
generateValidInvalidConditionKeys := func() map[string]map[string]set.StringSet {
|
||||
innerMapValid := make(map[string]set.StringSet)
|
||||
generateValidInvalidConditionKeys := func() policy.ConditionMap {
|
||||
innerMapValid := make(policy.ConditionKeyMap)
|
||||
innerMapValid["s3:prefix"] = set.CreateStringSet("Asia/")
|
||||
innerMapInValid := make(map[string]set.StringSet)
|
||||
innerMapInValid["s3:invalid"] = set.CreateStringSet("Asia/")
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions["StringEquals"] = innerMapValid
|
||||
conditions["StringEquals"] = innerMapInValid
|
||||
return conditions
|
||||
}
|
||||
|
||||
// List of Conditions used for test cases.
|
||||
testConditions := []map[string]map[string]set.StringSet{
|
||||
testConditions := []policy.ConditionMap{
|
||||
generateConditions("StringValues", "s3:max-keys", "100"),
|
||||
generateConditions("StringEquals", "s3:Object", "100"),
|
||||
generateAmbigiousConditions(),
|
||||
@@ -427,6 +435,8 @@ func TestIsValidConditions(t *testing.T) {
|
||||
setEmptyStringNotEquals(),
|
||||
setEmptyStringLike(),
|
||||
setEmptyStringNotLike(),
|
||||
setEmptyIPAddress(),
|
||||
setEmptyNotIPAddress(),
|
||||
generateConditions("StringEquals", "s3:prefix", "Asia/"),
|
||||
generateConditions("StringEquals", "s3:max-keys", "100"),
|
||||
generateConditions("StringNotEquals", "s3:prefix", "Asia/"),
|
||||
@@ -439,7 +449,7 @@ func TestIsValidConditions(t *testing.T) {
|
||||
"please validate your policy document", "s3:max-keys", getObjectActionSet)
|
||||
testCases := []struct {
|
||||
inputActions set.StringSet
|
||||
inputCondition map[string]map[string]set.StringSet
|
||||
inputCondition policy.ConditionMap
|
||||
// expected result.
|
||||
expectedErr error
|
||||
// flag indicating whether test should pass.
|
||||
@@ -482,7 +492,13 @@ func TestIsValidConditions(t *testing.T) {
|
||||
// Test case - 12.
|
||||
{roBucketActionSet, testConditions[11], nil, true},
|
||||
// Test case - 13.
|
||||
{getObjectActionSet, testConditions[11], maxKeysConditionErr, false},
|
||||
{roBucketActionSet, testConditions[12], nil, true},
|
||||
// Test case - 11.
|
||||
{roBucketActionSet, testConditions[13], nil, true},
|
||||
// Test case - 12.
|
||||
{roBucketActionSet, testConditions[14], nil, true},
|
||||
// Test case - 13.
|
||||
{getObjectActionSet, testConditions[15], maxKeysConditionErr, false},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
actualErr := isValidConditions(testCase.inputActions, testCase.inputCondition)
|
||||
@@ -504,26 +520,26 @@ func TestIsValidConditions(t *testing.T) {
|
||||
// Tests validate Policy Action and Resource fields.
|
||||
func TestCheckbucketPolicyResources(t *testing.T) {
|
||||
// constructing policy statement without invalidPrefixActions (check bucket-policy-parser.go).
|
||||
setValidPrefixActions := func(statements []policyStatement) []policyStatement {
|
||||
setValidPrefixActions := func(statements []policy.Statement) []policy.Statement {
|
||||
statements[0].Actions = set.CreateStringSet([]string{"s3:DeleteObject", "s3:PutObject"}...)
|
||||
return statements
|
||||
}
|
||||
// contracting policy statement with recursive resources.
|
||||
// should result in ErrMalformedPolicy
|
||||
setRecurseResource := func(statements []policyStatement) []policyStatement {
|
||||
setRecurseResource := func(statements []policy.Statement) []policy.Statement {
|
||||
statements[0].Resources = set.CreateStringSet([]string{"arn:aws:s3:::minio-bucket/Asia/*", "arn:aws:s3:::minio-bucket/Asia/India/*"}...)
|
||||
return statements
|
||||
}
|
||||
|
||||
// constructing policy statement with lexically close characters.
|
||||
// should not result in ErrMalformedPolicy
|
||||
setResourceLexical := func(statements []policyStatement) []policyStatement {
|
||||
setResourceLexical := func(statements []policy.Statement) []policy.Statement {
|
||||
statements[0].Resources = set.CreateStringSet([]string{"arn:aws:s3:::minio-bucket/op*", "arn:aws:s3:::minio-bucket/oo*"}...)
|
||||
return statements
|
||||
}
|
||||
|
||||
// List of bucketPolicy used for tests.
|
||||
bucketAccessPolicies := []bucketPolicy{
|
||||
bucketAccessPolicies := []policy.BucketAccessPolicy{
|
||||
// bucketPolicy - 1.
|
||||
// Contains valid read only policy statement.
|
||||
{Version: "1.0", Statements: getReadOnlyStatement("minio-bucket", "")},
|
||||
@@ -554,7 +570,7 @@ func TestCheckbucketPolicyResources(t *testing.T) {
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
inputPolicy bucketPolicy
|
||||
inputPolicy policy.BucketAccessPolicy
|
||||
// expected results.
|
||||
apiErrCode APIErrorCode
|
||||
// Flag indicating whether the test should pass.
|
||||
@@ -585,7 +601,7 @@ func TestCheckbucketPolicyResources(t *testing.T) {
|
||||
{bucketAccessPolicies[6], ErrNone, true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
apiErrCode := checkBucketPolicyResources("minio-bucket", &testCase.inputPolicy)
|
||||
apiErrCode := checkBucketPolicyResources("minio-bucket", testCase.inputPolicy)
|
||||
if apiErrCode != ErrNone && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass, but failed with Errocode %v", i+1, apiErrCode)
|
||||
}
|
||||
@@ -604,39 +620,39 @@ func TestCheckbucketPolicyResources(t *testing.T) {
|
||||
// Tests validate parsing of BucketAccessPolicy.
|
||||
func TestParseBucketPolicy(t *testing.T) {
|
||||
// set Unsupported Actions.
|
||||
setUnsupportedActions := func(statements []policyStatement) []policyStatement {
|
||||
setUnsupportedActions := func(statements []policy.Statement) []policy.Statement {
|
||||
// "s3:DeleteEverything"" is an Unsupported Action.
|
||||
statements[0].Actions = set.CreateStringSet([]string{"s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:DeleteEverything"}...)
|
||||
return statements
|
||||
}
|
||||
// set unsupported Effect.
|
||||
setUnsupportedEffect := func(statements []policyStatement) []policyStatement {
|
||||
setUnsupportedEffect := func(statements []policy.Statement) []policy.Statement {
|
||||
// Effect "Don't allow" is Unsupported.
|
||||
statements[0].Effect = "DontAllow"
|
||||
return statements
|
||||
}
|
||||
// set unsupported principals.
|
||||
setUnsupportedPrincipals := func(statements []policyStatement) []policyStatement {
|
||||
setUnsupportedPrincipals := func(statements []policy.Statement) []policy.Statement {
|
||||
// "User1111"" is an Unsupported Principal.
|
||||
statements[0].Principal = map[string]interface{}{
|
||||
"AWS": []string{"*", "User1111"},
|
||||
statements[0].Principal = policy.User{
|
||||
AWS: set.CreateStringSet([]string{"*", "User1111"}...),
|
||||
}
|
||||
return statements
|
||||
}
|
||||
// set unsupported Resources.
|
||||
setUnsupportedResources := func(statements []policyStatement) []policyStatement {
|
||||
setUnsupportedResources := func(statements []policy.Statement) []policy.Statement {
|
||||
// "s3:DeleteEverything"" is an Unsupported Action.
|
||||
statements[0].Resources = set.CreateStringSet([]string{"my-resource"}...)
|
||||
return statements
|
||||
}
|
||||
// List of bucketPolicy used for test cases.
|
||||
bucketAccesPolicies := []bucketPolicy{
|
||||
bucketAccesPolicies := []policy.BucketAccessPolicy{
|
||||
// bucketPolicy - 0.
|
||||
// bucketPolicy statement empty.
|
||||
{Version: "1.0"},
|
||||
// bucketPolicy - 1.
|
||||
// bucketPolicy version empty.
|
||||
{Version: "", Statements: []policyStatement{}},
|
||||
{Version: "", Statements: []policy.Statement{}},
|
||||
// bucketPolicy - 2.
|
||||
// Readonly bucketPolicy.
|
||||
{Version: "1.0", Statements: getReadOnlyStatement("minio-bucket", "")},
|
||||
@@ -661,19 +677,19 @@ func TestParseBucketPolicy(t *testing.T) {
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
inputPolicy bucketPolicy
|
||||
inputPolicy policy.BucketAccessPolicy
|
||||
// expected results.
|
||||
expectedPolicy bucketPolicy
|
||||
expectedPolicy policy.BucketAccessPolicy
|
||||
err error
|
||||
// Flag indicating whether the test should pass.
|
||||
shouldPass bool
|
||||
}{
|
||||
// Test case - 1.
|
||||
// bucketPolicy statement empty.
|
||||
{bucketAccesPolicies[0], bucketPolicy{}, errors.New("Policy statement cannot be empty"), false},
|
||||
{bucketAccesPolicies[0], policy.BucketAccessPolicy{}, errors.New("Policy statement cannot be empty"), false},
|
||||
// Test case - 2.
|
||||
// bucketPolicy version empty.
|
||||
{bucketAccesPolicies[1], bucketPolicy{}, errors.New("Policy version cannot be empty"), false},
|
||||
{bucketAccesPolicies[1], policy.BucketAccessPolicy{}, errors.New("Policy version cannot be empty"), false},
|
||||
// Test case - 3.
|
||||
// Readonly bucketPolicy.
|
||||
{bucketAccesPolicies[2], bucketAccesPolicies[2], nil, true},
|
||||
@@ -704,8 +720,8 @@ func TestParseBucketPolicy(t *testing.T) {
|
||||
t.Fatalf("Test %d: Couldn't Marshal bucket policy %s", i+1, err)
|
||||
}
|
||||
|
||||
var actualAccessPolicy = &bucketPolicy{}
|
||||
err = parseBucketPolicy(&buffer, actualAccessPolicy)
|
||||
var actualAccessPolicy = policy.BucketAccessPolicy{}
|
||||
err = parseBucketPolicy(&buffer, &actualAccessPolicy)
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
|
||||
}
|
||||
@@ -720,7 +736,7 @@ func TestParseBucketPolicy(t *testing.T) {
|
||||
}
|
||||
// Test passes as expected, but the output values are verified for correctness here.
|
||||
if err == nil && testCase.shouldPass {
|
||||
if testCase.expectedPolicy.String() != actualAccessPolicy.String() {
|
||||
if !reflect.DeepEqual(testCase.expectedPolicy, actualAccessPolicy) {
|
||||
t.Errorf("Test %d: The expected statements from resource statement generator doesn't match the actual statements", i+1)
|
||||
}
|
||||
}
|
||||
@@ -737,8 +753,8 @@ func TestAWSRefererCondition(t *testing.T) {
|
||||
set.CreateStringSet("www.example.com",
|
||||
"http://www.example.com"))
|
||||
|
||||
requestConditionKeyMap := make(map[string]set.StringSet)
|
||||
requestConditionKeyMap["referer"] = set.CreateStringSet("www.example.com")
|
||||
requestConditionMap := make(policy.ConditionKeyMap)
|
||||
requestConditionMap["referer"] = set.CreateStringSet("www.example.com")
|
||||
|
||||
testCases := []struct {
|
||||
effect string
|
||||
@@ -768,20 +784,82 @@ func TestAWSRefererCondition(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions[test.conditionKey] = conditionsKeyMap
|
||||
|
||||
allowStatement := policyStatement{
|
||||
allowStatement := policy.Statement{
|
||||
Sid: "Testing AWS referer condition",
|
||||
Effect: test.effect,
|
||||
Principal: map[string]interface{}{
|
||||
"AWS": "*",
|
||||
Principal: policy.User{
|
||||
AWS: set.CreateStringSet("*"),
|
||||
},
|
||||
Resources: resource,
|
||||
Conditions: conditions,
|
||||
}
|
||||
|
||||
if result := bucketPolicyConditionMatch(requestConditionKeyMap, allowStatement); result != test.match {
|
||||
if result := bucketPolicyConditionMatch(requestConditionMap, allowStatement); result != test.match {
|
||||
t.Errorf("Test %d - Expected conditons to evaluate to %v but got %v",
|
||||
i+1, test.match, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWSSourceIPCondition(t *testing.T) {
|
||||
resource := set.CreateStringSet([]string{
|
||||
fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/"+"Asia"+"*"),
|
||||
}...)
|
||||
|
||||
conditionsKeyMap := make(policy.ConditionKeyMap)
|
||||
// Test both IPv4 and IPv6 addresses.
|
||||
conditionsKeyMap.Add("aws:SourceIp",
|
||||
set.CreateStringSet("54.240.143.0/24",
|
||||
"2001:DB8:1234:5678::/64"))
|
||||
|
||||
requestConditionMap := make(policy.ConditionKeyMap)
|
||||
requestConditionMap["ip"] = set.CreateStringSet("54.240.143.2")
|
||||
|
||||
testCases := []struct {
|
||||
effect string
|
||||
conditionKey string
|
||||
match bool
|
||||
}{
|
||||
{
|
||||
effect: "Allow",
|
||||
conditionKey: "IpAddress",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
effect: "Allow",
|
||||
conditionKey: "NotIpAddress",
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
effect: "Deny",
|
||||
conditionKey: "IpAddress",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
effect: "Deny",
|
||||
conditionKey: "NotIpAddress",
|
||||
match: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions[test.conditionKey] = conditionsKeyMap
|
||||
|
||||
allowStatement := policy.Statement{
|
||||
Sid: "Testing AWS referer condition",
|
||||
Effect: test.effect,
|
||||
Principal: policy.User{
|
||||
AWS: set.CreateStringSet("*"),
|
||||
},
|
||||
Resources: resource,
|
||||
Conditions: conditions,
|
||||
}
|
||||
|
||||
if result := bucketPolicyConditionMatch(requestConditionMap, allowStatement); result != test.match {
|
||||
t.Errorf("Test %d - Expected conditons to evaluate to %v but got %v",
|
||||
i+1, test.match, result)
|
||||
}
|
||||
|
||||
+50
-30
@@ -20,7 +20,11 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -41,7 +45,7 @@ type bucketPolicies struct {
|
||||
rwMutex *sync.RWMutex
|
||||
|
||||
// Collection of 'bucket' policies.
|
||||
bucketPolicyConfigs map[string]*bucketPolicy
|
||||
bucketPolicyConfigs map[string]policy.BucketAccessPolicy
|
||||
}
|
||||
|
||||
// Represent a policy change
|
||||
@@ -51,11 +55,11 @@ type policyChange struct {
|
||||
IsRemove bool
|
||||
|
||||
// represents the new policy for the bucket
|
||||
BktPolicy *bucketPolicy
|
||||
BktPolicy policy.BucketAccessPolicy
|
||||
}
|
||||
|
||||
// Fetch bucket policy for a given bucket.
|
||||
func (bp bucketPolicies) GetBucketPolicy(bucket string) *bucketPolicy {
|
||||
func (bp bucketPolicies) GetBucketPolicy(bucket string) policy.BucketAccessPolicy {
|
||||
bp.rwMutex.RLock()
|
||||
defer bp.rwMutex.RUnlock()
|
||||
return bp.bucketPolicyConfigs[bucket]
|
||||
@@ -70,7 +74,7 @@ func (bp *bucketPolicies) SetBucketPolicy(bucket string, pCh policyChange) error
|
||||
if pCh.IsRemove {
|
||||
delete(bp.bucketPolicyConfigs, bucket)
|
||||
} else {
|
||||
if pCh.BktPolicy == nil {
|
||||
if reflect.DeepEqual(pCh.BktPolicy, emptyBucketPolicy) {
|
||||
return errInvalidArgument
|
||||
}
|
||||
bp.bucketPolicyConfigs[bucket] = pCh.BktPolicy
|
||||
@@ -79,19 +83,19 @@ func (bp *bucketPolicies) SetBucketPolicy(bucket string, pCh policyChange) error
|
||||
}
|
||||
|
||||
// Loads all bucket policies from persistent layer.
|
||||
func loadAllBucketPolicies(objAPI ObjectLayer) (policies map[string]*bucketPolicy, err error) {
|
||||
func loadAllBucketPolicies(objAPI ObjectLayer) (policies map[string]policy.BucketAccessPolicy, err error) {
|
||||
// List buckets to proceed loading all notification configuration.
|
||||
buckets, err := objAPI.ListBuckets()
|
||||
errorIf(err, "Unable to list buckets.")
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to list buckets.")
|
||||
return nil, errorCause(err)
|
||||
}
|
||||
|
||||
policies = make(map[string]*bucketPolicy)
|
||||
policies = make(map[string]policy.BucketAccessPolicy)
|
||||
var pErrs []error
|
||||
// Loads bucket policy.
|
||||
for _, bucket := range buckets {
|
||||
policy, pErr := readBucketPolicy(bucket.Name, objAPI)
|
||||
bp, pErr := readBucketPolicy(bucket.Name, objAPI)
|
||||
if pErr != nil {
|
||||
// net.Dial fails for rpc client or any
|
||||
// other unexpected errors during net.Dial.
|
||||
@@ -103,7 +107,7 @@ func loadAllBucketPolicies(objAPI ObjectLayer) (policies map[string]*bucketPolic
|
||||
// Continue to load other bucket policies if possible.
|
||||
continue
|
||||
}
|
||||
policies[bucket.Name] = policy
|
||||
policies[bucket.Name] = bp
|
||||
}
|
||||
|
||||
// Look for any errors occurred while reading bucket policies.
|
||||
@@ -146,7 +150,9 @@ func readBucketPolicyJSON(bucket string, objAPI ObjectLayer) (bucketPolicyReader
|
||||
|
||||
// Acquire a read lock on policy config before reading.
|
||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, policyPath)
|
||||
objLock.RLock()
|
||||
if err = objLock.GetRLock(globalOperationTimeout); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer objLock.RUnlock()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
@@ -164,21 +170,20 @@ func readBucketPolicyJSON(bucket string, objAPI ObjectLayer) (bucketPolicyReader
|
||||
|
||||
// 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) {
|
||||
func readBucketPolicy(bucket string, objAPI ObjectLayer) (policy.BucketAccessPolicy, error) {
|
||||
// Read bucket policy JSON.
|
||||
bucketPolicyReader, err := readBucketPolicyJSON(bucket, objAPI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return emptyBucketPolicy, err
|
||||
}
|
||||
|
||||
// Parse the saved policy.
|
||||
var policy = &bucketPolicy{}
|
||||
err = parseBucketPolicy(bucketPolicyReader, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var bp policy.BucketAccessPolicy
|
||||
if err = parseBucketPolicy(bucketPolicyReader, &bp); err != nil {
|
||||
return emptyBucketPolicy, err
|
||||
|
||||
}
|
||||
return policy, nil
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
// removeBucketPolicy - removes any previously written bucket policy. Returns BucketPolicyNotFound
|
||||
@@ -187,9 +192,12 @@ func removeBucketPolicy(bucket string, objAPI ObjectLayer) error {
|
||||
policyPath := pathJoin(bucketConfigPrefix, bucket, bucketPolicyConfig)
|
||||
// Acquire a write lock on policy config before modifying.
|
||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, policyPath)
|
||||
objLock.Lock()
|
||||
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer objLock.Unlock()
|
||||
if err := objAPI.DeleteObject(minioMetaBucket, policyPath); err != nil {
|
||||
err := objAPI.DeleteObject(minioMetaBucket, policyPath)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to remove bucket-policy on bucket %s.", bucket)
|
||||
err = errorCause(err)
|
||||
if _, ok := err.(ObjectNotFound); ok {
|
||||
@@ -201,18 +209,27 @@ func removeBucketPolicy(bucket string, objAPI ObjectLayer) error {
|
||||
}
|
||||
|
||||
// writeBucketPolicy - save a bucket policy that is assumed to be validated.
|
||||
func writeBucketPolicy(bucket string, objAPI ObjectLayer, bpy *bucketPolicy) error {
|
||||
func writeBucketPolicy(bucket string, objAPI ObjectLayer, bpy policy.BucketAccessPolicy) error {
|
||||
buf, err := json.Marshal(bpy)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to marshal bucket policy '%v' to JSON", *bpy)
|
||||
errorIf(err, "Unable to marshal bucket policy '%#v' to JSON", bpy)
|
||||
return err
|
||||
}
|
||||
policyPath := pathJoin(bucketConfigPrefix, bucket, bucketPolicyConfig)
|
||||
// Acquire a write lock on policy config before modifying.
|
||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, policyPath)
|
||||
objLock.Lock()
|
||||
if err = objLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer objLock.Unlock()
|
||||
if _, err := objAPI.PutObject(minioMetaBucket, policyPath, int64(len(buf)), bytes.NewReader(buf), nil, ""); err != nil {
|
||||
|
||||
hashReader, err := hash.NewReader(bytes.NewReader(buf), int64(len(buf)), "", getSHA256Hash(buf))
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to set policy for the bucket %s", bucket)
|
||||
return errorCause(err)
|
||||
}
|
||||
|
||||
if _, err = objAPI.PutObject(minioMetaBucket, policyPath, hashReader, nil); err != nil {
|
||||
errorIf(err, "Unable to set policy for the bucket %s", bucket)
|
||||
return errorCause(err)
|
||||
}
|
||||
@@ -221,26 +238,28 @@ func writeBucketPolicy(bucket string, objAPI ObjectLayer, bpy *bucketPolicy) err
|
||||
|
||||
func parseAndPersistBucketPolicy(bucket string, policyBytes []byte, objAPI ObjectLayer) APIErrorCode {
|
||||
// Parse bucket policy.
|
||||
var policy = &bucketPolicy{}
|
||||
err := parseBucketPolicy(bytes.NewReader(policyBytes), policy)
|
||||
var bktPolicy policy.BucketAccessPolicy
|
||||
err := parseBucketPolicy(bytes.NewReader(policyBytes), &bktPolicy)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to parse bucket policy.")
|
||||
return ErrInvalidPolicyDocument
|
||||
}
|
||||
|
||||
// Parse check bucket policy.
|
||||
if s3Error := checkBucketPolicyResources(bucket, policy); s3Error != ErrNone {
|
||||
if s3Error := checkBucketPolicyResources(bucket, bktPolicy); s3Error != ErrNone {
|
||||
return s3Error
|
||||
}
|
||||
|
||||
// Acquire a write lock on bucket before modifying its configuration.
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.Lock()
|
||||
if bucketLock.GetLock(globalOperationTimeout) != nil {
|
||||
return ErrOperationTimedOut
|
||||
}
|
||||
// Release lock after notifying peers
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
// Save bucket policy.
|
||||
if err = persistAndNotifyBucketPolicyChange(bucket, policyChange{false, policy}, objAPI); err != nil {
|
||||
if err = persistAndNotifyBucketPolicyChange(bucket, policyChange{false, bktPolicy}, objAPI); err != nil {
|
||||
switch err.(type) {
|
||||
case BucketNameInvalid:
|
||||
return ErrInvalidBucketName
|
||||
@@ -259,11 +278,12 @@ func parseAndPersistBucketPolicy(bucket string, policyBytes []byte, objAPI Objec
|
||||
// change. In-memory state is updated in response to the notification.
|
||||
func persistAndNotifyBucketPolicyChange(bucket string, pCh policyChange, objAPI ObjectLayer) error {
|
||||
if pCh.IsRemove {
|
||||
if err := removeBucketPolicy(bucket, objAPI); err != nil {
|
||||
err := removeBucketPolicy(bucket, objAPI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if pCh.BktPolicy == nil {
|
||||
if reflect.DeepEqual(pCh.BktPolicy, emptyBucketPolicy) {
|
||||
return errInvalidArgument
|
||||
}
|
||||
if err := writeBucketPolicy(bucket, objAPI, pCh.BktPolicy); err != nil {
|
||||
|
||||
+71
-122
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2015, 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.
|
||||
@@ -17,150 +17,99 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// createCertsPath create certs path.
|
||||
func createCertsPath() error {
|
||||
certsPath, err := getCertsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(certsPath, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
rootCAsPath := filepath.Join(certsPath, globalMinioCertsCADir)
|
||||
return os.MkdirAll(rootCAsPath, 0700)
|
||||
}
|
||||
|
||||
// getCertsPath get certs path.
|
||||
func getCertsPath() (string, error) {
|
||||
var certsPath string
|
||||
configDir, err := getConfigPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
certsPath = filepath.Join(configDir, globalMinioCertsDir)
|
||||
return certsPath, nil
|
||||
}
|
||||
|
||||
// mustGetCertsPath must get certs path.
|
||||
func mustGetCertsPath() string {
|
||||
certsPath, err := getCertsPath()
|
||||
fatalIf(err, "Failed to get certificate path.")
|
||||
return certsPath
|
||||
}
|
||||
|
||||
// mustGetCertFile must get cert file.
|
||||
func mustGetCertFile() string {
|
||||
return filepath.Join(mustGetCertsPath(), globalMinioCertFile)
|
||||
}
|
||||
|
||||
// mustGetKeyFile must get key file.
|
||||
func mustGetKeyFile() string {
|
||||
return filepath.Join(mustGetCertsPath(), globalMinioKeyFile)
|
||||
}
|
||||
|
||||
// mustGetCAFiles must get the list of the CA certificates stored in minio config dir
|
||||
func mustGetCAFiles() (caCerts []string) {
|
||||
CAsDir := filepath.Join(mustGetCertsPath(), globalMinioCertsCADir)
|
||||
caFiles, _ := ioutil.ReadDir(CAsDir)
|
||||
for _, cert := range caFiles {
|
||||
caCerts = append(caCerts, filepath.Join(CAsDir, cert.Name()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// mustGetSystemCertPool returns empty cert pool in case of error (windows)
|
||||
func mustGetSystemCertPool() *x509.CertPool {
|
||||
pool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
return x509.NewCertPool()
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
// isCertFileExists verifies if cert file exists, returns true if
|
||||
// found, false otherwise.
|
||||
func isCertFileExists() bool {
|
||||
st, e := os.Stat(filepath.Join(mustGetCertsPath(), globalMinioCertFile))
|
||||
// If file exists and is regular return true.
|
||||
if e == nil && st.Mode().IsRegular() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isKeyFileExists verifies if key file exists, returns true if found,
|
||||
// false otherwise.
|
||||
func isKeyFileExists() bool {
|
||||
st, e := os.Stat(filepath.Join(mustGetCertsPath(), globalMinioKeyFile))
|
||||
// If file exists and is regular return true.
|
||||
if e == nil && st.Mode().IsRegular() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isSSL - returns true with both cert and key exists.
|
||||
func isSSL() bool {
|
||||
return isCertFileExists() && isKeyFileExists()
|
||||
}
|
||||
|
||||
// Reads certificated file and returns a list of parsed certificates.
|
||||
func readCertificateChain() ([]*x509.Certificate, error) {
|
||||
bytes, err := ioutil.ReadFile(mustGetCertFile())
|
||||
if err != nil {
|
||||
func parsePublicCertFile(certFile string) (x509Certs []*x509.Certificate, err error) {
|
||||
// Read certificate file.
|
||||
var data []byte
|
||||
if data, err = ioutil.ReadFile(certFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Proceed to parse the certificates.
|
||||
return parseCertificateChain(bytes)
|
||||
}
|
||||
|
||||
// Parses certificate chain, returns a list of parsed certificates.
|
||||
func parseCertificateChain(bytes []byte) ([]*x509.Certificate, error) {
|
||||
var certs []*x509.Certificate
|
||||
var block *pem.Block
|
||||
current := bytes
|
||||
|
||||
// Parse all certs in the chain.
|
||||
current := data
|
||||
for len(current) > 0 {
|
||||
block, current = pem.Decode(current)
|
||||
if block == nil {
|
||||
return nil, errors.New("Could not PEM block")
|
||||
var pemBlock *pem.Block
|
||||
if pemBlock, current = pem.Decode(current); pemBlock == nil {
|
||||
return nil, fmt.Errorf("Could not read PEM block from file %s", certFile)
|
||||
}
|
||||
// Parse the decoded certificate.
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
|
||||
var x509Cert *x509.Certificate
|
||||
if x509Cert, err = x509.ParseCertificate(pemBlock.Bytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certs = append(certs, cert)
|
||||
|
||||
x509Certs = append(x509Certs, x509Cert)
|
||||
}
|
||||
return certs, nil
|
||||
|
||||
if len(x509Certs) == 0 {
|
||||
return nil, fmt.Errorf("Empty public certificate file %s", certFile)
|
||||
}
|
||||
|
||||
return x509Certs, nil
|
||||
}
|
||||
|
||||
// loadRootCAs fetches CA files provided in minio config and adds them to globalRootCAs
|
||||
// Currently under Windows, there is no way to load system + user CAs at the same time
|
||||
func loadRootCAs() {
|
||||
caFiles := mustGetCAFiles()
|
||||
if len(caFiles) == 0 {
|
||||
return
|
||||
func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
|
||||
// Get all CA file names.
|
||||
var caFiles []string
|
||||
fis, err := ioutil.ReadDir(certsCAsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Get system cert pool, and empty cert pool under Windows because it is not supported
|
||||
globalRootCAs = mustGetSystemCertPool()
|
||||
for _, fi := range fis {
|
||||
caFiles = append(caFiles, filepath.Join(certsCAsDir, fi.Name()))
|
||||
}
|
||||
|
||||
if len(caFiles) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rootCAs, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
// In some systems like Windows, system cert pool is not supported.
|
||||
// Hence we create a new cert pool.
|
||||
rootCAs = x509.NewCertPool()
|
||||
}
|
||||
|
||||
// Load custom root CAs for client requests
|
||||
for _, caFile := range caFiles {
|
||||
caCert, err := ioutil.ReadFile(caFile)
|
||||
if err != nil {
|
||||
fatalIf(err, "Unable to load a CA file")
|
||||
return nil, err
|
||||
}
|
||||
globalRootCAs.AppendCertsFromPEM(caCert)
|
||||
|
||||
rootCAs.AppendCertsFromPEM(caCert)
|
||||
}
|
||||
|
||||
return rootCAs, nil
|
||||
}
|
||||
|
||||
func getSSLConfig() (x509Certs []*x509.Certificate, rootCAs *x509.CertPool, tlsCert *tls.Certificate, secureConn bool, err error) {
|
||||
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
|
||||
return nil, nil, nil, false, nil
|
||||
}
|
||||
|
||||
if x509Certs, err = parsePublicCertFile(getPublicCertFile()); err != nil {
|
||||
return nil, nil, nil, false, err
|
||||
}
|
||||
|
||||
var cert tls.Certificate
|
||||
if cert, err = tls.LoadX509KeyPair(getPublicCertFile(), getPrivateKeyFile()); err != nil {
|
||||
return nil, nil, nil, false, err
|
||||
}
|
||||
|
||||
tlsCert = &cert
|
||||
|
||||
if rootCAs, err = getRootCAs(getCADir()); err != nil {
|
||||
return nil, nil, nil, false, err
|
||||
}
|
||||
|
||||
secureConn = true
|
||||
return x509Certs, rootCAs, tlsCert, secureConn, nil
|
||||
}
|
||||
|
||||
+203
-55
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2015, 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.
|
||||
@@ -17,52 +17,86 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Make sure we have a valid certs path.
|
||||
func TestGetCertsPath(t *testing.T) {
|
||||
path, err := getCertsPath()
|
||||
func createTempFile(prefix, content string) (tempFile string, err error) {
|
||||
var tmpfile *os.File
|
||||
|
||||
if tmpfile, err = ioutil.TempFile("", prefix); err != nil {
|
||||
return tempFile, err
|
||||
}
|
||||
|
||||
if _, err = tmpfile.Write([]byte(content)); err != nil {
|
||||
return tempFile, err
|
||||
}
|
||||
|
||||
if err = tmpfile.Close(); err != nil {
|
||||
return tempFile, err
|
||||
}
|
||||
|
||||
tempFile = tmpfile.Name()
|
||||
return tempFile, err
|
||||
}
|
||||
|
||||
func TestParsePublicCertFile(t *testing.T) {
|
||||
tempFile1, err := createTempFile("public-cert-file", "")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if path == "" {
|
||||
t.Errorf("expected path to not be an empty string, got: '%s'", path)
|
||||
}
|
||||
// Ensure it contains some sort of path separator.
|
||||
if !strings.ContainsRune(path, os.PathSeparator) {
|
||||
t.Errorf("expected path to contain file separator")
|
||||
}
|
||||
// It should also be an absolute path.
|
||||
if !filepath.IsAbs(path) {
|
||||
t.Errorf("expected path to be an absolute path")
|
||||
t.Fatalf("Unable to create temporary file. %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile1)
|
||||
|
||||
// This will error if something goes wrong, so just call it.
|
||||
mustGetCertsPath()
|
||||
}
|
||||
|
||||
// Ensure that the certificate and key file getters contain their respective
|
||||
// file name and endings.
|
||||
func TestGetFiles(t *testing.T) {
|
||||
file := mustGetCertFile()
|
||||
if !strings.Contains(file, globalMinioCertFile) {
|
||||
t.Errorf("CertFile does not contain %s", globalMinioCertFile)
|
||||
tempFile2, err := createTempFile("public-cert-file",
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
|
||||
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
|
||||
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
|
||||
AQkBFg50ZXN0c0BtaW5pby5pbzAeFw0xNjEwMTQxMTM0MjJaFw0xNzEwMTQxMTM0
|
||||
MjJaMH8xCzAJBgNVBAYTAlpZMQ4wDAYDVQQIEwVNaW5pbzERMA8GA1UEBxMISW50
|
||||
ZXJuZXQxDjAMBgNVBA-some-junk-Q4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF
|
||||
TWluaW8xHTAbBgkqhkiG9w0BCQEWDnRlc3RzQG1pbmlvLmlvMIGfMA0GCSqGSIb3
|
||||
DQEBAQUAA4GNADCBiQKBgQDwNUYB/Sj79WsUE8qnXzzh2glSzWxUE79sCOpQYK83
|
||||
HWkrl5WxlG8ZxDR1IQV9Ex/lzigJu8G+KXahon6a+3n5GhNrYRe5kIXHQHz0qvv4
|
||||
aMulqlnYpvSfC83aaO9GVBtwXS/O4Nykd7QBg4nZlazVmsGk7POOjhpjGShRsqpU
|
||||
JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
|
||||
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
|
||||
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
|
||||
M9ofSEt/bdRD
|
||||
-----END CERTIFICATE-----`)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create temporary file. %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile2)
|
||||
|
||||
file = mustGetKeyFile()
|
||||
if !strings.Contains(file, globalMinioKeyFile) {
|
||||
t.Errorf("KeyFile does not contain %s", globalMinioKeyFile)
|
||||
tempFile3, err := createTempFile("public-cert-file",
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
|
||||
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
|
||||
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
|
||||
AQkBFg50ZXN0c0BtaW5pby5pbzAeFw0xNjEwMTQxMTM0MjJaFw0xNzEwMTQxMTM0
|
||||
MjJaMH8xCzAJBgNVBAYTAlpZMQ4wDAYDVQQIEwVNaW5pbzERMA8GA1UEBxMISW50
|
||||
ZXJuZXQxDjAMBgNVBAabababababaQ4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF
|
||||
TWluaW8xHTAbBgkqhkiG9w0BCQEWDnRlc3RzQG1pbmlvLmlvMIGfMA0GCSqGSIb3
|
||||
DQEBAQUAA4GNADCBiQKBgQDwNUYB/Sj79WsUE8qnXzzh2glSzWxUE79sCOpQYK83
|
||||
HWkrl5WxlG8ZxDR1IQV9Ex/lzigJu8G+KXahon6a+3n5GhNrYRe5kIXHQHz0qvv4
|
||||
aMulqlnYpvSfC83aaO9GVBtwXS/O4Nykd7QBg4nZlazVmsGk7POOjhpjGShRsqpU
|
||||
JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
|
||||
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
|
||||
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
|
||||
M9ofSEt/bdRD
|
||||
-----END CERTIFICATE-----`)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create temporary file. %v", err)
|
||||
}
|
||||
}
|
||||
defer os.Remove(tempFile3)
|
||||
|
||||
// Parses .crt file contents
|
||||
func TestParseCertificateChain(t *testing.T) {
|
||||
// given
|
||||
cert := `-----BEGIN CERTIFICATE-----
|
||||
tempFile4, err := createTempFile("public-cert-file",
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
|
||||
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
|
||||
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
|
||||
@@ -77,35 +111,149 @@ JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
|
||||
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
|
||||
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
|
||||
M9ofSEt/bdRD
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
// when
|
||||
certs, err := parseCertificateChain([]byte(cert))
|
||||
|
||||
// then
|
||||
-----END CERTIFICATE-----`)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not parse certificate: %s", err)
|
||||
t.Fatalf("Unable to create temporary file. %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile4)
|
||||
|
||||
tempFile5, err := createTempFile("public-cert-file",
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
|
||||
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
|
||||
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
|
||||
AQkBFg50ZXN0c0BtaW5pby5pbzAeFw0xNjEwMTQxMTM0MjJaFw0xNzEwMTQxMTM0
|
||||
MjJaMH8xCzAJBgNVBAYTAlpZMQ4wDAYDVQQIEwVNaW5pbzERMA8GA1UEBxMISW50
|
||||
ZXJuZXQxDjAMBgNVBAoTBU1pbmlvMQ4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF
|
||||
TWluaW8xHTAbBgkqhkiG9w0BCQEWDnRlc3RzQG1pbmlvLmlvMIGfMA0GCSqGSIb3
|
||||
DQEBAQUAA4GNADCBiQKBgQDwNUYB/Sj79WsUE8qnXzzh2glSzWxUE79sCOpQYK83
|
||||
HWkrl5WxlG8ZxDR1IQV9Ex/lzigJu8G+KXahon6a+3n5GhNrYRe5kIXHQHz0qvv4
|
||||
aMulqlnYpvSfC83aaO9GVBtwXS/O4Nykd7QBg4nZlazVmsGk7POOjhpjGShRsqpU
|
||||
JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
|
||||
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
|
||||
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
|
||||
M9ofSEt/bdRD
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
|
||||
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
|
||||
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
|
||||
AQkBFg50ZXN0c0BtaW5pby5pbzAeFw0xNjEwMTQxMTM0MjJaFw0xNzEwMTQxMTM0
|
||||
MjJaMH8xCzAJBgNVBAYTAlpZMQ4wDAYDVQQIEwVNaW5pbzERMA8GA1UEBxMISW50
|
||||
ZXJuZXQxDjAMBgNVBAoTBU1pbmlvMQ4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF
|
||||
TWluaW8xHTAbBgkqhkiG9w0BCQEWDnRlc3RzQG1pbmlvLmlvMIGfMA0GCSqGSIb3
|
||||
DQEBAQUAA4GNADCBiQKBgQDwNUYB/Sj79WsUE8qnXzzh2glSzWxUE79sCOpQYK83
|
||||
HWkrl5WxlG8ZxDR1IQV9Ex/lzigJu8G+KXahon6a+3n5GhNrYRe5kIXHQHz0qvv4
|
||||
aMulqlnYpvSfC83aaO9GVBtwXS/O4Nykd7QBg4nZlazVmsGk7POOjhpjGShRsqpU
|
||||
JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
|
||||
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
|
||||
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
|
||||
M9ofSEt/bdRD
|
||||
-----END CERTIFICATE-----`)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create temporary file. %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile5)
|
||||
|
||||
nonexistentErr := fmt.Errorf("open nonexistent-file: no such file or directory")
|
||||
if runtime.GOOS == "windows" {
|
||||
// Below concatenation is done to get rid of goline error
|
||||
// "error strings should not be capitalized or end with punctuation or a newline"
|
||||
nonexistentErr = fmt.Errorf("open nonexistent-file:" + " The system cannot find the file specified.")
|
||||
}
|
||||
|
||||
if len(certs) != 1 {
|
||||
t.Fatalf("Expected number of certificates in chain was 1, actual: %d", len(certs))
|
||||
testCases := []struct {
|
||||
certFile string
|
||||
expectedResultLen int
|
||||
expectedErr error
|
||||
}{
|
||||
{"nonexistent-file", 0, nonexistentErr},
|
||||
{tempFile1, 0, fmt.Errorf("Empty public certificate file %s", tempFile1)},
|
||||
{tempFile2, 0, fmt.Errorf("Could not read PEM block from file %s", tempFile2)},
|
||||
{tempFile3, 0, fmt.Errorf("asn1: structure error: sequence tag mismatch")},
|
||||
{tempFile4, 1, nil},
|
||||
{tempFile5, 2, nil},
|
||||
}
|
||||
|
||||
if certs[0].Subject.CommonName != "Minio" {
|
||||
t.Fatalf("Expected Subject.CommonName was Minio, actual: %s", certs[0].Subject.CommonName)
|
||||
for _, testCase := range testCases {
|
||||
certs, err := parsePublicCertFile(testCase.certFile)
|
||||
|
||||
if testCase.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
} else if testCase.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
|
||||
if len(certs) != testCase.expectedResultLen {
|
||||
t.Fatalf("certs: expected = %v, got = %v", testCase.expectedResultLen, len(certs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parses invalid .crt file contents and returns error
|
||||
func TestParseInvalidCertificateChain(t *testing.T) {
|
||||
// given
|
||||
cert := `This is now valid certificate`
|
||||
func TestGetRootCAs(t *testing.T) {
|
||||
emptydir, err := ioutil.TempDir("", "test-get-root-cas")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable create temp directory. %v", emptydir)
|
||||
}
|
||||
defer os.RemoveAll(emptydir)
|
||||
|
||||
// when
|
||||
_, err := parseCertificateChain([]byte(cert))
|
||||
dir1, err := ioutil.TempDir("", "test-get-root-cas")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable create temp directory. %v", dir1)
|
||||
}
|
||||
defer os.RemoveAll(dir1)
|
||||
if err = os.Mkdir(filepath.Join(dir1, "empty-dir"), 0755); err != nil {
|
||||
t.Fatalf("Unable create empty dir. %v", err)
|
||||
}
|
||||
|
||||
// then
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error but none occurred")
|
||||
dir2, err := ioutil.TempDir("", "test-get-root-cas")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable create temp directory. %v", dir2)
|
||||
}
|
||||
defer os.RemoveAll(dir2)
|
||||
if err = ioutil.WriteFile(filepath.Join(dir2, "empty-file"), []byte{}, 0644); err != nil {
|
||||
t.Fatalf("Unable create test file. %v", err)
|
||||
}
|
||||
|
||||
nonexistentErr := fmt.Errorf("open nonexistent-dir: no such file or directory")
|
||||
if runtime.GOOS == "windows" {
|
||||
// Below concatenation is done to get rid of goline error
|
||||
// "error strings should not be capitalized or end with punctuation or a newline"
|
||||
nonexistentErr = fmt.Errorf("open nonexistent-dir:" + " The system cannot find the file specified.")
|
||||
}
|
||||
|
||||
err1 := fmt.Errorf("read %s: is a directory", filepath.Join(dir1, "empty-dir"))
|
||||
if runtime.GOOS == "windows" {
|
||||
// Below concatenation is done to get rid of goline error
|
||||
// "error strings should not be capitalized or end with punctuation or a newline"
|
||||
err1 = fmt.Errorf("read %s:"+" The handle is invalid.", filepath.Join(dir1, "empty-dir"))
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
certCAsDir string
|
||||
expectedErr error
|
||||
}{
|
||||
{"nonexistent-dir", nonexistentErr},
|
||||
{dir1, err1},
|
||||
{emptydir, nil},
|
||||
{dir2, nil},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
_, err := getRootCAs(testCase.certCAsDir)
|
||||
|
||||
if testCase.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
} else if testCase.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +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 cmd
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// checkPortAvailability - check if given port is already in use.
|
||||
// Note: The check method tries to listen on given port and closes it.
|
||||
// It is possible to have a disconnected client in this tiny window of time.
|
||||
func checkPortAvailability(port string) error {
|
||||
network := [3]string{"tcp", "tcp4", "tcp6"}
|
||||
for _, n := range network {
|
||||
l, err := net.Listen(n, net.JoinHostPort("", port))
|
||||
if err != nil {
|
||||
if isAddrInUse(err) {
|
||||
// Return error if another process is listening on the
|
||||
// same port.
|
||||
return err
|
||||
}
|
||||
// Ignore any other error (ex. EAFNOSUPPORT)
|
||||
continue
|
||||
}
|
||||
|
||||
// look for error so we don't have dangling connection
|
||||
if err = l.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return true if err is "address already in use" error.
|
||||
// syscall.EADDRINUSE is available on all OSes.
|
||||
func isAddrInUse(err error) bool {
|
||||
if opErr, ok := err.(*net.OpError); ok {
|
||||
if sysErr, ok := opErr.Err.(*os.SyscallError); ok {
|
||||
if errno, ok := sysErr.Err.(syscall.Errno); ok {
|
||||
if errno == syscall.EADDRINUSE {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests for port availability logic written for server startup sequence.
|
||||
func TestCheckPortAvailability(t *testing.T) {
|
||||
tests := []struct {
|
||||
port string
|
||||
}{
|
||||
{getFreePort()},
|
||||
{getFreePort()},
|
||||
}
|
||||
for _, test := range tests {
|
||||
// This test should pass if the ports are available
|
||||
err := checkPortAvailability(test.port)
|
||||
if err != nil {
|
||||
t.Fatalf("checkPortAvailability test failed for port: %s. Error: %v", test.port, err)
|
||||
}
|
||||
|
||||
// Now use the ports and check again
|
||||
ln, err := net.Listen("tcp", net.JoinHostPort("", test.port))
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
err = checkPortAvailability(test.port)
|
||||
|
||||
// Skip if the os is windows due to https://github.com/golang/go/issues/7598
|
||||
if err == nil && runtime.GOOS != globalWindowsOSName {
|
||||
t.Fatalf("checkPortAvailability should fail for port: %s. Error: %v", test.port, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user