mirror of
https://github.com/pgsty/minio.git
synced 2026-08-01 08:30:47 +03:00
Compare commits
583 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a3d43273f | |||
| 4dec50ba51 | |||
| f2bffe6086 | |||
| 8e2f64aea4 | |||
| c054e633fd | |||
| e86dfcf41e | |||
| 3b9dbd748b | |||
| 3d1bb8f439 | |||
| bb8a425d49 | |||
| 0e745fdb05 | |||
| 51d7749c3e | |||
| 76d56c6ff2 | |||
| b41bfcbf2f | |||
| 341171f326 | |||
| a3592228f5 | |||
| 5526ac13d2 | |||
| 9606cb9bcd | |||
| 7829ccea2c | |||
| 723153951c | |||
| 64dc2a2e7f | |||
| fdab984c8d | |||
| 70fd38818e | |||
| ef0a108dde | |||
| 0b225269e1 | |||
| fe62688683 | |||
| fadb71351c | |||
| 402af93da2 | |||
| d1bb8a5b21 | |||
| 97c1289659 | |||
| 8274ac2e5a | |||
| 758aa21b9c | |||
| 0dc5662f9b | |||
| 82cd38e959 | |||
| e0cf4ee9fc | |||
| 2a920e568c | |||
| 9c7f75d1e7 | |||
| 7e46055a15 | |||
| 0c125f3596 | |||
| a1f3bf57c7 | |||
| 0188cd0b84 | |||
| b23605a2b5 | |||
| 8c2985a9f5 | |||
| d28fb5fe23 | |||
| 62c0612eac | |||
| d63ce9d60d | |||
| 5a44c34fd7 | |||
| 064c51162d | |||
| 90c20a8c11 | |||
| e783d77c3d | |||
| e887fea485 | |||
| de5d5ff241 | |||
| 2e0742e309 | |||
| cc0d5b6fe0 | |||
| 2db51e9d61 | |||
| 69fd196471 | |||
| 1494af485e | |||
| c1d70f1f9e | |||
| 9baf599c91 | |||
| 8d090a20ce | |||
| 5b86dd7659 | |||
| 4b05b6a6c1 | |||
| 851d05161a | |||
| cf9ba7b88f | |||
| 50dae0ab04 | |||
| 14cefd352c | |||
| f239fcac67 | |||
| dcc3463e48 | |||
| 7850d17f48 | |||
| f503ac3db8 | |||
| 65f71ce0c5 | |||
| ad19bf0ec1 | |||
| f0067babe0 | |||
| 6b5fce826b | |||
| 8b3cb3a0de | |||
| 2f7358a8a6 | |||
| 77248bd6e8 | |||
| 3054b74260 | |||
| 7d42d09da8 | |||
| 95ddf061ab | |||
| 5fe72cf205 | |||
| 1e3d80552f | |||
| f253dfc922 | |||
| 1f9e38e3cd | |||
| 530ed67b59 | |||
| a2b6f0524d | |||
| 091d80666a | |||
| efbf7dbc0f | |||
| 043ddbd834 | |||
| 04f90bd463 | |||
| 9212e11b90 | |||
| 79bab6b561 | |||
| 7e5a78985d | |||
| b0b919a1d6 | |||
| 6c2fb19ed7 | |||
| f248089523 | |||
| f85d94288d | |||
| 55cb55675c | |||
| 7e076577de | |||
| 5d118141cd | |||
| 646ff2c64d | |||
| 0add96f655 | |||
| 303f216150 | |||
| a7b5b8e63f | |||
| 5730d40478 | |||
| 0eaf684777 | |||
| a0635dcdd9 | |||
| 41f4f2806d | |||
| 2a972ef1fd | |||
| c1e953b368 | |||
| 18728a0b59 | |||
| cef26fd6ea | |||
| f67c930731 | |||
| 3589a58179 | |||
| e8155abc18 | |||
| 2e8360120d | |||
| 02b191222c | |||
| b699795901 | |||
| 86d31e99d5 | |||
| 897d78d113 | |||
| 1f706e067d | |||
| 5cc9e4e214 | |||
| 27a5b61f40 | |||
| aeac902747 | |||
| aaf7803831 | |||
| 8cc163e51a | |||
| 2d38046a5a | |||
| cbb6b48b94 | |||
| d0636d633d | |||
| 41187fc2ef | |||
| 204ec2c6c0 | |||
| 3bce5db6d1 | |||
| 35d438e0ff | |||
| b090c7112e | |||
| af6109f89a | |||
| 3f27734c22 | |||
| cdf1373f8e | |||
| 45240f158d | |||
| 0bd6b67ca5 | |||
| 8c84df5e74 | |||
| dc3bafb194 | |||
| 3b69b4ada4 | |||
| 0610527868 | |||
| 0fcfb5df3b | |||
| 623e0f9243 | |||
| e5cd35aad0 | |||
| 5cd629adca | |||
| 0fddf3fe17 | |||
| ac6ff67546 | |||
| 126865e8df | |||
| 1c82b81408 | |||
| 749a94f6c9 | |||
| e9647b5f12 | |||
| bef72f26db | |||
| ede4dd0f9c | |||
| bfc59b7d50 | |||
| a162198623 | |||
| de468f92ec | |||
| d676e660c9 | |||
| aa7079fc7b | |||
| bdff0848ed | |||
| 0793237d94 | |||
| 2a95eabb8a | |||
| bc8720406d | |||
| ae80f8ca35 | |||
| 5102a5877e | |||
| 3341fe9b28 | |||
| c0c8a8430e | |||
| b0c180b77c | |||
| 778b870b77 | |||
| cb415ef12e | |||
| 6266328a85 | |||
| 63b3f1dcfd | |||
| 5ff1203fc0 | |||
| c3ab8bbd51 | |||
| a4a55bf134 | |||
| ec35330ebb | |||
| ca1b1921c4 | |||
| 4c21d6d09d | |||
| 5ec7734d88 | |||
| 2c837128ef | |||
| 282044d454 | |||
| 8e53064bb4 | |||
| f55093cdd6 | |||
| ddf3245677 | |||
| 169c72cdab | |||
| 8c767218a4 | |||
| a51bb1d728 | |||
| 26b7d79a85 | |||
| 01cbacd803 | |||
| 7bde27032d | |||
| 44ae7a037b | |||
| a35341448f | |||
| d6dfcd0ba7 | |||
| 8ddf52021a | |||
| 8a028a9efb | |||
| 0540863663 | |||
| 4cfbdb1bf0 | |||
| 1ec86dac2c | |||
| 7a8b8cd0a1 | |||
| a8a3e95835 | |||
| 1ad5fb8f76 | |||
| 52b55afce0 | |||
| 355f06cfea | |||
| d2557bb538 | |||
| 8d4365d23c | |||
| 55ae7cac42 | |||
| 48ac34919f | |||
| d64c3fd464 | |||
| e5dd917c37 | |||
| eb5f782c74 | |||
| 734e779b19 | |||
| 3f2b4d9dc2 | |||
| cd1c2db864 | |||
| 48aa5f2199 | |||
| bcb822c390 | |||
| 285a94d2c0 | |||
| 64899e5197 | |||
| 812554087f | |||
| 57bc08cc7e | |||
| 0e3907072c | |||
| 8e8f6f90a4 | |||
| 4c1a11aae6 | |||
| ae936a0147 | |||
| d484157d67 | |||
| 796fe165c7 | |||
| 3ac39ff107 | |||
| b6b9e88e47 | |||
| 59366d8f4c | |||
| 748dc80047 | |||
| 6dcfa7b046 | |||
| a854e8cc5c | |||
| 76f6533f8d | |||
| 4db2b03312 | |||
| 5291db60c6 | |||
| 2e1f66c37d | |||
| c88720ea2c | |||
| ce7d5eddbc | |||
| 0d3a9c9438 | |||
| 293ba00249 | |||
| d0be09fdd3 | |||
| 1e52759c3c | |||
| 9add048f3c | |||
| 936a916e78 | |||
| 57146fbbb8 | |||
| 3d02f7471e | |||
| b2d9a46cbb | |||
| 42286cba70 | |||
| ed2fdd90b0 | |||
| cb1200a66d | |||
| a3a310cde8 | |||
| f625392211 | |||
| e8990e42c2 | |||
| ff9fc22c72 | |||
| 41c089a7e0 | |||
| 5725f3c809 | |||
| f4830162a4 | |||
| e10934a88e | |||
| a07751f61b | |||
| 0766e903e3 | |||
| 75dddfb2ae | |||
| 9b82e64a11 | |||
| bdf8738076 | |||
| d4bea5fbf8 | |||
| 17efaaa902 | |||
| 78ae696749 | |||
| 3fa95f5263 | |||
| ba5bb4a127 | |||
| 409b4ddecb | |||
| ad779a8ba4 | |||
| 7d757033f2 | |||
| 393c504de0 | |||
| 6143c87c3a | |||
| 8559e89494 | |||
| 468ca4ccda | |||
| 4ee2136b28 | |||
| 80d83220ad | |||
| fb10c09da7 | |||
| 2f136e92f7 | |||
| 50d25ca94a | |||
| e1aad066c6 | |||
| 1ea1dba528 | |||
| c41bf26712 | |||
| 8c0942bf0d | |||
| 7f38f46e20 | |||
| 466a2e01f1 | |||
| d31b38aac8 | |||
| 365f80efa3 | |||
| f51d34cedd | |||
| 129ebbd685 | |||
| e2743d05e8 | |||
| de1c7d33eb | |||
| afc3102488 | |||
| 23c88ffb1d | |||
| ed4fe689b4 | |||
| 4ab57f7d60 | |||
| 276282957e | |||
| 71632b375e | |||
| 6f3bd76754 | |||
| f2765d98a8 | |||
| 61598ed02f | |||
| 1b9db9ee6c | |||
| 51f3d4e0ca | |||
| d13e6e7156 | |||
| d32f3288f8 | |||
| c5b6cb2420 | |||
| 2eb6fa3fce | |||
| acc393ba8b | |||
| 37551a2ad3 | |||
| c6ac3fa6db | |||
| 18b3871705 | |||
| 73ddb5be75 | |||
| 002c5bf7dd | |||
| 1cf1532ca3 | |||
| 70a1231f02 | |||
| 82fd907933 | |||
| f39a6f8df7 | |||
| f6013c46ea | |||
| da069a18c4 | |||
| 5108ba6eb1 | |||
| 1213bf9fa1 | |||
| b00ac40c35 | |||
| fb95c1fad3 | |||
| aa1d769b1e | |||
| 611c892f8f | |||
| 67bba270a0 | |||
| de21126f7e | |||
| ae311aa53b | |||
| 9b79760dcf | |||
| 116b5607d7 | |||
| 614c770b5d | |||
| c493ab5d0d | |||
| db2fdbf38d | |||
| 89f65333fb | |||
| 22511dc4c7 | |||
| 1947ae198e | |||
| 2e4ab71303 | |||
| 445dc22118 | |||
| ffc2b3c304 | |||
| 967c2b2940 | |||
| b466f27705 | |||
| a4a0ea605b | |||
| 5e8de786b3 | |||
| feb337098d | |||
| c87f259820 | |||
| 7278b90fe1 | |||
| 41a5b3908b | |||
| 3fb0b5e455 | |||
| ba8bdec077 | |||
| 27cc8a6529 | |||
| 302ec27fa2 | |||
| 5f679d9d1e | |||
| 51bb613fdf | |||
| d65101a8c8 | |||
| 6dc8323684 | |||
| 616a257bfa | |||
| 3487b3c095 | |||
| 1f51af6f37 | |||
| b1e2b7dea2 | |||
| 34e9ad24aa | |||
| b2293c2bf4 | |||
| 553fdb9211 | |||
| cae4782973 | |||
| 3550660163 | |||
| a4771265cf | |||
| a9e778f460 | |||
| ee6645f421 | |||
| a97230dd56 | |||
| 1e393c6c5b | |||
| b38b9fea79 | |||
| 6d84e84b3c | |||
| a00a5c6e7e | |||
| ed43d5e02b | |||
| 293d246f95 | |||
| 63c65b4635 | |||
| 64b0976e1b | |||
| b48b2e7f7c | |||
| 584813e214 | |||
| 3a980eac1a | |||
| 5a4b074ca0 | |||
| f76d975304 | |||
| 6015a7a3cd | |||
| b83b87a7f6 | |||
| 5b95f097d4 | |||
| e4240aa58f | |||
| f5dfa895a5 | |||
| 50c328ff19 | |||
| b8405ca172 | |||
| f6d9e73548 | |||
| 7ae5470395 | |||
| 9fdb69563d | |||
| 00d0558131 | |||
| 13e4618309 | |||
| 2f05aacbf2 | |||
| dc36594ef4 | |||
| 537568f9f9 | |||
| 62b4fd6964 | |||
| 7d6ed50fc2 | |||
| af85acf388 | |||
| a4fef436c8 | |||
| 404364ba73 | |||
| 3c1ef3fee2 | |||
| a0865122a7 | |||
| 824c8a39f1 | |||
| 71b9341fc7 | |||
| b69a97aed4 | |||
| 2da34e4668 | |||
| 4bc923e63b | |||
| 4214da65af | |||
| 596fe65e84 | |||
| 39865c0d2e | |||
| 1760687c83 | |||
| 9472299308 | |||
| 8828fd1e5c | |||
| 7de206cb85 | |||
| 498ce1e9bb | |||
| 5b29cefd40 | |||
| e03ebfd13b | |||
| 74c23a3544 | |||
| 025054fb36 | |||
| 7264cd2ab3 | |||
| 0e4e9c4bc1 | |||
| 8099396ff0 | |||
| d267696110 | |||
| 43539a0c86 | |||
| 9e45d138cc | |||
| ee8605e333 | |||
| e4958f9757 | |||
| b62774d32f | |||
| d6e0f3ab33 | |||
| 3ff0a56e62 | |||
| 50431e91a6 | |||
| d4745c7d6a | |||
| adbcafefad | |||
| 86e5d71519 | |||
| 72748d2073 | |||
| 49141eb3e4 | |||
| d1fa1d9352 | |||
| 26e2c4bf4d | |||
| b044336329 | |||
| e99cb05516 | |||
| 409e09c1e5 | |||
| c314a98c1a | |||
| 5f0ca64346 | |||
| 0db3218d5d | |||
| eec41c369c | |||
| b66c3bf35e | |||
| f733120d3d | |||
| 6627388dc3 | |||
| 9d41414fb5 | |||
| 722abe2d0f | |||
| 04a5b25929 | |||
| 90ea494338 | |||
| a8fdd04e62 | |||
| 76c511c9fe | |||
| 75320f70d0 | |||
| 88e1c04259 | |||
| a205aca6d2 | |||
| 3f51dd4fd4 | |||
| 56b7df90e1 | |||
| a56d5ef415 | |||
| 937d68202d | |||
| bf563afb80 | |||
| 091c1e8456 | |||
| 751fa972f5 | |||
| 434423de89 | |||
| a20ccb1e83 | |||
| 0625c050e6 | |||
| 0b74f5624e | |||
| c06b9abc15 | |||
| a5d31d4254 | |||
| 20ca65c793 | |||
| 0b4bbe6d9e | |||
| 48d3be36da | |||
| 5133ea50bd | |||
| 17868ccd7f | |||
| ba5805e60a | |||
| ca097de96c | |||
| 658a595d7a | |||
| 247e835d7b | |||
| 46680788f9 | |||
| ad40036cba | |||
| 82fbe908a3 | |||
| f145e1042f | |||
| 9dccbd6478 | |||
| 82113b747c | |||
| a5959789d5 | |||
| 6988ed9257 | |||
| 321aefa026 | |||
| dd417e5476 | |||
| da3a53376c | |||
| e4d89d8156 | |||
| d0e854afb7 | |||
| 6f1811ee4d | |||
| bba5468368 | |||
| 7ae40eb1bb | |||
| ad8e27a963 | |||
| ac7a7cec20 | |||
| afd59c45dc | |||
| 3bf3d18f1f | |||
| 8102a4712a | |||
| 2393a3a0be | |||
| d006129fb5 | |||
| 7caa82f32f | |||
| 286de4de2c | |||
| ba7a55c321 | |||
| e05aa762a9 | |||
| 0c27d8e5b1 | |||
| 443ec37765 | |||
| d5df8b8b8d | |||
| ac2933c799 | |||
| 84afec9ae0 | |||
| 27c50a70cc | |||
| a978975eea | |||
| dc45ea3946 | |||
| 9eb56f0676 | |||
| 10a010c1ad | |||
| a9935f886c | |||
| 4e34e03dd4 | |||
| 4d1b3d5e9a | |||
| 7066ce5160 | |||
| 39df425b2a | |||
| 984903cce1 | |||
| 8deddb82f4 | |||
| a1a667ae5d | |||
| 41b35cff7b | |||
| eed756777b | |||
| 2ac10209cc | |||
| 5bd6b0b510 | |||
| 1813e9c070 | |||
| f87a19a15c | |||
| 5fffd558d0 | |||
| b51bef85e6 | |||
| d0e5470050 | |||
| 90987df9b4 | |||
| ad1abc4486 | |||
| 4333e529e6 | |||
| 9685f88b84 | |||
| 5f80edf232 | |||
| 42254b5c4d | |||
| 00c697393a | |||
| 55032ffdf9 | |||
| 8bce699dae | |||
| 57f35c2bcc | |||
| cab6805f09 | |||
| 8c85815106 | |||
| becc814531 | |||
| 9bd9441107 | |||
| f3784d1087 | |||
| 91588209fa | |||
| 5c33b68318 | |||
| 45b3d3e21f | |||
| 141a44bfbf | |||
| c7bf471c9e | |||
| c302875774 | |||
| b76f3f1d62 | |||
| ada0f82b9a | |||
| a98a7fb1ad | |||
| bf8a9702a4 | |||
| 6e372f83b4 | |||
| e9fba04b36 | |||
| d63d17012d | |||
| 444d1f8a65 | |||
| 560c3bd153 | |||
| 4cf73caf02 | |||
| 1284ecc6f2 | |||
| cb1116725b | |||
| c3d0a3d51e | |||
| e0f8fed011 | |||
| 6bc17a3aea | |||
| ff9a6b00cc | |||
| af907a35a9 | |||
| b47d722d8e | |||
| 33633fd15d | |||
| b2ec7da9f9 | |||
| be002ac01e | |||
| 149c6ca094 | |||
| 8457af5708 | |||
| caa35f68fa | |||
| 30b0b4deba | |||
| 01a439f95b | |||
| 3c48537f20 | |||
| 188bb92d8a | |||
| 6b3fc03701 |
@@ -0,0 +1,13 @@
|
||||
## Expected behaviour
|
||||
|
||||
## Actual behaviour
|
||||
|
||||
## Steps to reproduce the behaviour
|
||||
|
||||
## Minio version
|
||||
- (paste output of `minio version`)
|
||||
|
||||
## System information
|
||||
|
||||
|
||||
|
||||
@@ -13,3 +13,6 @@ build
|
||||
vendor/**/*.js
|
||||
vendor/**/*.json
|
||||
release
|
||||
.DS_Store
|
||||
*.syso
|
||||
coverage.txt
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"numFilesToCheck": 10,
|
||||
"requiredOrgs": ["minio"]
|
||||
}
|
||||
+5
-6
@@ -13,12 +13,11 @@ env:
|
||||
- ARCH=i686
|
||||
|
||||
script:
|
||||
- make test
|
||||
- make test GOFLAGS="-race"
|
||||
- make coverage
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
go:
|
||||
- 1.6
|
||||
|
||||
notifications:
|
||||
slack:
|
||||
secure: K9tsn5MvrCAxuEZTxn+m3Kq1K2NG2xMEJFSv/sTp+RQBW7TslPHzv859GsIvrm8mU1y1btOU9RlOzqrRUczI5cJpE8IL1oljPZbXrIXgetE0kbsw0Wpy99g27UQ2VGp933WDu8tfj7zU4cZv+BI0RltNLwqYO6GWXmcWP0IueCU=
|
||||
- 1.6.2
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
## Using aws-sdk-go with Minio
|
||||
|
||||
aws-sdk-go is the official AWS SDK for the Go programming language. This document covers
|
||||
how to use aws-sdk-go with Minio server.
|
||||
|
||||
### Install AWS SDK S3 service
|
||||
|
||||
```sh
|
||||
$ go get github.com/aws/aws-sdk-go/service/s3
|
||||
```
|
||||
|
||||
### List all buckets on Minio
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
newSession := session.New()
|
||||
s3Config := &aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials("<YOUR-ACCESS-KEY-ID>", "<YOUR-SECRET-ACCESS-KEY", ""),
|
||||
Endpoint: aws.String("http://localhost:9000"),
|
||||
Region: aws.String("us-east-1"),
|
||||
DisableSSL: aws.Bool(true),
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
}
|
||||
// Create an S3 service object in the default region.
|
||||
s3Client := s3.New(newSession, s3Config)
|
||||
|
||||
cparams := &s3.CreateBucketInput{
|
||||
Bucket: aws.String("newbucket"), // Required
|
||||
}
|
||||
_, err := s3Client.CreateBucket(cparams)
|
||||
if err != nil {
|
||||
// Message from an error.
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var lparams *s3.ListBucketsInput
|
||||
// Call the ListBuckets() Operation
|
||||
resp, err := s3Client.ListBuckets(lparams)
|
||||
if err != nil {
|
||||
// Message from an error.
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Pretty-print the response data.
|
||||
fmt.Println(resp)
|
||||
}
|
||||
```
|
||||
|
||||
Populate your AccessKeyId and SecretAccessKey credentials and run the program as shown below.
|
||||
|
||||
```sh
|
||||
$ go run aws-sdk-minio.go
|
||||
{
|
||||
Buckets: [{
|
||||
CreationDate: 2015-10-22 01:46:04 +0000 UTC,
|
||||
Name: "newbucket"
|
||||
}],
|
||||
Owner: {
|
||||
DisplayName: "minio",
|
||||
ID: "minio"
|
||||
}
|
||||
}
|
||||
```
|
||||
+3
-3
@@ -3,11 +3,11 @@
|
||||
If you do not have a working Golang environment setup please follow [Golang Installation Guide](./INSTALLGO.md).
|
||||
|
||||
### Setup your Minio Github Repository
|
||||
Fork [Minio upstream](https://github.com/minio/minio/fork) source repository to your own personal repository. Copy the URL and pass it to ``go get`` command. Go uses git to clone a copy into your project workspace folder.
|
||||
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).
|
||||
```sh
|
||||
$ mkdir -p $GOPATH/src/github.com/minio
|
||||
$ cd $GOPATH/src/github.com/minio
|
||||
$ git clone https://github.com/$USER_ID/minio
|
||||
$ git clone <paste saved URL for personal forked minio repo>
|
||||
$ cd minio
|
||||
```
|
||||
|
||||
@@ -51,7 +51,7 @@ Building Libraries
|
||||
- Push to the branch (git push origin my-new-feature)
|
||||
- Create new Pull Request
|
||||
|
||||
* If you have additional dependencies for ``Minio``, ``Minio`` manages its depedencies using [govendor](https://github.com/kardianos/govendor)
|
||||
* 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
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
## Setting up Proxy using Caddy.
|
||||
|
||||
Please download [Caddy Server](https://caddyserver.com/download)
|
||||
|
||||
Create a caddy configuration file as below, change the ip addresses according to your local
|
||||
minio and DNS configuration.
|
||||
|
||||
```bash
|
||||
$ ./minio --address localhost:9000 server <your_export_dir>
|
||||
```
|
||||
|
||||
```bash
|
||||
your.public.com {
|
||||
proxy / localhost:9000 {
|
||||
proxy_header Host {host}
|
||||
proxy_header X-Real-IP {remote}
|
||||
proxy_header X-Forwarded-Proto {scheme}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
$ ./caddy
|
||||
Activating privacy features... done.
|
||||
your.public.com:443
|
||||
your.public.com:80
|
||||
```
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
FROM golang:1.6
|
||||
|
||||
RUN mkdir -p /go/src/app
|
||||
WORKDIR /go/src/app
|
||||
|
||||
COPY . /go/src/app
|
||||
RUN go-wrapper download
|
||||
RUN go-wrapper install
|
||||
|
||||
ENV ALLOW_CONTAINER_ROOT=1
|
||||
RUN mkdir -p /export/docker && cp /go/src/app/docs/Docker.md /export/docker/
|
||||
|
||||
EXPOSE 9000
|
||||
ENTRYPOINT ["go-wrapper", "run", "server"]
|
||||
CMD ["/export"]
|
||||
@@ -1,84 +0,0 @@
|
||||
## Ubuntu (Kylin) 14.04
|
||||
### Build Dependencies
|
||||
This installation document assumes Ubuntu 14.04+ on x86-64 platform.
|
||||
|
||||
##### Install Git, GCC
|
||||
```sh
|
||||
$ sudo apt-get install git build-essential
|
||||
```
|
||||
|
||||
##### Install Go 1.6+
|
||||
|
||||
Download Go 1.6+ from [https://golang.org/dl/](https://golang.org/dl/).
|
||||
|
||||
```sh
|
||||
$ wget https://storage.googleapis.com/golang/go1.6.linux-amd64.tar.gz
|
||||
$ mkdir -p ${HOME}/bin/
|
||||
$ mkdir -p ${HOME}/go/
|
||||
$ tar -C ${HOME}/bin/ -xzf go1.6.linux-amd64.tar.gz
|
||||
```
|
||||
##### Setup GOROOT and GOPATH
|
||||
|
||||
Add the following exports to your ``~/.bashrc``. Environment variable GOROOT specifies the location of your golang binaries
|
||||
and GOPATH specifies the location of your project workspace.
|
||||
|
||||
```sh
|
||||
export GOROOT=${HOME}/bin/go
|
||||
export GOPATH=${HOME}/go
|
||||
export PATH=${HOME}/bin/go/bin:${GOPATH}/bin:$PATH
|
||||
```
|
||||
##### Source the new enviornment
|
||||
|
||||
```sh
|
||||
$ source ~/.bashrc
|
||||
```
|
||||
|
||||
##### Testing it all
|
||||
|
||||
```sh
|
||||
$ go env
|
||||
```
|
||||
|
||||
## OS X (Yosemite) 10.10
|
||||
### Build Dependencies
|
||||
This installation document assumes OS X Yosemite 10.10+ on x86-64 platform.
|
||||
|
||||
##### Install brew
|
||||
```sh
|
||||
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||
```
|
||||
|
||||
##### Install Git, Python
|
||||
```sh
|
||||
$ brew install git python
|
||||
```
|
||||
|
||||
##### Install Go 1.5+
|
||||
|
||||
Install golang binaries using `brew`
|
||||
|
||||
```sh
|
||||
$ brew install go
|
||||
$ mkdir -p $HOME/go
|
||||
```
|
||||
|
||||
##### Setup GOROOT and GOPATH
|
||||
|
||||
Add the following exports to your ``~/.bash_profile``. Environment variable GOROOT specifies the location of your golang binaries
|
||||
and GOPATH specifies the location of your project workspace.
|
||||
|
||||
```sh
|
||||
export GOPATH=${HOME}/go
|
||||
export GOVERSION=$(brew list go | head -n 1 | cut -d '/' -f 6)
|
||||
export GOROOT=$(brew --prefix)/Cellar/go/${GOVERSION}/libexec
|
||||
export PATH=${GOPATH}/bin:$PATH
|
||||
```
|
||||
##### Source the new enviornment
|
||||
```sh
|
||||
$ source ~/.bash_profile
|
||||
```
|
||||
##### Testing it all
|
||||
|
||||
```sh
|
||||
$ go env
|
||||
```
|
||||
@@ -1,8 +1,6 @@
|
||||
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
||||
DOCKER_BIN := $(shell which docker)
|
||||
PWD := $(shell pwd)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
DOCKER_LDFLAGS := '$(LDFLAGS) -extldflags "-static"'
|
||||
BUILD_LDFLAGS := '$(LDFLAGS)'
|
||||
TAG := latest
|
||||
|
||||
@@ -62,15 +60,12 @@ checks:
|
||||
@(env bash $(PWD)/buildscripts/checkdeps.sh)
|
||||
@(env bash $(PWD)/buildscripts/checkgopath.sh)
|
||||
|
||||
checkdocker:
|
||||
@echo "Checking if docker is installed.. "
|
||||
@if [ -z ${DOCKER_BIN} ]; then echo "Docker not installed, cannot build docker image. Please install 'sudo apt-get install docker.io'" && exit 1; else echo "Docker installed at ${DOCKER_BIN}."; fi;
|
||||
|
||||
getdeps: checks
|
||||
@go get -u github.com/golang/lint/golint && echo "Installed golint:"
|
||||
@go get -u github.com/fzipp/gocyclo && echo "Installed gocyclo:"
|
||||
@go get -u github.com/remyoudompheng/go-misc/deadcode && echo "Installed deadcode:"
|
||||
@go get -u github.com/client9/misspell/cmd/misspell && echo "Installed misspell:"
|
||||
@go get -u github.com/gordonklaus/ineffassign && echo "Installed ineffassign:"
|
||||
|
||||
verifiers: vet fmt lint cyclo spelling
|
||||
|
||||
@@ -91,6 +86,10 @@ lint:
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/golint *.go
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/golint github.com/minio/minio/pkg...
|
||||
|
||||
ineffassign:
|
||||
@echo "Running $@:"
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/ineffassign .
|
||||
|
||||
cyclo:
|
||||
@echo "Running $@:"
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/gocyclo -over 65 *.go
|
||||
@@ -102,14 +101,18 @@ deadcode:
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/deadcode
|
||||
|
||||
spelling:
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell *.go
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell pkg/**/*
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell -error *.go
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell -error pkg/**/*
|
||||
|
||||
test: build
|
||||
@echo "Running all minio testing:"
|
||||
@GO15VENDOREXPERIMENT=1 go test $(GOFLAGS) .
|
||||
@GO15VENDOREXPERIMENT=1 go test $(GOFLAGS) github.com/minio/minio/pkg...
|
||||
|
||||
coverage: build
|
||||
@echo "Running all coverage for minio:"
|
||||
@GO15VENDOREXPERIMENT=1 ./buildscripts/go-coverage.sh
|
||||
|
||||
gomake-all: build
|
||||
@echo "Installing minio:"
|
||||
@GO15VENDOREXPERIMENT=1 go build --ldflags $(BUILD_LDFLAGS) -o $(GOPATH)/bin/minio
|
||||
@@ -128,13 +131,6 @@ pkg-list:
|
||||
|
||||
install: gomake-all
|
||||
|
||||
dockerimage: checkdocker getdeps verifiers $(UI_ASSETS)
|
||||
@echo "Building docker image:" minio:$(TAG)
|
||||
@GO15VENDOREXPERIMENT=1 GOOS=linux GOARCH=amd64 go build --ldflags $(DOCKER_LDFLAGS) -o docker/minio.dockerimage
|
||||
@cd docker; mkdir -p export; sudo docker build --rm --tag=minio/minio:$(TAG) .
|
||||
@rmdir docker/export
|
||||
@rm docker/minio.dockerimage
|
||||
|
||||
release: verifiers
|
||||
@MINIO_RELEASE=RELEASE ./buildscripts/build.sh
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
## Multipart backend format
|
||||
|
||||
When multipart upload is used for objects, below meta-data/staging files are created
|
||||
|
||||
- New multipart upload call creates file ```EXPORT_DIR/.minio/BUCKET/PATH/TO/OBJECT/UPLOAD_ID.uploadid```
|
||||
- Put object part call creates file ```EXPORT_DIR/.minio/BUCKET/PATH/TO/OBJECT/UPLOAD_ID.PART_NUMBER.MD5SUM_STRING```
|
||||
- Abort multipart call removes all files matching ```EXPORT_DIR/.minio/BUCKET/PATH/TO/OBJECT/UPLOAD_ID.*```
|
||||
- Complete multipart call does
|
||||
1. Create a staging file ```EXPORT_DIR/.minio/BUCKET/PATH/TO/OBJECT/UPLOAD_ID.complete.TEMP_NAME``` then rename to ```EXPORT_DIR/.minio/BUCKET/PATH/TO/OBJECT/UPLOAD_ID.complete```
|
||||
2. Rename staging file ```EXPORT_DIR/.minio/BUCKET/PATH/TO/OBJECT/UPLOAD_ID.complete``` to ```EXPORT_DIR/BUCKET/PATH/TO/OBJECT```
|
||||
@@ -1,232 +1,191 @@
|
||||
## Minio [](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
# Minio Quickstart Guide [](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://codecov.io/gh/minio/minio)
|
||||
|
||||
Minio is an object storage server compatible with Amazon S3 and licensed under [Apache license 2.0](./LICENSE).
|
||||
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.
|
||||
|
||||
## Description
|
||||
## 1. Download
|
||||
|
||||
Minio is an open source object storage server released under Apache License V2. It is compatible with Amazon S3 cloud storage service. Minio follows a minimalist design philosophy.
|
||||
Minio is light enough to be bundled with the application stack. It sits on the side of NodeJS, Redis, MySQL and the likes. Unlike databases, Minio stores objects such as photos, videos, log files, backups, container / VM images and so on. Minio is best suited for storing blobs of information ranging from KBs to 5 TBs each. In a simplistic sense, it is like a FTP server with a simple get / put API over HTTP.
|
||||
Minio server is light enough to be bundled with the application stack, similar to NodeJS, Redis and MySQL.
|
||||
|
||||
Minio currently implements two backends
|
||||
| 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|
|
||||
|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|
|
||||
|
||||
- Filesystem (FS) - is available and ready for general purpose use. This version of the Minio binary is built using Filesystem storage backend for magnetic and solid state disks.
|
||||
- ErasureCoded (XL) - is work in progress and not ready for general purpose use.
|
||||
### Install from Source
|
||||
|
||||
## Minio Client
|
||||
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).
|
||||
|
||||
[Minio Client (mc)](https://github.com/minio/mc#minio-client-mc-) provides a modern alternative to Unix commands like ``ls``, ``cat``, ``cp``, ``sync``, and ``diff``. It supports POSIX compatible filesystems and Amazon S3 compatible cloud storage systems. It is entirely written in Golang.
|
||||
|
||||
## Amazon S3 Compatible Client Libraries
|
||||
- [Golang Library](https://github.com/minio/minio-go)
|
||||
- [Java Library](https://github.com/minio/minio-java)
|
||||
- [Nodejs Library](https://github.com/minio/minio-js)
|
||||
- [Python Library](https://github.com/minio/minio-py)
|
||||
- [.Net Library](https://github.com/minio/minio-dotnet)
|
||||
```sh
|
||||
|
||||
### Install [](https://travis-ci.org/minio/minio)[](https://ci.appveyor.com/project/harshavardhana/minio-qxbjq/branch/master)
|
||||
|
||||
#### GNU/Linux
|
||||
|
||||
Download ``minio`` for:
|
||||
|
||||
- ``64-bit Intel`` from https://dl.minio.io/server/minio/release/linux-amd64/minio
|
||||
- ``32-bit Intel`` from https://dl.minio.io/server/minio/release/linux-386/minio
|
||||
- ``32-bit ARM`` from https://dl.minio.io/server/minio/release/linux-arm/minio
|
||||
|
||||
~~~
|
||||
$ chmod +x minio
|
||||
$ ./minio --help
|
||||
~~~
|
||||
|
||||
#### OS X
|
||||
|
||||
Download ``minio`` from https://dl.minio.io/server/minio/release/darwin-amd64/minio
|
||||
|
||||
~~~
|
||||
$ chmod 755 minio
|
||||
$ ./minio --help
|
||||
~~~
|
||||
|
||||
#### Microsoft Windows
|
||||
|
||||
Download ``minio`` for:
|
||||
|
||||
- ``64-bit`` from https://dl.minio.io/server/minio/release/windows-amd64/minio.exe
|
||||
- ``32-bit`` from https://dl.minio.io/server/minio/release/windows-386/minio.exe
|
||||
|
||||
~~~
|
||||
C:\Users\Username\Downloads> minio.exe --help
|
||||
~~~
|
||||
|
||||
#### FreeBSD
|
||||
|
||||
Download ``minio`` from https://dl.minio.io/server/minio/release/freebsd-amd64/minio
|
||||
|
||||
~~~
|
||||
$ chmod 755 minio
|
||||
$ ./minio --help
|
||||
~~~
|
||||
|
||||
#### Docker container
|
||||
|
||||
Download ``minio`` for docker.
|
||||
|
||||
~~~
|
||||
$ docker pull minio/minio
|
||||
~~~
|
||||
|
||||
Read more here on [How to configure data volume containers for Minio?](./Docker.md)
|
||||
|
||||
#### Source
|
||||
<blockquote>
|
||||
NOTE: Source installation is intended for only developers and advanced users. For general use, please download official releases from https://minio.io/download.
|
||||
</blockquote>
|
||||
|
||||
If you do not have a working Golang environment, please follow [Install Golang](./INSTALLGO.md).
|
||||
|
||||
~~~
|
||||
$ go get -d github.com/minio/minio
|
||||
$ cd $GOPATH/src/github.com/minio/minio
|
||||
$ make
|
||||
~~~
|
||||
|
||||
### How to use Minio?
|
||||
|
||||
Start minio server.
|
||||
|
||||
~~~
|
||||
$ minio server ~/Photos
|
||||
|
||||
AccessKey: WLGDGYAQYIGI833EV05A SecretKey: BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF Region: us-east-1
|
||||
|
||||
Minio Object Storage:
|
||||
http://127.0.0.1:9000
|
||||
http://10.1.10.177:9000
|
||||
|
||||
Minio Browser:
|
||||
http://127.0.0.1:9000
|
||||
http://10.1.10.177:9000
|
||||
|
||||
To configure Minio Client:
|
||||
$ wget https://dl.minio.io/client/mc/release/darwin-amd64/mc
|
||||
$ chmod 755 mc
|
||||
$ ./mc config host add myminio http://localhost:9000 WLGDGYAQYIGI833EV05A BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF
|
||||
~~~
|
||||
|
||||
#### How to use AWS CLI with Minio?
|
||||
|
||||
<blockquote>
|
||||
This section assumes that you have already installed aws-cli, if not please visit https://aws.amazon.com/cli/
|
||||
</blockquote>
|
||||
|
||||
To configure `aws-cli`, type `aws configure` and follow below steps.
|
||||
|
||||
```
|
||||
$ aws configure
|
||||
AWS Access Key ID [None]: YOUR_ACCESS_KEY_HERE
|
||||
AWS Secret Access Key [None]: YOUR_SECRET_KEY_HERE
|
||||
Default region name [None]: us-east-1
|
||||
Default output format [None]: ENTER
|
||||
```
|
||||
|
||||
Additionally enable `aws-cli` to use AWS Signature Version '4' for Minio server.
|
||||
## 2. Run Minio Server
|
||||
|
||||
|
||||
### GNU/Linux
|
||||
|
||||
```sh
|
||||
|
||||
$ chmod +x minio
|
||||
$ ./minio --help
|
||||
$ ./minio server ~/Photos
|
||||
|
||||
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
Region: us-east-1
|
||||
|
||||
Browser Access:
|
||||
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
|
||||
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
|
||||
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
|
||||
Object API (Amazon S3 compatible):
|
||||
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
|
||||
Java: https://docs.minio.io/docs/java-client-quickstart-guide
|
||||
Python: https://docs.minio.io/docs/python-client-quickstart-guide
|
||||
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
|
||||
|
||||
```
|
||||
$ aws configure set default.s3.signature_version s3v4
|
||||
```
|
||||
|
||||
To list your buckets.
|
||||
```
|
||||
$ aws --endpoint-url http://localhost:9000 s3 ls
|
||||
2016-01-07 16:38:23 testbucket
|
||||
```
|
||||
### OS X
|
||||
|
||||
To list contents inside bucket.
|
||||
```
|
||||
$ aws --endpoint-url http://localhost:9000 s3 ls s3://testbucket
|
||||
PRE test/
|
||||
2015-12-17 08:46:41 12232928 vim
|
||||
2016-01-07 16:38:23 32232928 emacs
|
||||
2015-12-09 08:05:24 138504 s3cmd
|
||||
```
|
||||
|
||||
#### How to use AWS SDK with Minio?
|
||||
```sh
|
||||
|
||||
Please follow the documentation here - [Using aws-sdk-go with Minio server](./AWS-SDK-GO.md)
|
||||
$ chmod 755 minio
|
||||
$ ./minio --help
|
||||
$ ./minio server ~/Photos
|
||||
|
||||
#### How to use s3cmd with Minio?
|
||||
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
Region: us-east-1
|
||||
|
||||
<blockquote>
|
||||
This section assumes that you have already installed s3cmd, if not please visit http://s3tools.org/s3cmd
|
||||
</blockquote>
|
||||
Browser Access:
|
||||
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
|
||||
Edit the following fields in your s3cmd configuration file `~/.s3cfg` .
|
||||
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
|
||||
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
|
||||
Object API (Amazon S3 compatible):
|
||||
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
|
||||
Java: https://docs.minio.io/docs/java-client-quickstart-guide
|
||||
Python: https://docs.minio.io/docs/python-client-quickstart-guide
|
||||
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
|
||||
|
||||
```
|
||||
host_base = localhost:9000
|
||||
host_bucket = localhost:9000
|
||||
access_key = YOUR_ACCESS_KEY_HERE
|
||||
secret_key = YOUR_SECRET_KEY_HERE
|
||||
signature_v2 = False
|
||||
|
||||
### Microsoft Windows
|
||||
|
||||
```sh
|
||||
|
||||
C:\Users\Username\Downloads> minio.exe --help
|
||||
C:\Users\Username\Downloads> minio.exe server D:\Photos
|
||||
|
||||
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
Region: us-east-1
|
||||
|
||||
Browser Access:
|
||||
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
|
||||
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
|
||||
$ mc.exe config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
|
||||
Object API (Amazon S3 compatible):
|
||||
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
|
||||
Java: https://docs.minio.io/docs/java-client-quickstart-guide
|
||||
Python: https://docs.minio.io/docs/python-client-quickstart-guide
|
||||
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
|
||||
|
||||
|
||||
```
|
||||
|
||||
To make a bucket
|
||||
```
|
||||
$ s3cmd mb s3://mybucket
|
||||
Bucket 's3://mybucket/' created
|
||||
### Docker Container
|
||||
|
||||
```sh
|
||||
|
||||
$ docker pull minio/minio
|
||||
$ docker run -p 9000:9000 minio/minio
|
||||
|
||||
```
|
||||
|
||||
To copy an object to bucket
|
||||
```
|
||||
$ s3cmd put newfile.txt s3://testbucket
|
||||
upload: 'newfile' -> 's3://testbucket/newfile'
|
||||
### FreeBSD
|
||||
|
||||
```sh
|
||||
|
||||
$ chmod 755 minio
|
||||
$ ./minio --help
|
||||
$ ./minio server ~/Photos
|
||||
|
||||
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
Region: us-east-1
|
||||
|
||||
Browser Access:
|
||||
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
|
||||
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
|
||||
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
|
||||
Object API (Amazon S3 compatible):
|
||||
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
|
||||
Java: https://docs.minio.io/docs/java-client-quickstart-guide
|
||||
Python: https://docs.minio.io/docs/python-client-quickstart-guide
|
||||
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
|
||||
|
||||
|
||||
```
|
||||
|
||||
To copy an object to local system
|
||||
```
|
||||
$ s3cmd get s3://testbucket/newfile
|
||||
download: 's3://testbucket/newfile' -> './newfile'
|
||||
## 3. Test Minio Server using Minio Browser
|
||||
|
||||
Open a web browser and navigate to http://127.0.0.1:9000 to view your buckets on minio server.
|
||||
|
||||

|
||||
|
||||
|
||||
## 4. Test Minio Server using `mc`
|
||||
|
||||
|
||||
Install mc from [here](https://docs.minio.io/docs/minio-client-quickstart-guide). Use `mc ls` command to list all the buckets on your minio server.
|
||||
|
||||
```sh
|
||||
|
||||
$ mc ls myminio/
|
||||
[2015-08-05 08:13:22 IST] 0B andoria/
|
||||
[2015-08-05 06:14:26 IST] 0B deflector/
|
||||
[2015-08-05 08:13:11 IST] 0B ferenginar/
|
||||
[2016-03-08 14:56:35 IST] 0B jarjarbing/
|
||||
[2016-01-20 16:07:41 IST] 0B my.minio.io/
|
||||
|
||||
```
|
||||
|
||||
To sync local file/directory to a bucket
|
||||
```
|
||||
$ s3cmd sync newdemo s3://testbucket
|
||||
upload: 'newdemo/newdemofile.txt' -> 's3://testbucket/newdemo/newdemofile.txt'
|
||||
```
|
||||
For more examples please navigate to [Minio Client Complete Guide](https://docs.minio.io/docs/minio-client-complete-guide).
|
||||
|
||||
To sync bucket or object with local filesystem
|
||||
```
|
||||
$ s3cmd sync s3://otherbucket otherlocalbucket
|
||||
download: 's3://otherbucket/cat.jpg' -> 'otherlocalbucket/cat.jpg'
|
||||
```
|
||||
|
||||
To list buckets.
|
||||
```
|
||||
$ s3cmd ls s3://
|
||||
2015-12-09 16:12 s3://testbbucket
|
||||
```
|
||||
## 5. Explore Further
|
||||
|
||||
To list contents inside bucket.
|
||||
```
|
||||
$ s3cmd ls s3://testbucket/
|
||||
DIR s3://testbucket/test/
|
||||
2015-12-09 16:05 138504 s3://testbucket/newfile
|
||||
```
|
||||
- [Minio Erasure Code QuickStart Guide](https://docs.minio.io/docs/minio-erasure-code-quickstart-guide)
|
||||
- [Minio Docker Quickstart Guide](https://docs.minio.io/docs/minio-docker-quickstart-guide)
|
||||
- [Use `mc` with Minio Server](https://docs.minio.io/docs/minio-client-quickstart-guide)
|
||||
- [Use `aws-cli` with Minio Server](https://docs.minio.io/docs/aws-cli-with-minio)
|
||||
- [Use `s3cmd` with Minio Server](https://docs.minio.io/docs/s3cmd-with-minio)
|
||||
- [Use `minio-go` SDK with Minio Server](https://docs.minio.io/docs/golang-client-quickstart-guide)
|
||||
|
||||
Delete an object from bucket
|
||||
```
|
||||
$ s3cmd del s3://testbucket/newfile
|
||||
delete: 's3://testbucket/newfile'
|
||||
```
|
||||
|
||||
Delete a bucket
|
||||
```
|
||||
$ s3cmd rb s3://testbucket
|
||||
Bucket 's3://testbucket/' removed
|
||||
```
|
||||
|
||||
## Contribute to Minio Project
|
||||
Please follow Minio [Contributor's Guide](./CONTRIBUTING.md)
|
||||
|
||||
### Jobs
|
||||
If you think in Lisp or Haskell and hack in go, you would blend right in. Send your github link to callhome@minio.io.
|
||||
## 6. Contribute to Minio Project
|
||||
Please follow Minio [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
|
||||
|
||||
+10
-20
@@ -19,10 +19,7 @@ package main
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// credential container for access and secret keys.
|
||||
@@ -31,13 +28,6 @@ type credential struct {
|
||||
SecretAccessKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
// stringer colorized access keys.
|
||||
func (a credential) String() string {
|
||||
accessStr := colorMagenta("AccessKey: ") + colorWhite(a.AccessKeyID)
|
||||
secretStr := colorMagenta("SecretKey: ") + colorWhite(a.SecretAccessKey)
|
||||
return fmt.Sprint(accessStr + " " + secretStr)
|
||||
}
|
||||
|
||||
const (
|
||||
minioAccessID = 20
|
||||
minioSecretID = 40
|
||||
@@ -52,19 +42,19 @@ var isValidAccessKey = regexp.MustCompile(`^[a-zA-Z0-9\\-\\.\\_\\~]{5,20}$`)
|
||||
// mustGenAccessKeys - must generate access credentials.
|
||||
func mustGenAccessKeys() (creds credential) {
|
||||
creds, err := genAccessKeys()
|
||||
fatalIf(err.Trace(), "Unable to generate access keys.", nil)
|
||||
fatalIf(err, "Unable to generate access keys.")
|
||||
return creds
|
||||
}
|
||||
|
||||
// genAccessKeys - generate access credentials.
|
||||
func genAccessKeys() (credential, *probe.Error) {
|
||||
func genAccessKeys() (credential, error) {
|
||||
accessKeyID, err := genAccessKeyID()
|
||||
if err != nil {
|
||||
return credential{}, err.Trace()
|
||||
return credential{}, err
|
||||
}
|
||||
secretAccessKey, err := genSecretAccessKey()
|
||||
if err != nil {
|
||||
return credential{}, err.Trace()
|
||||
return credential{}, err
|
||||
}
|
||||
creds := credential{
|
||||
AccessKeyID: string(accessKeyID),
|
||||
@@ -75,10 +65,10 @@ func genAccessKeys() (credential, *probe.Error) {
|
||||
|
||||
// genAccessKeyID - generate random alpha numeric value using only uppercase characters
|
||||
// takes input as size in integer
|
||||
func genAccessKeyID() ([]byte, *probe.Error) {
|
||||
func genAccessKeyID() ([]byte, error) {
|
||||
alpha := make([]byte, minioAccessID)
|
||||
if _, e := rand.Read(alpha); e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
if _, err := rand.Read(alpha); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < minioAccessID; i++ {
|
||||
alpha[i] = alphaNumericTable[alpha[i]%byte(len(alphaNumericTable))]
|
||||
@@ -87,10 +77,10 @@ func genAccessKeyID() ([]byte, *probe.Error) {
|
||||
}
|
||||
|
||||
// genSecretAccessKey - generate random base64 numeric value from a random seed.
|
||||
func genSecretAccessKey() ([]byte, *probe.Error) {
|
||||
func genSecretAccessKey() ([]byte, error) {
|
||||
rb := make([]byte, minioSecretID)
|
||||
if _, e := rand.Read(rb); e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
if _, err := rand.Read(rb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(base64.StdEncoding.EncodeToString(rb))[:minioSecretID], nil
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Minimalist Object Storage, (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
type accessLogHandler struct {
|
||||
http.Handler
|
||||
accessLogFile *os.File
|
||||
}
|
||||
|
||||
// LogMessage is a serializable json log message
|
||||
type LogMessage struct {
|
||||
StartTime time.Time
|
||||
Duration time.Duration
|
||||
StatusMessage string // human readable http status message
|
||||
ContentLength string // human readable content length
|
||||
|
||||
// HTTP detailed message
|
||||
HTTP struct {
|
||||
ResponseHeaders http.Header
|
||||
Request struct {
|
||||
Method string
|
||||
URL *url.URL
|
||||
Proto string // "HTTP/1.0"
|
||||
ProtoMajor int // 1
|
||||
ProtoMinor int // 0
|
||||
Header http.Header
|
||||
Host string
|
||||
Form url.Values
|
||||
PostForm url.Values
|
||||
Trailer http.Header
|
||||
RemoteAddr string
|
||||
RequestURI string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *accessLogHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
message, err := getLogMessage(w, req)
|
||||
fatalIf(err.Trace(), "Unable to extract http message.", nil)
|
||||
_, e := h.accessLogFile.Write(message)
|
||||
fatalIf(probe.NewError(e), "Writing to log file failed.", nil)
|
||||
|
||||
h.Handler.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func getLogMessage(w http.ResponseWriter, req *http.Request) ([]byte, *probe.Error) {
|
||||
logMessage := &LogMessage{
|
||||
StartTime: time.Now().UTC(),
|
||||
}
|
||||
// store lower level details
|
||||
logMessage.HTTP.ResponseHeaders = w.Header()
|
||||
logMessage.HTTP.Request = struct {
|
||||
Method string
|
||||
URL *url.URL
|
||||
Proto string // "HTTP/1.0"
|
||||
ProtoMajor int // 1
|
||||
ProtoMinor int // 0
|
||||
Header http.Header
|
||||
Host string
|
||||
Form url.Values
|
||||
PostForm url.Values
|
||||
Trailer http.Header
|
||||
RemoteAddr string
|
||||
RequestURI string
|
||||
}{
|
||||
Method: req.Method,
|
||||
URL: req.URL,
|
||||
Proto: req.Proto,
|
||||
ProtoMajor: req.ProtoMajor,
|
||||
ProtoMinor: req.ProtoMinor,
|
||||
Header: req.Header,
|
||||
Host: req.Host,
|
||||
Form: req.Form,
|
||||
PostForm: req.PostForm,
|
||||
Trailer: req.Header,
|
||||
RemoteAddr: req.RemoteAddr,
|
||||
RequestURI: req.RequestURI,
|
||||
}
|
||||
|
||||
// logMessage.HTTP.Request = req
|
||||
logMessage.Duration = time.Now().UTC().Sub(logMessage.StartTime)
|
||||
js, e := json.Marshal(logMessage)
|
||||
if e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
js = append(js, byte('\n')) // append a new line
|
||||
return js, nil
|
||||
}
|
||||
|
||||
// setAccessLogHandler logs requests
|
||||
func setAccessLogHandler(h http.Handler) http.Handler {
|
||||
file, e := os.OpenFile("access.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
fatalIf(probe.NewError(e), "Unable to open access log.", nil)
|
||||
|
||||
return &accessLogHandler{Handler: h, accessLogFile: file}
|
||||
}
|
||||
@@ -16,11 +16,22 @@
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// ObjectIdentifier carries key name for the object to delete.
|
||||
type ObjectIdentifier struct {
|
||||
ObjectName string `xml:"Key"`
|
||||
}
|
||||
|
||||
// createBucketConfiguration container for bucket configuration request from client.
|
||||
// Used for parsing the location from the request body for MakeBucketbucket.
|
||||
type createBucketLocationConfiguration struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CreateBucketConfiguration" json:"-"`
|
||||
Location string `xml:"LocationConstraint"`
|
||||
}
|
||||
|
||||
// DeleteObjectsRequest - xml carrying the object key names which needs to be deleted.
|
||||
type DeleteObjectsRequest struct {
|
||||
// Element to enable quiet mode for the request
|
||||
|
||||
+238
-30
@@ -74,6 +74,7 @@ const (
|
||||
ErrNoSuchKey
|
||||
ErrNoSuchUpload
|
||||
ErrNotImplemented
|
||||
ErrPreconditionFailed
|
||||
ErrRequestTimeTooSkewed
|
||||
ErrSignatureDoesNotMatch
|
||||
ErrMethodNotAllowed
|
||||
@@ -83,8 +84,6 @@ const (
|
||||
ErrMalformedPOSTRequest
|
||||
ErrSignatureVersionNotSupported
|
||||
ErrBucketNotEmpty
|
||||
ErrRootPathFull
|
||||
ErrObjectExistsAsPrefix
|
||||
ErrAllAccessDisabled
|
||||
ErrMalformedPolicy
|
||||
ErrMissingFields
|
||||
@@ -97,14 +96,45 @@ const (
|
||||
ErrMissingSignHeadersTag
|
||||
ErrPolicyAlreadyExpired
|
||||
ErrMalformedDate
|
||||
ErrMalformedPresignedDate
|
||||
ErrMalformedCredentialDate
|
||||
ErrMalformedCredentialRegion
|
||||
ErrMalformedExpires
|
||||
ErrNegativeExpires
|
||||
ErrAuthHeaderEmpty
|
||||
ErrDateHeaderMissing
|
||||
ErrExpiredPresignRequest
|
||||
ErrUnsignedHeaders
|
||||
ErrMissingDateHeader
|
||||
ErrInvalidQuerySignatureAlgo
|
||||
ErrInvalidQueryParams
|
||||
ErrBucketAlreadyOwnedByYou
|
||||
// Add new error codes here.
|
||||
|
||||
// Bucket notification related errors.
|
||||
ErrEventNotification
|
||||
ErrARNNotification
|
||||
ErrRegionNotification
|
||||
ErrOverlappingFilterNotification
|
||||
ErrFilterNameInvalid
|
||||
ErrFilterNamePrefix
|
||||
ErrFilterNameSuffix
|
||||
ErrFilterPrefixValueInvalid
|
||||
|
||||
// S3 extended errors.
|
||||
ErrContentSHA256Mismatch
|
||||
|
||||
// Add new extended error codes here.
|
||||
|
||||
// Minio extended errors.
|
||||
ErrReadQuorum
|
||||
ErrWriteQuorum
|
||||
ErrStorageFull
|
||||
ErrObjectExistsAsDirectory
|
||||
ErrPolicyNesting
|
||||
ErrInvalidObjectName
|
||||
// Add new extended error codes here.
|
||||
// Please open a https://github.com/minio/minio/issues before adding
|
||||
// new error codes here.
|
||||
)
|
||||
|
||||
// error code to APIError structure, these fields carry respective
|
||||
@@ -127,17 +157,17 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
},
|
||||
ErrInvalidMaxUploads: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Argument maxUploads must be an integer between 0 and 2147483647.",
|
||||
Description: "Argument max-uploads must be an integer between 0 and 2147483647",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidMaxKeys: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Argument maxKeys must be an integer between 0 and 2147483647.",
|
||||
Description: "Argument maxKeys must be an integer between 0 and 2147483647",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidMaxParts: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Argument maxParts must be an integer between 1 and 10000.",
|
||||
Description: "Argument max-parts must be an integer between 0 and 2147483647",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidPartNumberMarker: {
|
||||
@@ -202,7 +232,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
},
|
||||
ErrInvalidRange: {
|
||||
Code: "InvalidRange",
|
||||
Description: "The requested range cannot be satisfied.",
|
||||
Description: "The requested range is not satisfiable",
|
||||
HTTPStatusCode: http.StatusRequestedRangeNotSatisfiable,
|
||||
},
|
||||
ErrMalformedXML: {
|
||||
@@ -227,7 +257,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
},
|
||||
ErrNoSuchBucket: {
|
||||
Code: "NoSuchBucket",
|
||||
Description: "The specified bucket does not exist.",
|
||||
Description: "The specified bucket does not exist",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
ErrNoSuchBucketPolicy: {
|
||||
@@ -242,14 +272,19 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
},
|
||||
ErrNoSuchUpload: {
|
||||
Code: "NoSuchUpload",
|
||||
Description: "The specified multipart upload does not exist.",
|
||||
Description: "The specified multipart upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
ErrNotImplemented: {
|
||||
Code: "NotImplemented",
|
||||
Description: "A header you provided implies functionality that is not implemented.",
|
||||
Description: "A header you provided implies functionality that is not implemented",
|
||||
HTTPStatusCode: http.StatusNotImplemented,
|
||||
},
|
||||
ErrPreconditionFailed: {
|
||||
Code: "PreconditionFailed",
|
||||
Description: "At least one of the pre-conditions you specified did not hold",
|
||||
HTTPStatusCode: http.StatusPreconditionFailed,
|
||||
},
|
||||
ErrRequestTimeTooSkewed: {
|
||||
Code: "RequestTimeTooSkewed",
|
||||
Description: "The difference between the request time and the server's time is too large.",
|
||||
@@ -257,7 +292,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
},
|
||||
ErrSignatureDoesNotMatch: {
|
||||
Code: "SignatureDoesNotMatch",
|
||||
Description: "The request signature we calculated does not match the signature you provided.",
|
||||
Description: "The request signature we calculated does not match the signature you provided. Check your key and signing method.",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
ErrMethodNotAllowed: {
|
||||
@@ -295,16 +330,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "The bucket you tried to delete is not empty.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrRootPathFull: {
|
||||
Code: "RootPathFull",
|
||||
Description: "Root path has reached its minimum free disk threshold. Please delete few objects to proceed.",
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
ErrObjectExistsAsPrefix: {
|
||||
Code: "ObjectExistsAsPrefix",
|
||||
Description: "An object already exists as your prefix, choose a different prefix to proceed.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrAllAccessDisabled: {
|
||||
Code: "AllAccessDisabled",
|
||||
Description: "All access to this bucket has been disabled.",
|
||||
@@ -326,8 +351,8 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrCredMalformed: {
|
||||
Code: "CredentialMalformed",
|
||||
Description: "Credential field malformed does not follow accessKeyID/credScope.",
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
Description: "Error parsing the X-Amz-Credential parameter; the Credential is mal-formed; expecting \"<YOUR-AKID>/YYYYMMDD/REGION/SERVICE/aws4_request\".",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMalformedDate: {
|
||||
@@ -335,19 +360,46 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Invalid date format header, expected to be in ISO8601, RFC1123 or RFC1123Z time format.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMalformedPresignedDate: {
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
Description: "X-Amz-Date must be in the ISO8601 Long Format \"yyyyMMdd'T'HHmmss'Z'\"",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
||||
// right Description: "Error parsing the X-Amz-Credential parameter; incorrect date format \"%s\". This date in the credential must be in the format \"yyyyMMdd\".",
|
||||
// Need changes to make sure variable messages can be constructed.
|
||||
ErrMalformedCredentialDate: {
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
Description: "Error parsing the X-Amz-Credential parameter; incorrect date format \"%s\". This date in the credential must be in the format \"yyyyMMdd\".",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
||||
// right Description: "Error parsing the X-Amz-Credential parameter; the region 'us-east-' is wrong; expecting 'us-east-1'".
|
||||
// Need changes to make sure variable messages can be constructed.
|
||||
ErrMalformedCredentialRegion: {
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
Description: "Error parsing the X-Amz-Credential parameter; the region is wrong;",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidRegion: {
|
||||
Code: "InvalidRegion",
|
||||
Description: "Region does not match.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
||||
// right Description: "Error parsing the X-Amz-Credential parameter; incorrect service \"s4\". This endpoint belongs to \"s3\".".
|
||||
// Need changes to make sure variable messages can be constructed.
|
||||
ErrInvalidService: {
|
||||
Code: "AccessDenied",
|
||||
Description: "Service scope should be of value 's3'.",
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
Description: "Error parsing the X-Amz-Credential parameter; incorrect service. This endpoint belongs to \"s3\".",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
||||
// Description: "Error parsing the X-Amz-Credential parameter; incorrect terminal "aws4_reque". This endpoint uses "aws4_request".
|
||||
// Need changes to make sure variable messages can be constructed.
|
||||
ErrInvalidRequestVersion: {
|
||||
Code: "AccessDenied",
|
||||
Description: "Request scope should be of value 'aws4_request'.",
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
Description: "Error parsing the X-Amz-Credential parameter; incorrect terminal. This endpoint uses \"aws4_request\".",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingSignTag: {
|
||||
@@ -366,8 +418,13 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMalformedExpires: {
|
||||
Code: "MalformedExpires",
|
||||
Description: "Malformed expires header, expected non-zero number.",
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
Description: "X-Amz-Expires should be a number",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrNegativeExpires: {
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
Description: "X-Amz-Expires must be non-negative",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAuthHeaderEmpty: {
|
||||
@@ -387,7 +444,13 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
},
|
||||
ErrExpiredPresignRequest: {
|
||||
Code: "AccessDenied",
|
||||
Description: "Request has expired.",
|
||||
Description: "Request has expired",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
// FIXME: Actual XML error response also contains the header which missed in lsit of signed header parameters.
|
||||
ErrUnsignedHeaders: {
|
||||
Code: "AccessDenied",
|
||||
Description: "There were headers present in the request which were not signed",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidQueryParams: {
|
||||
@@ -395,9 +458,150 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Query-string authentication version 4 requires the X-Amz-Algorithm, X-Amz-Credential, X-Amz-Signature, X-Amz-Date, X-Amz-SignedHeaders, and X-Amz-Expires parameters.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrBucketAlreadyOwnedByYou: {
|
||||
Code: "BucketAlreadyOwnedByYou",
|
||||
Description: "Your previous request to create the named bucket succeeded and you already own it.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
|
||||
/// Bucket notification related errors.
|
||||
ErrEventNotification: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "A specified event is not supported for notifications.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrARNNotification: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "A specified destination ARN does not exist or is not well-formed. Verify the destination ARN.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrRegionNotification: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "A specified destination is in a different region than the bucket. You must use a destination that resides in the same region as the bucket.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrOverlappingFilterNotification: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "An object key name filtering rule defined with overlapping prefixes, overlapping suffixes, or overlapping combinations of prefixes and suffixes for the same event types.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrFilterNameInvalid: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "filter rule name must be either prefix or suffix",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrFilterNamePrefix: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Cannot specify more than one prefix rule in a filter.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrFilterNameSuffix: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Cannot specify more than one suffix rule in a filter.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrFilterPrefixValueInvalid: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "prefix rule value cannot exceed 1024 characters",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
/// S3 extensions.
|
||||
ErrContentSHA256Mismatch: {
|
||||
Code: "XAmzContentSHA256Mismatch",
|
||||
Description: "The provided 'x-amz-content-sha256' header does not match what was computed.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
/// Minio extensions.
|
||||
ErrStorageFull: {
|
||||
Code: "XMinioStorageFull",
|
||||
Description: "Storage backend has reached its minimum free disk threshold. Please delete few objects to proceed.",
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
ErrObjectExistsAsDirectory: {
|
||||
Code: "XMinioObjectExistsAsDirectory",
|
||||
Description: "Object name already exists as a directory.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrReadQuorum: {
|
||||
Code: "XMinioReadQuorum",
|
||||
Description: "Multiple disk failures, unable to reconstruct data.",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrWriteQuorum: {
|
||||
Code: "XMinioWriteQuorum",
|
||||
Description: "Multiple disks failures, unable to write data.",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrPolicyNesting: {
|
||||
Code: "XMinioPolicyNesting",
|
||||
Description: "Policy nesting conflict has occurred.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrInvalidObjectName: {
|
||||
Code: "XMinioInvalidObjectName",
|
||||
Description: "Object name contains unsupported characters. Unsupported characters are `^*|\\\"",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
// Add your error structure here.
|
||||
}
|
||||
|
||||
// toAPIErrorCode - Converts embedded errors. Convenience
|
||||
// function written to handle all cases where we have known types of
|
||||
// errors returned by underlying layers.
|
||||
func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
if err == nil {
|
||||
return ErrNone
|
||||
}
|
||||
// Verify if the underlying error is signature mismatch.
|
||||
switch err {
|
||||
case errSignatureMismatch:
|
||||
apiErr = ErrSignatureDoesNotMatch
|
||||
case errContentSHA256Mismatch:
|
||||
apiErr = ErrContentSHA256Mismatch
|
||||
}
|
||||
if apiErr != ErrNone {
|
||||
// If there was a match in the above switch case.
|
||||
return apiErr
|
||||
}
|
||||
switch err.(type) {
|
||||
case StorageFull:
|
||||
apiErr = ErrStorageFull
|
||||
case BadDigest:
|
||||
apiErr = ErrBadDigest
|
||||
case IncompleteBody:
|
||||
apiErr = ErrIncompleteBody
|
||||
case ObjectExistsAsDirectory:
|
||||
apiErr = ErrObjectExistsAsDirectory
|
||||
case BucketNameInvalid:
|
||||
apiErr = ErrInvalidBucketName
|
||||
case BucketNotFound:
|
||||
apiErr = ErrNoSuchBucket
|
||||
case BucketNotEmpty:
|
||||
apiErr = ErrBucketNotEmpty
|
||||
case BucketExists:
|
||||
apiErr = ErrBucketAlreadyOwnedByYou
|
||||
case ObjectNotFound:
|
||||
apiErr = ErrNoSuchKey
|
||||
case ObjectNameInvalid:
|
||||
apiErr = ErrInvalidObjectName
|
||||
case InvalidUploadID:
|
||||
apiErr = ErrNoSuchUpload
|
||||
case InvalidPart:
|
||||
apiErr = ErrInvalidPart
|
||||
case InsufficientWriteQuorum:
|
||||
apiErr = ErrWriteQuorum
|
||||
case InsufficientReadQuorum:
|
||||
apiErr = ErrReadQuorum
|
||||
case PartTooSmall:
|
||||
apiErr = ErrEntityTooSmall
|
||||
default:
|
||||
apiErr = ErrInternalError
|
||||
}
|
||||
return apiErr
|
||||
}
|
||||
|
||||
// getAPIError provides API Error for input API error code.
|
||||
func getAPIError(code APIErrorCode) APIError {
|
||||
return errorCodeResponse[code]
|
||||
@@ -418,3 +622,7 @@ func getAPIErrorResponse(err APIError, resource string) APIErrorResponse {
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func getErrMalformedCredentialDate(malformedDateStr string) {
|
||||
|
||||
}
|
||||
|
||||
+15
-11
@@ -62,24 +62,28 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *h
|
||||
// set common headers
|
||||
setCommonHeaders(w)
|
||||
|
||||
// set object-related metadata headers
|
||||
lastModified := objInfo.ModifiedTime.UTC().Format(http.TimeFormat)
|
||||
// Set content length.
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(objInfo.Size, 10))
|
||||
|
||||
// Set last modified time.
|
||||
lastModified := objInfo.ModTime.UTC().Format(http.TimeFormat)
|
||||
w.Header().Set("Last-Modified", lastModified)
|
||||
|
||||
w.Header().Set("Content-Type", objInfo.ContentType)
|
||||
// Set Etag if available.
|
||||
if objInfo.MD5Sum != "" {
|
||||
w.Header().Set("ETag", "\""+objInfo.MD5Sum+"\"")
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(objInfo.Size, 10))
|
||||
// Set all other user defined metadata.
|
||||
for k, v := range objInfo.UserDefined {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
// for providing ranged content
|
||||
if contentRange != nil {
|
||||
if contentRange.start > 0 || contentRange.length > 0 {
|
||||
// override content-length
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(contentRange.length, 10))
|
||||
w.Header().Set("Content-Range", contentRange.String())
|
||||
w.WriteHeader(http.StatusPartialContent)
|
||||
}
|
||||
if contentRange != nil && contentRange.offsetBegin > -1 {
|
||||
// Override content-length
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(contentRange.getLength(), 10))
|
||||
w.Header().Set("Content-Range", contentRange.String())
|
||||
w.WriteHeader(http.StatusPartialContent)
|
||||
}
|
||||
}
|
||||
|
||||
+16
-7
@@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
// Parse bucket url queries
|
||||
func getBucketResources(values url.Values) (prefix, marker, delimiter string, maxkeys int, encodingType string) {
|
||||
func getListObjectsV1Args(values url.Values) (prefix, marker, delimiter string, maxkeys int, encodingType string) {
|
||||
prefix = values.Get("prefix")
|
||||
marker = values.Get("marker")
|
||||
delimiter = values.Get("delimiter")
|
||||
@@ -35,6 +35,21 @@ func getBucketResources(values url.Values) (prefix, marker, delimiter string, ma
|
||||
return
|
||||
}
|
||||
|
||||
// Parse bucket url queries for ListObjects V2.
|
||||
func getListObjectsV2Args(values url.Values) (prefix, token, startAfter, delimiter string, maxkeys int, encodingType string) {
|
||||
prefix = values.Get("prefix")
|
||||
startAfter = values.Get("start-after")
|
||||
delimiter = values.Get("delimiter")
|
||||
if values.Get("max-keys") != "" {
|
||||
maxkeys, _ = strconv.Atoi(values.Get("max-keys"))
|
||||
} else {
|
||||
maxkeys = maxObjectList
|
||||
}
|
||||
encodingType = values.Get("encoding-type")
|
||||
token = values.Get("continuation-token")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse bucket url queries for ?uploads
|
||||
func getBucketMultipartResources(values url.Values) (prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int, encodingType string) {
|
||||
prefix = values.Get("prefix")
|
||||
@@ -62,9 +77,3 @@ func getObjectResources(values url.Values) (uploadID string, partNumberMarker, m
|
||||
encodingType = values.Get("encoding-type")
|
||||
return
|
||||
}
|
||||
|
||||
// Get upload id.
|
||||
func getUploadID(values url.Values) (uploadID string) {
|
||||
uploadID, _, _, _ = getObjectResources(values)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// File carries any specific responses constructed/necessary in
|
||||
// multipart operations.
|
||||
package main
|
||||
|
||||
import "net/http"
|
||||
|
||||
// writeErrorResponsePartTooSmall - function is used specifically to
|
||||
// construct a proper error response during CompleteMultipartUpload
|
||||
// when one of the parts is < 5MB.
|
||||
// The requirement comes due to the fact that generic ErrorResponse
|
||||
// XML doesn't carry the additional fields required to send this
|
||||
// error. So we construct a new type which lies well within the scope
|
||||
// of this function.
|
||||
func writePartSmallErrorResponse(w http.ResponseWriter, r *http.Request, err PartTooSmall) {
|
||||
// Represents additional fields necessary for ErrPartTooSmall S3 error.
|
||||
type completeMultipartAPIError struct {
|
||||
// Proposed size represents uploaded size of the part.
|
||||
ProposedSize int64
|
||||
// Minimum size allowed epresents the minimum size allowed per
|
||||
// part. Defaults to 5MB.
|
||||
MinSizeAllowed int64
|
||||
// Part number of the part which is incorrect.
|
||||
PartNumber int
|
||||
// ETag of the part which is incorrect.
|
||||
PartETag string
|
||||
// Other default XML error responses.
|
||||
APIErrorResponse
|
||||
}
|
||||
// Generate complete multipart error response.
|
||||
errorResponse := getAPIErrorResponse(getAPIError(toAPIErrorCode(err)), r.URL.Path)
|
||||
cmpErrResp := completeMultipartAPIError{err.PartSize, int64(5242880), err.PartNumber, err.PartETag, errorResponse}
|
||||
encodedErrorResponse := encodeResponse(cmpErrResp)
|
||||
// Write error body
|
||||
w.Write(encodedErrorResponse)
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// Add any other multipart specific responses here.
|
||||
+104
-7
@@ -19,6 +19,7 @@ package main
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -65,6 +66,37 @@ type ListObjectsResponse struct {
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// ListObjectsV2Response - format for list objects response.
|
||||
type ListObjectsV2Response struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListBucketResult" json:"-"`
|
||||
|
||||
CommonPrefixes []CommonPrefix
|
||||
Contents []Object
|
||||
|
||||
Delimiter string
|
||||
|
||||
// Encoding type used to encode object keys in the response.
|
||||
EncodingType string
|
||||
|
||||
// A flag that indicates whether or not ListObjects returned all of the results
|
||||
// that satisfied the search criteria.
|
||||
IsTruncated bool
|
||||
StartAfter string
|
||||
MaxKeys int
|
||||
Name string
|
||||
|
||||
// When response is truncated (the IsTruncated element value in the response
|
||||
// is true), you can use the key name in this field as marker in the subsequent
|
||||
// request to get next set of objects. Server lists objects in alphabetical
|
||||
// order Note: This element is returned only if you have delimiter request parameter
|
||||
// specified. If response does not include the NextMaker and it is truncated,
|
||||
// you can use the value of the last Key in the response as the marker in the
|
||||
// subsequent request to get the next set of object keys.
|
||||
ContinuationToken string
|
||||
NextContinuationToken string
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// Part container for part metadata.
|
||||
type Part struct {
|
||||
PartNumber int
|
||||
@@ -194,6 +226,16 @@ type CompleteMultipartUploadResponse struct {
|
||||
ETag string
|
||||
}
|
||||
|
||||
// PostResponse container for completed post upload response
|
||||
type PostResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ PostResponse" json:"-"`
|
||||
|
||||
Location string
|
||||
Bucket string
|
||||
Key string
|
||||
ETag string
|
||||
}
|
||||
|
||||
// DeleteError structure.
|
||||
type DeleteError struct {
|
||||
Code string
|
||||
@@ -214,7 +256,12 @@ type DeleteObjectsResponse struct {
|
||||
|
||||
// getLocation get URL location.
|
||||
func getLocation(r *http.Request) string {
|
||||
return r.URL.Path
|
||||
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
|
||||
}
|
||||
|
||||
// takes an array of Bucketmetadata information for serialization
|
||||
@@ -244,8 +291,8 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
|
||||
return data
|
||||
}
|
||||
|
||||
// generates an ListObjects response for the said bucket with other enumerated options.
|
||||
func generateListObjectsResponse(bucket, prefix, marker, delimiter string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
|
||||
// generates an ListObjectsV1 response for the said bucket with other enumerated options.
|
||||
func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
|
||||
var contents []Object
|
||||
var prefixes []CommonPrefix
|
||||
var owner = Owner{}
|
||||
@@ -260,7 +307,7 @@ func generateListObjectsResponse(bucket, prefix, marker, delimiter string, maxKe
|
||||
continue
|
||||
}
|
||||
content.Key = object.Name
|
||||
content.LastModified = object.ModifiedTime.UTC().Format(timeFormatAMZ)
|
||||
content.LastModified = object.ModTime.UTC().Format(timeFormatAMZ)
|
||||
if object.MD5Sum != "" {
|
||||
content.ETag = "\"" + object.MD5Sum + "\""
|
||||
}
|
||||
@@ -289,6 +336,51 @@ func generateListObjectsResponse(bucket, prefix, marker, delimiter string, maxKe
|
||||
return data
|
||||
}
|
||||
|
||||
// generates an ListObjectsV2 response for the said bucket with other enumerated options.
|
||||
func generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter string, maxKeys int, resp ListObjectsInfo) ListObjectsV2Response {
|
||||
var contents []Object
|
||||
var prefixes []CommonPrefix
|
||||
var owner = Owner{}
|
||||
var data = ListObjectsV2Response{}
|
||||
|
||||
owner.ID = "minio"
|
||||
owner.DisplayName = "minio"
|
||||
|
||||
for _, object := range resp.Objects {
|
||||
var content = Object{}
|
||||
if object.Name == "" {
|
||||
continue
|
||||
}
|
||||
content.Key = object.Name
|
||||
content.LastModified = object.ModTime.UTC().Format(timeFormatAMZ)
|
||||
if object.MD5Sum != "" {
|
||||
content.ETag = "\"" + object.MD5Sum + "\""
|
||||
}
|
||||
content.Size = object.Size
|
||||
content.StorageClass = "STANDARD"
|
||||
content.Owner = owner
|
||||
contents = append(contents, content)
|
||||
}
|
||||
// TODO - support EncodingType in xml decoding
|
||||
data.Name = bucket
|
||||
data.Contents = contents
|
||||
|
||||
data.StartAfter = startAfter
|
||||
data.Delimiter = delimiter
|
||||
data.Prefix = prefix
|
||||
data.MaxKeys = maxKeys
|
||||
data.ContinuationToken = token
|
||||
data.NextContinuationToken = resp.NextMarker
|
||||
data.IsTruncated = resp.IsTruncated
|
||||
for _, prefix := range resp.Prefixes {
|
||||
var prefixItem = CommonPrefix{}
|
||||
prefixItem.Prefix = prefix
|
||||
prefixes = append(prefixes, prefixItem)
|
||||
}
|
||||
data.CommonPrefixes = prefixes
|
||||
return data
|
||||
}
|
||||
|
||||
// generateCopyObjectResponse
|
||||
func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectResponse {
|
||||
return CopyObjectResponse{
|
||||
@@ -406,13 +498,18 @@ func writeSuccessNoContent(w http.ResponseWriter) {
|
||||
// writeErrorRespone write error headers
|
||||
func writeErrorResponse(w http.ResponseWriter, req *http.Request, errorCode APIErrorCode, resource string) {
|
||||
error := getAPIError(errorCode)
|
||||
// generate error response
|
||||
errorResponse := getAPIErrorResponse(error, resource)
|
||||
encodedErrorResponse := encodeResponse(errorResponse)
|
||||
// set common headers
|
||||
setCommonHeaders(w)
|
||||
// write Header
|
||||
w.WriteHeader(error.HTTPStatusCode)
|
||||
writeErrorResponseNoHeader(w, req, errorCode, resource)
|
||||
}
|
||||
|
||||
func writeErrorResponseNoHeader(w http.ResponseWriter, req *http.Request, errorCode APIErrorCode, resource string) {
|
||||
error := getAPIError(errorCode)
|
||||
// Generate error response.
|
||||
errorResponse := getAPIErrorResponse(error, resource)
|
||||
encodedErrorResponse := encodeResponse(errorResponse)
|
||||
// HEAD should have no body, do not attempt to write to it
|
||||
if req.Method != "HEAD" {
|
||||
// write error body
|
||||
|
||||
+15
-7
@@ -18,13 +18,13 @@ package main
|
||||
|
||||
import router "github.com/gorilla/mux"
|
||||
|
||||
// objectStorageAPI container for S3 compatible API.
|
||||
type objectStorageAPI struct {
|
||||
ObjectAPI ObjectAPI
|
||||
// objectAPIHandler implements and provides http handlers for S3 API.
|
||||
type objectAPIHandlers struct {
|
||||
ObjectAPI ObjectLayer
|
||||
}
|
||||
|
||||
// registerAPIRouter - registers S3 compatible APIs.
|
||||
func registerAPIRouter(mux *router.Router, api objectStorageAPI) {
|
||||
func registerAPIRouter(mux *router.Router, api objectAPIHandlers) {
|
||||
// API Router
|
||||
apiRouter := mux.NewRoute().PathPrefix("/").Subrouter()
|
||||
|
||||
@@ -48,7 +48,7 @@ func registerAPIRouter(mux *router.Router, api objectStorageAPI) {
|
||||
// GetObject
|
||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(api.GetObjectHandler)
|
||||
// CopyObject
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/).*?").HandlerFunc(api.CopyObjectHandler)
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(api.CopyObjectHandler)
|
||||
// PutObject
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(api.PutObjectHandler)
|
||||
// DeleteObject
|
||||
@@ -60,12 +60,20 @@ func registerAPIRouter(mux *router.Router, api objectStorageAPI) {
|
||||
bucket.Methods("GET").HandlerFunc(api.GetBucketLocationHandler).Queries("location", "")
|
||||
// GetBucketPolicy
|
||||
bucket.Methods("GET").HandlerFunc(api.GetBucketPolicyHandler).Queries("policy", "")
|
||||
// GetBucketNotification
|
||||
bucket.Methods("GET").HandlerFunc(api.GetBucketNotificationHandler).Queries("notification", "")
|
||||
// ListenBucketNotification
|
||||
bucket.Methods("GET").HandlerFunc(api.ListenBucketNotificationHandler).Queries("notificationARN", "{notificationARN:.*}")
|
||||
// ListMultipartUploads
|
||||
bucket.Methods("GET").HandlerFunc(api.ListMultipartUploadsHandler).Queries("uploads", "")
|
||||
// ListObjects
|
||||
bucket.Methods("GET").HandlerFunc(api.ListObjectsHandler)
|
||||
// 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
|
||||
|
||||
+10
-10
@@ -1,6 +1,10 @@
|
||||
# Operating system (build VM template)
|
||||
os: Visual Studio 2015
|
||||
# version format
|
||||
version: "{build}"
|
||||
|
||||
# Operating system (build VM template)
|
||||
os: Windows Server 2012 R2
|
||||
|
||||
# Platform.
|
||||
platform: x64
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\minio\minio
|
||||
@@ -8,30 +12,26 @@ clone_folder: c:\gopath\src\github.com\minio\minio
|
||||
# environment variables
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
GO_EXTLINK_ENABLED: 0
|
||||
GO15VENDOREXPERIMENT: 1
|
||||
|
||||
# scripts that run after cloning repository
|
||||
install:
|
||||
- '"C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64'
|
||||
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
|
||||
- rd C:\Go /s /q
|
||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.6.windows-amd64.zip
|
||||
- 7z x go1.6.windows-amd64.zip -oC:\ >nul
|
||||
- go version
|
||||
- go env
|
||||
- cd %GOPATH%\src\github.com\minio\minio
|
||||
|
||||
# to run your custom scripts instead of automatic MSBuild
|
||||
build_script:
|
||||
- go test .
|
||||
- go test -race .
|
||||
- go test github.com/minio/minio/pkg...
|
||||
- go test -race github.com/minio/minio/pkg...
|
||||
- go test -coverprofile=coverage.txt -covermode=atomic
|
||||
- go run buildscripts/gen-ldflags.go > temp.txt
|
||||
- set /p BUILD_LDFLAGS=<temp.txt
|
||||
- go build -ldflags="%BUILD_LDFLAGS%" -o %GOPATH%\bin\minio.exe
|
||||
|
||||
after_test:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
# to disable automatic tests
|
||||
test: off
|
||||
|
||||
|
||||
+76
-44
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,52 +19,43 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
fastSha256 "github.com/minio/minio/pkg/crypto/sha256"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// Verify if the request http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD"
|
||||
func isRequestUnsignedPayload(r *http.Request) bool {
|
||||
return r.Header.Get("x-amz-content-sha256") == unsignedPayload
|
||||
}
|
||||
|
||||
// Verify if request has JWT.
|
||||
func isRequestJWT(r *http.Request) bool {
|
||||
if _, ok := r.Header["Authorization"]; ok {
|
||||
if strings.HasPrefix(r.Header.Get("Authorization"), jwtAlgorithm) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return strings.HasPrefix(r.Header.Get("Authorization"), jwtAlgorithm)
|
||||
}
|
||||
|
||||
// Verify if request has AWS Signature Version '4'.
|
||||
func isRequestSignatureV4(r *http.Request) bool {
|
||||
if _, ok := r.Header["Authorization"]; ok {
|
||||
if strings.HasPrefix(r.Header.Get("Authorization"), signV4Algorithm) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return strings.HasPrefix(r.Header.Get("Authorization"), signV4Algorithm)
|
||||
}
|
||||
|
||||
// Verify if request has AWS Presignature Version '4'.
|
||||
// Verify if request has AWS PreSign Version '4'.
|
||||
func isRequestPresignedSignatureV4(r *http.Request) bool {
|
||||
if _, ok := r.URL.Query()["X-Amz-Credential"]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
_, ok := r.URL.Query()["X-Amz-Credential"]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Verify if request has AWS Post policy Signature Version '4'.
|
||||
func isRequestPostPolicySignatureV4(r *http.Request) bool {
|
||||
if _, ok := r.Header["Content-Type"]; ok {
|
||||
if strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") && r.Method == "POST" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") && r.Method == "POST"
|
||||
}
|
||||
|
||||
// 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 == "PUT"
|
||||
}
|
||||
|
||||
// Authorization type.
|
||||
@@ -76,13 +67,16 @@ const (
|
||||
authTypeAnonymous
|
||||
authTypePresigned
|
||||
authTypePostPolicy
|
||||
authTypeStreamingSigned
|
||||
authTypeSigned
|
||||
authTypeJWT
|
||||
)
|
||||
|
||||
// Get request authentication type.
|
||||
func getRequestAuthType(r *http.Request) authType {
|
||||
if isRequestSignatureV4(r) {
|
||||
if isRequestSignStreamingV4(r) {
|
||||
return authTypeStreamingSigned
|
||||
} else if isRequestSignatureV4(r) {
|
||||
return authTypeSigned
|
||||
} else if isRequestPresignedSignatureV4(r) {
|
||||
return authTypePresigned
|
||||
@@ -98,7 +92,7 @@ func getRequestAuthType(r *http.Request) authType {
|
||||
|
||||
// sum256 calculate sha256 sum for an input byte array
|
||||
func sum256(data []byte) []byte {
|
||||
hash := fastSha256.New()
|
||||
hash := sha256.New()
|
||||
hash.Write(data)
|
||||
return hash.Sum(nil)
|
||||
}
|
||||
@@ -113,12 +107,10 @@ func sumMD5(data []byte) []byte {
|
||||
// Verify if request has valid AWS Signature Version '4'.
|
||||
func isReqAuthenticated(r *http.Request) (s3Error APIErrorCode) {
|
||||
if r == nil {
|
||||
errorIf(probe.NewError(errInvalidArgument), "HTTP request cannot be empty.", nil)
|
||||
return ErrInternalError
|
||||
}
|
||||
payload, e := ioutil.ReadAll(r.Body)
|
||||
if e != nil {
|
||||
errorIf(probe.NewError(e), "Unable to read HTTP body.", nil)
|
||||
payload, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return ErrInternalError
|
||||
}
|
||||
// Verify Content-Md5, if payload is set.
|
||||
@@ -130,16 +122,39 @@ func isReqAuthenticated(r *http.Request) (s3Error APIErrorCode) {
|
||||
// Populate back the payload.
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
||||
validateRegion := true // Validate region.
|
||||
var sha256sum string
|
||||
// Skips calculating sha256 on the payload on server,
|
||||
// if client requested for it.
|
||||
if skipContentSha256Cksum(r) {
|
||||
sha256sum = unsignedPayload
|
||||
} else {
|
||||
sha256sum = hex.EncodeToString(sum256(payload))
|
||||
}
|
||||
if isRequestSignatureV4(r) {
|
||||
return doesSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
|
||||
return doesSignatureMatch(sha256sum, r, validateRegion)
|
||||
} else if isRequestPresignedSignatureV4(r) {
|
||||
return doesPresignedSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
|
||||
return doesPresignedSignatureMatch(sha256sum, r, validateRegion)
|
||||
}
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
// authHandler - handles all the incoming authorization headers and
|
||||
// validates them if possible.
|
||||
// checkAuth - checks for conditions satisfying the authorization of
|
||||
// the incoming request. Request should be either Presigned or Signed
|
||||
// in accordance with AWS S3 Signature V4 requirements. ErrAccessDenied
|
||||
// is returned for unhandled auth type. Once the auth type is indentified
|
||||
// request headers and body are used to calculate the signature validating
|
||||
// the client signature present in request.
|
||||
func checkAuth(r *http.Request) APIErrorCode {
|
||||
aType := getRequestAuthType(r)
|
||||
if aType != authTypePresigned && aType != authTypeSigned {
|
||||
// For all unhandled auth types return error AccessDenied.
|
||||
return ErrAccessDenied
|
||||
}
|
||||
// Validates the request for both Presigned and Signed.
|
||||
return isReqAuthenticated(r)
|
||||
}
|
||||
|
||||
// authHandler - handles all the incoming authorization headers and validates them if possible.
|
||||
type authHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
@@ -149,23 +164,40 @@ func setAuthHandler(h http.Handler) http.Handler {
|
||||
return authHandler{h}
|
||||
}
|
||||
|
||||
// List of all support S3 auth types.
|
||||
var supportedS3AuthTypes = []authType{
|
||||
authTypeAnonymous,
|
||||
authTypePresigned,
|
||||
authTypeSigned,
|
||||
authTypePostPolicy,
|
||||
authTypeStreamingSigned,
|
||||
}
|
||||
|
||||
// Validate if the authType is valid and supported.
|
||||
func isSupportedS3AuthType(aType authType) bool {
|
||||
for _, a := range supportedS3AuthTypes {
|
||||
if a == aType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// handler for validating incoming authorization headers.
|
||||
func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch getRequestAuthType(r) {
|
||||
case authTypeAnonymous, authTypePresigned, authTypeSigned, authTypePostPolicy:
|
||||
// Let top level caller validate for anonymous and known
|
||||
// signed requests.
|
||||
aType := getRequestAuthType(r)
|
||||
if isSupportedS3AuthType(aType) {
|
||||
// Let top level caller validate for anonymous and known signed requests.
|
||||
a.handler.ServeHTTP(w, r)
|
||||
return
|
||||
case authTypeJWT:
|
||||
} else if aType == authTypeJWT {
|
||||
// Validate Authorization header if its valid for JWT request.
|
||||
if !isJWTReqAuthenticated(r) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
a.handler.ServeHTTP(w, r)
|
||||
default:
|
||||
writeErrorResponse(w, r, ErrSignatureVersionNotSupported, r.URL.Path)
|
||||
return
|
||||
}
|
||||
writeErrorResponse(w, r, ErrSignatureVersionNotSupported, r.URL.Path)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test all s3 supported auth types.
|
||||
func TestS3SupportedAuthType(t *testing.T) {
|
||||
type testCase struct {
|
||||
authT authType
|
||||
pass bool
|
||||
}
|
||||
// List of all valid and invalid test cases.
|
||||
testCases := []testCase{
|
||||
// Test 1 - supported s3 type anonymous.
|
||||
{
|
||||
authT: authTypeAnonymous,
|
||||
pass: true,
|
||||
},
|
||||
// Test 2 - supported s3 type presigned.
|
||||
{
|
||||
authT: authTypePresigned,
|
||||
pass: true,
|
||||
},
|
||||
// Test 3 - supported s3 type signed.
|
||||
{
|
||||
authT: authTypeSigned,
|
||||
pass: true,
|
||||
},
|
||||
// Test 4 - supported s3 type with post policy.
|
||||
{
|
||||
authT: authTypePostPolicy,
|
||||
pass: true,
|
||||
},
|
||||
// Test 5 - supported s3 type with streaming signed.
|
||||
{
|
||||
authT: authTypeStreamingSigned,
|
||||
pass: true,
|
||||
},
|
||||
// Test 6 - JWT is not supported s3 type.
|
||||
{
|
||||
authT: authTypeJWT,
|
||||
pass: false,
|
||||
},
|
||||
// Test 7 - unknown auth header is not supported s3 type.
|
||||
{
|
||||
authT: authTypeUnknown,
|
||||
pass: false,
|
||||
},
|
||||
// Test 8 - some new auth type is not supported s3 type.
|
||||
{
|
||||
authT: authType(7),
|
||||
pass: false,
|
||||
},
|
||||
}
|
||||
// Validate all the test cases.
|
||||
for i, tt := range testCases {
|
||||
ok := isSupportedS3AuthType(tt.authT)
|
||||
if ok != tt.pass {
|
||||
t.Errorf("Test %d:, Expected %t, got %t", i+1, tt.pass, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsRequestUnsignedPayload - Test validates the Unsigned payload detection logic.
|
||||
func TestIsRequestUnsignedPayload(t *testing.T) {
|
||||
testCases := []struct {
|
||||
inputAmzContentHeader string
|
||||
expectedResult bool
|
||||
}{
|
||||
// Test case - 1.
|
||||
// Test case with "X-Amz-Content-Sha256" header set to empty value.
|
||||
{"", false},
|
||||
// Test case - 2.
|
||||
// Test case with "X-Amz-Content-Sha256" header set to "UNSIGNED-PAYLOAD"
|
||||
// The payload is flagged as unsigned When "X-Amz-Content-Sha256" header is set to "UNSIGNED-PAYLOAD".
|
||||
{"UNSIGNED-PAYLOAD", true},
|
||||
// Test case - 3.
|
||||
// set to a random value.
|
||||
{"abcd", false},
|
||||
}
|
||||
|
||||
// creating an input HTTP request.
|
||||
// Only the headers are relevant for this particular test.
|
||||
inputReq, err := http.NewRequest("GET", "http://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Error initializing input HTTP request: %v", err)
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
inputReq.Header.Set("X-Amz-Content-Sha256", testCase.inputAmzContentHeader)
|
||||
actualResult := isRequestUnsignedPayload(inputReq)
|
||||
if testCase.expectedResult != actualResult {
|
||||
t.Errorf("Test %d: Expected the result to `%v`, but instead got `%v`", i+1, testCase.expectedResult, actualResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsRequestPresignedSignatureV4 - Test validates the logic for presign signature verision v4 detection.
|
||||
func TestIsRequestPresignedSignatureV4(t *testing.T) {
|
||||
testCases := []struct {
|
||||
inputQueryKey string
|
||||
inputQueryValue string
|
||||
expectedResult bool
|
||||
}{
|
||||
// Test case - 1.
|
||||
// Test case with query key ""X-Amz-Credential" set.
|
||||
{"", "", false},
|
||||
// Test case - 2.
|
||||
{"X-Amz-Credential", "", true},
|
||||
// Test case - 3.
|
||||
{"X-Amz-Content-Sha256", "", false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
// creating an input HTTP request.
|
||||
// Only the query parameters are relevant for this particular test.
|
||||
inputReq, err := http.NewRequest("GET", "http://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Error initializing input HTTP request: %v", err)
|
||||
}
|
||||
q := inputReq.URL.Query()
|
||||
q.Add(testCase.inputQueryKey, testCase.inputQueryValue)
|
||||
inputReq.URL.RawQuery = q.Encode()
|
||||
|
||||
actualResult := isRequestPresignedSignatureV4(inputReq)
|
||||
if testCase.expectedResult != actualResult {
|
||||
t.Errorf("Test %d: Expected the result to `%v`, but instead got `%v`", i+1, testCase.expectedResult, actualResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Provides a fully populated http request instance, fails otherwise.
|
||||
func mustNewRequest(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
|
||||
req, err := newTestRequest(method, urlStr, contentLength, body)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize new http request %s", err)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
// This is similar to mustNewRequest but additionally the request
|
||||
// is signed with AWS Signature V4, fails if not able to do so.
|
||||
func mustNewSignedRequest(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
|
||||
req := mustNewRequest(method, urlStr, contentLength, body, t)
|
||||
cred := serverConfig.GetCredential()
|
||||
if err := signRequest(req, cred.AccessKeyID, cred.SecretAccessKey); err != nil {
|
||||
t.Fatalf("Unable to inititalized 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("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer removeAll(path)
|
||||
|
||||
serverConfig.SetCredential(credential{"myuser", "mypassword"})
|
||||
|
||||
// List of test cases for validating http request authentication.
|
||||
testCases := []struct {
|
||||
req *http.Request
|
||||
s3Error APIErrorCode
|
||||
}{
|
||||
// When request is nil, internal error is returned.
|
||||
{nil, ErrInternalError},
|
||||
// When request is unsigned, access denied is returned.
|
||||
{mustNewRequest("GET", "http://localhost:9000", 0, nil, t), ErrAccessDenied},
|
||||
// When request is properly signed, but has bad Content-MD5 header.
|
||||
{mustNewSignedRequest("PUT", "http://localhost:9000", 5, bytes.NewReader([]byte("hello")), t), ErrBadDigest},
|
||||
// When request is properly signed, error is none.
|
||||
{mustNewSignedRequest("GET", "http://localhost: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); s3Error != testCase.s3Error {
|
||||
t.Fatalf("Unexpected s3error returned wanted %d, got %d", testCase.s3Error, s3Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Benchmark utility functions for ObjectLayer.PutObject().
|
||||
// Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark.
|
||||
func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
var err error
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucket(bucket)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// PutObject returns md5Sum of the object inserted.
|
||||
// md5Sum variable is assigned with that value.
|
||||
var md5Sum 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 PutObject.
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(textData))
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = hex.EncodeToString(hasher.Sum(nil))
|
||||
// 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.
|
||||
md5Sum, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if md5Sum != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, md5Sum, metadata["md5Sum"])
|
||||
}
|
||||
}
|
||||
// Benchmark ends here. Stop timer.
|
||||
b.StopTimer()
|
||||
}
|
||||
|
||||
// Benchmark utility functions for ObjectLayer.PutObjectPart().
|
||||
// Creates Object layer setup ( MakeBucket ) and then runs the PutObjectPart benchmark.
|
||||
func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
||||
var err error
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
object := getRandomObjectName()
|
||||
|
||||
// create bucket.
|
||||
err = obj.MakeBucket(bucket)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
objSize := 128 * 1024 * 1024
|
||||
|
||||
// PutObjectPart returns md5Sum of the object inserted.
|
||||
// md5Sum variable is assigned with that value.
|
||||
var md5Sum, 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.
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(textData))
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = hex.EncodeToString(hasher.Sum(nil))
|
||||
uploadID, err = obj.NewMultipartUpload(bucket, object, metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
var textPartData []byte
|
||||
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
|
||||
b.ReportAllocs()
|
||||
// the actual benchmark for PutObjectPart starts here. Reset the benchmark timer.
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// insert the object.
|
||||
totalPartsNR := int(math.Ceil(float64(objSize) / float64(partSize)))
|
||||
for j := 0; j < totalPartsNR; j++ {
|
||||
hasher.Reset()
|
||||
if j < totalPartsNR-1 {
|
||||
textPartData = textData[j*partSize : (j+1)*partSize-1]
|
||||
} else {
|
||||
textPartData = textData[j*partSize:]
|
||||
}
|
||||
hasher.Write([]byte(textPartData))
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = hex.EncodeToString(hasher.Sum(nil))
|
||||
md5Sum, err = obj.PutObjectPart(bucket, object, uploadID, j, int64(len(textPartData)), bytes.NewBuffer(textPartData), metadata["md5Sum"])
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if md5Sum != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, md5Sum, metadata["md5Sum"])
|
||||
}
|
||||
}
|
||||
}
|
||||
// Benchmark ends here. Stop timer.
|
||||
b.StopTimer()
|
||||
}
|
||||
|
||||
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectPartBenchmark function.
|
||||
func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := makeTestBackend(instanceType)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
||||
}
|
||||
// cleaning up the backend by removing all the directories and files created on function return.
|
||||
defer removeRoots(disks)
|
||||
// uses *testing.B and the object Layer to run the benchmark.
|
||||
runPutObjectPartBenchmark(b, objLayer, objSize)
|
||||
}
|
||||
|
||||
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectBenchmark function.
|
||||
func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := makeTestBackend(instanceType)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
||||
}
|
||||
// cleaning up the backend by removing all the directories and files created on function return.
|
||||
defer removeRoots(disks)
|
||||
// uses *testing.B and the object Layer to run the benchmark.
|
||||
runPutObjectBenchmark(b, objLayer, objSize)
|
||||
}
|
||||
|
||||
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for put object.
|
||||
func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int) {
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := makeTestBackend(instanceType)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
||||
}
|
||||
// cleaning up the backend by removing all the directories and files created on function return.
|
||||
defer removeRoots(disks)
|
||||
// uses *testing.B and the object Layer to run the benchmark.
|
||||
runPutObjectBenchmarkParallel(b, objLayer, objSize)
|
||||
}
|
||||
|
||||
// Benchmark utility functions for ObjectLayer.GetObject().
|
||||
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
|
||||
func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
var err error
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucket(bucket)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// PutObject returns md5Sum of the object inserted.
|
||||
// md5Sum variable is assigned with that value.
|
||||
var md5Sum string
|
||||
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.
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(textData))
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = hex.EncodeToString(hasher.Sum(nil))
|
||||
// insert the object.
|
||||
md5Sum, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if md5Sum != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, md5Sum, metadata["md5Sum"])
|
||||
}
|
||||
}
|
||||
|
||||
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
|
||||
b.ReportAllocs()
|
||||
// the actual benchmark for GetObject starts here. Reset the benchmark timer.
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var buffer = new(bytes.Buffer)
|
||||
err = obj.GetObject(bucket, "object"+strconv.Itoa(i%10), 0, int64(objSize), buffer)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
// Benchmark ends here. Stop timer.
|
||||
b.StopTimer()
|
||||
|
||||
}
|
||||
|
||||
// randomly picks a character and returns its equivalent byte array.
|
||||
func getRandomByte() []byte {
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
// seeding the random number generator.
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
var b byte
|
||||
// pick a character randomly.
|
||||
b = letterBytes[rand.Intn(len(letterBytes))]
|
||||
return []byte{b}
|
||||
}
|
||||
|
||||
// picks a random byte and repeats it to size bytes.
|
||||
func generateBytesData(size int) []byte {
|
||||
// repeat the random character chosen size
|
||||
return bytes.Repeat(getRandomByte(), size)
|
||||
}
|
||||
|
||||
// creates XL/FS backend setup, obtains the object layer and calls the runGetObjectBenchmark function.
|
||||
func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := makeTestBackend(instanceType)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
||||
}
|
||||
// cleaning up the backend by removing all the directories and files created.
|
||||
defer removeRoots(disks)
|
||||
// uses *testing.B and the object Layer to run the benchmark.
|
||||
runGetObjectBenchmark(b, objLayer, objSize)
|
||||
}
|
||||
|
||||
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for ObjectLayer.GetObject() .
|
||||
func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int) {
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := makeTestBackend(instanceType)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed obtaining Temp Backend: <ERROR> %s", err)
|
||||
}
|
||||
// cleaning up the backend by removing all the directories and files created.
|
||||
defer removeRoots(disks)
|
||||
// uses *testing.B and the object Layer to run the benchmark.
|
||||
runGetObjectBenchmarkParallel(b, objLayer, objSize)
|
||||
}
|
||||
|
||||
// Parallel benchmark utility functions for ObjectLayer.PutObject().
|
||||
// Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark.
|
||||
func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
var err error
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucket(bucket)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// PutObject returns md5Sum of the object inserted.
|
||||
// md5Sum variable is assigned with that value.
|
||||
var md5Sum 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 PutObject.
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(textData))
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = hex.EncodeToString(hasher.Sum(nil))
|
||||
// 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()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
// insert the object.
|
||||
md5Sum, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if md5Sum != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", md5Sum, metadata["md5Sum"])
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
|
||||
// Benchmark ends here. Stop timer.
|
||||
b.StopTimer()
|
||||
}
|
||||
|
||||
// Parallel benchmark utility functions for ObjectLayer.GetObject().
|
||||
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
|
||||
func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
var err error
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucket(bucket)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// PutObject returns md5Sum of the object inserted.
|
||||
// md5Sum variable is assigned with that value.
|
||||
var md5Sum string
|
||||
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.
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(textData))
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = hex.EncodeToString(hasher.Sum(nil))
|
||||
// insert the object.
|
||||
md5Sum, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if md5Sum != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, md5Sum, metadata["md5Sum"])
|
||||
}
|
||||
}
|
||||
|
||||
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
|
||||
b.ReportAllocs()
|
||||
// the actual benchmark for GetObject starts here. Reset the benchmark timer.
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
err = obj.GetObject(bucket, "object"+strconv.Itoa(i), 0, int64(objSize), ioutil.Discard)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
i++
|
||||
if i == 10 {
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
})
|
||||
// Benchmark ends here. Stop timer.
|
||||
b.StopTimer()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Validate all the ListObjects 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.
|
||||
// - marker if set should have a common prefix with 'prefix' param, otherwise
|
||||
// the request is rejected.
|
||||
func listObjectsValidateArgs(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
|
||||
}
|
||||
// Marker is set validate pre-condition.
|
||||
if marker != "" {
|
||||
// Marker not common with prefix is not implemented.
|
||||
if !strings.HasPrefix(marker, prefix) {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
}
|
||||
// Success.
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// ListObjectsV2Handler - GET Bucket (List Objects) Version 2.
|
||||
// --------------------------
|
||||
// This implementation of the GET operation returns some or all (up to 1000)
|
||||
// of the objects in a bucket. You can use the request parameters as selection
|
||||
// criteria to return a subset of the objects in a bucket.
|
||||
//
|
||||
// NOTE: It is recommended that this API to be used for application development.
|
||||
// Minio continues to support ListObjectsV1 for supporting legacy tools.
|
||||
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
return
|
||||
case authTypeAnonymous:
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
||||
if s3Error := enforceBucketPolicy(bucket, "s3:ListBucket", r.URL); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
case authTypeSigned, authTypePresigned:
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Extract all the listObjectsV2 query params to their native values.
|
||||
prefix, token, startAfter, delimiter, maxKeys, _ := getListObjectsV2Args(r.URL.Query())
|
||||
|
||||
// In ListObjectsV2 'continuation-token' is the marker.
|
||||
marker := token
|
||||
// Check if 'continuation-token' is empty.
|
||||
if token == "" {
|
||||
// Then we need to use 'start-after' as marker instead.
|
||||
marker = startAfter
|
||||
}
|
||||
// Validate all the query params before beginning to serve the request.
|
||||
if s3Error := listObjectsValidateArgs(prefix, marker, delimiter, maxKeys); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
// Inititate a list objects operation based on the input params.
|
||||
// On success would return back ListObjectsInfo object to be
|
||||
// marshalled into S3 compatible XML header.
|
||||
listObjectsInfo, err := api.ObjectAPI.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to list objects.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
response := generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter, maxKeys, listObjectsInfo)
|
||||
// Write headers
|
||||
setCommonHeaders(w)
|
||||
// Write success response.
|
||||
writeSuccessResponse(w, encodeResponse(response))
|
||||
}
|
||||
|
||||
// ListObjectsV1Handler - GET Bucket (List Objects) Version 1.
|
||||
// --------------------------
|
||||
// This implementation of the GET operation returns some or all (up to 1000)
|
||||
// of the objects in a bucket. You can use the request parameters as selection
|
||||
// criteria to return a subset of the objects in a bucket.
|
||||
//
|
||||
func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
return
|
||||
case authTypeAnonymous:
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
||||
if s3Error := enforceBucketPolicy(bucket, "s3:ListBucket", r.URL); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
case authTypeSigned, authTypePresigned:
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Extract all the litsObjectsV1 query params to their native values.
|
||||
prefix, marker, delimiter, maxKeys, _ := getListObjectsV1Args(r.URL.Query())
|
||||
|
||||
// Validate all the query params before beginning to serve the request.
|
||||
if s3Error := listObjectsValidateArgs(prefix, marker, delimiter, maxKeys); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Inititate a list objects operation based on the input params.
|
||||
// On success would return back ListObjectsInfo object to be
|
||||
// marshalled into S3 compatible XML header.
|
||||
listObjectsInfo, err := api.ObjectAPI.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to list objects.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, maxKeys, listObjectsInfo)
|
||||
// Write headers
|
||||
setCommonHeaders(w)
|
||||
// Write success response.
|
||||
writeSuccessResponse(w, encodeResponse(response))
|
||||
}
|
||||
+137
-358
@@ -17,55 +17,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
mux "github.com/gorilla/mux"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
||||
func enforceBucketPolicy(action string, bucket string, reqURL *url.URL) (s3Error APIErrorCode) {
|
||||
// Read saved bucket policy.
|
||||
policy, err := readBucketPolicy(bucket)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(bucket), "GetBucketPolicy failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case BucketNotFound:
|
||||
return ErrNoSuchBucket
|
||||
case BucketNameInvalid:
|
||||
return ErrInvalidBucketName
|
||||
default:
|
||||
// For any other error just return AccessDenied.
|
||||
return ErrAccessDenied
|
||||
}
|
||||
// Enforces bucket policies for a bucket for a given tatusaction.
|
||||
func enforceBucketPolicy(bucket string, action string, reqURL *url.URL) (s3Error APIErrorCode) {
|
||||
if !IsValidBucketName(bucket) {
|
||||
return ErrInvalidBucketName
|
||||
}
|
||||
// Parse the saved policy.
|
||||
bucketPolicy, e := parseBucketPolicy(policy)
|
||||
if e != nil {
|
||||
errorIf(probe.NewError(e), "Parse policy failed.", nil)
|
||||
// Fetch bucket policy, if policy is not set return access denied.
|
||||
policy := globalBucketPolicies.GetBucketPolicy(bucket)
|
||||
if policy == nil {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
// Construct resource in 'arn:aws:s3:::examplebucket' format.
|
||||
// Construct resource in 'arn:aws:s3:::examplebucket/object' format.
|
||||
resource := AWSResourcePrefix + strings.TrimPrefix(reqURL.Path, "/")
|
||||
|
||||
// Get conditions for policy verification.
|
||||
conditions := make(map[string]string)
|
||||
for queryParam := range reqURL.Query() {
|
||||
conditions[queryParam] = reqURL.Query().Get("queryParam")
|
||||
conditions[queryParam] = reqURL.Query().Get(queryParam)
|
||||
}
|
||||
|
||||
// Validate action, resource and conditions with current policy statements.
|
||||
if !bucketPolicyEvalStatements(action, resource, conditions, bucketPolicy.Statements) {
|
||||
if !bucketPolicyEvalStatements(action, resource, conditions, policy.Statements) {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
return ErrNone
|
||||
@@ -74,7 +58,7 @@ func enforceBucketPolicy(action string, bucket string, reqURL *url.URL) (s3Error
|
||||
// GetBucketLocationHandler - GET Bucket location.
|
||||
// -------------------------
|
||||
// This operation returns bucket location.
|
||||
func (api objectStorageAPI) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
@@ -85,49 +69,20 @@ func (api objectStorageAPI) GetBucketLocationHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
case authTypeAnonymous:
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
||||
if s3Error := enforceBucketPolicy("s3:GetBucketLocation", bucket, r.URL); s3Error != ErrNone {
|
||||
if s3Error := enforceBucketPolicy(bucket, "s3:GetBucketLocation", r.URL); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
case authTypeSigned, authTypePresigned:
|
||||
payload, e := ioutil.ReadAll(r.Body)
|
||||
if e != nil {
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
return
|
||||
}
|
||||
// Verify Content-Md5, if payload is set.
|
||||
if r.Header.Get("Content-Md5") != "" {
|
||||
if r.Header.Get("Content-Md5") != base64.StdEncoding.EncodeToString(sumMD5(payload)) {
|
||||
writeErrorResponse(w, r, ErrBadDigest, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Populate back the payload.
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
||||
var s3Error APIErrorCode // API error code.
|
||||
validateRegion := false // Validate region.
|
||||
if isRequestSignatureV4(r) {
|
||||
s3Error = doesSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
|
||||
} else if isRequestPresignedSignatureV4(r) {
|
||||
s3Error = doesPresignedSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
|
||||
}
|
||||
if s3Error != ErrNone {
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err := api.ObjectAPI.GetBucketInfo(bucket)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "GetBucketInfo failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case BucketNotFound:
|
||||
writeErrorResponse(w, r, ErrNoSuchBucket, r.URL.Path)
|
||||
case BucketNameInvalid:
|
||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
}
|
||||
if _, err := api.ObjectAPI.GetBucketInfo(bucket); err != nil {
|
||||
errorIf(err, "Unable to fetch bucket info.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -152,7 +107,7 @@ func (api objectStorageAPI) GetBucketLocationHandler(w http.ResponseWriter, r *h
|
||||
// completed or aborted. This operation returns at most 1,000 multipart
|
||||
// uploads in the response.
|
||||
//
|
||||
func (api objectStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
@@ -163,7 +118,7 @@ func (api objectStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, r
|
||||
return
|
||||
case authTypeAnonymous:
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/mpuAndPermissions.html
|
||||
if s3Error := enforceBucketPolicy("s3:ListBucketMultipartUploads", bucket, r.URL); s3Error != ErrNone {
|
||||
if s3Error := enforceBucketPolicy(bucket, "s3:ListBucketMultipartUploads", r.URL); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
@@ -180,32 +135,17 @@ func (api objectStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
if keyMarker != "" {
|
||||
// Unescape keyMarker string
|
||||
keyMarkerUnescaped, e := url.QueryUnescape(keyMarker)
|
||||
if e != nil {
|
||||
if e != nil {
|
||||
// Return 'NoSuchKey' to indicate invalid marker key.
|
||||
writeErrorResponse(w, r, ErrNoSuchKey, r.URL.Path)
|
||||
return
|
||||
}
|
||||
keyMarker = keyMarkerUnescaped
|
||||
// Marker not common with prefix is not implemented.
|
||||
if !strings.HasPrefix(keyMarker, prefix) {
|
||||
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
|
||||
return
|
||||
}
|
||||
// Marker not common with prefix is not implemented.
|
||||
if !strings.HasPrefix(keyMarker, prefix) {
|
||||
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
listMultipartsInfo, err := api.ObjectAPI.ListMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "ListMultipartUploads failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case BucketNotFound:
|
||||
writeErrorResponse(w, r, ErrNoSuchBucket, r.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
}
|
||||
errorIf(err, "Unable to list multipart uploads.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
// generate response
|
||||
@@ -217,144 +157,35 @@ func (api objectStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, r
|
||||
writeSuccessResponse(w, encodedSuccessResponse)
|
||||
}
|
||||
|
||||
// ListObjectsHandler - GET Bucket (List Objects)
|
||||
// -- -----------------------
|
||||
// This implementation of the GET operation returns some or all (up to 1000)
|
||||
// of the objects in a bucket. You can use the request parameters as selection
|
||||
// criteria to return a subset of the objects in a bucket.
|
||||
//
|
||||
func (api objectStorageAPI) ListObjectsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
return
|
||||
case authTypeAnonymous:
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
||||
if s3Error := enforceBucketPolicy("s3:ListBucket", bucket, r.URL); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
case authTypeSigned, authTypePresigned:
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO handle encoding type.
|
||||
prefix, marker, delimiter, maxkeys, _ := getBucketResources(r.URL.Query())
|
||||
if maxkeys < 0 {
|
||||
writeErrorResponse(w, r, ErrInvalidMaxKeys, r.URL.Path)
|
||||
return
|
||||
}
|
||||
// Verify if delimiter is anything other than '/', which we do not support.
|
||||
if delimiter != "" && delimiter != "/" {
|
||||
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
|
||||
return
|
||||
}
|
||||
// If marker is set unescape.
|
||||
if marker != "" {
|
||||
// Try to unescape marker.
|
||||
markerUnescaped, e := url.QueryUnescape(marker)
|
||||
if e != nil {
|
||||
// Return 'NoSuchKey' to indicate invalid marker key.
|
||||
writeErrorResponse(w, r, ErrNoSuchKey, r.URL.Path)
|
||||
return
|
||||
}
|
||||
marker = markerUnescaped
|
||||
// Marker not common with prefix is not implemented.
|
||||
if !strings.HasPrefix(marker, prefix) {
|
||||
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
listObjectsInfo, err := api.ObjectAPI.ListObjects(bucket, prefix, marker, delimiter, maxkeys)
|
||||
if err == nil {
|
||||
// generate response
|
||||
response := generateListObjectsResponse(bucket, prefix, marker, delimiter, maxkeys, listObjectsInfo)
|
||||
encodedSuccessResponse := encodeResponse(response)
|
||||
// Write headers
|
||||
setCommonHeaders(w)
|
||||
// Write success response.
|
||||
writeSuccessResponse(w, encodedSuccessResponse)
|
||||
return
|
||||
}
|
||||
switch err.ToGoError().(type) {
|
||||
case BucketNameInvalid:
|
||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
||||
case BucketNotFound:
|
||||
writeErrorResponse(w, r, ErrNoSuchBucket, r.URL.Path)
|
||||
case ObjectNotFound:
|
||||
writeErrorResponse(w, r, ErrNoSuchKey, r.URL.Path)
|
||||
case ObjectNameInvalid:
|
||||
writeErrorResponse(w, r, ErrNoSuchKey, r.URL.Path)
|
||||
default:
|
||||
errorIf(err.Trace(), "ListObjects failed.", nil)
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// ListBucketsHandler - GET Service
|
||||
// -----------
|
||||
// This implementation of the GET operation returns a list of all buckets
|
||||
// owned by the authenticated sender of the request.
|
||||
func (api objectStorageAPI) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// List buckets does not support bucket policies.
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// List buckets does not support bucket policies, no need to enforce it.
|
||||
if s3Error := checkAuth(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
case authTypeSigned, authTypePresigned:
|
||||
payload, e := ioutil.ReadAll(r.Body)
|
||||
if e != nil {
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
return
|
||||
}
|
||||
// Verify Content-Md5, if payload is set.
|
||||
if r.Header.Get("Content-Md5") != "" {
|
||||
if r.Header.Get("Content-Md5") != base64.StdEncoding.EncodeToString(sumMD5(payload)) {
|
||||
writeErrorResponse(w, r, ErrBadDigest, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Populate back the payload.
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
||||
var s3Error APIErrorCode // API error code.
|
||||
validateRegion := false // Validate region.
|
||||
if isRequestSignatureV4(r) {
|
||||
s3Error = doesSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
|
||||
} else if isRequestPresignedSignatureV4(r) {
|
||||
s3Error = doesPresignedSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
|
||||
}
|
||||
if s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bucketsInfo, err := api.ObjectAPI.ListBuckets()
|
||||
if err == nil {
|
||||
// generate response
|
||||
response := generateListBucketsResponse(bucketsInfo)
|
||||
encodedSuccessResponse := encodeResponse(response)
|
||||
// write headers
|
||||
setCommonHeaders(w)
|
||||
// write response
|
||||
writeSuccessResponse(w, encodedSuccessResponse)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to list buckets.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
errorIf(err.Trace(), "ListBuckets failed.", nil)
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
|
||||
// Generate response.
|
||||
response := generateListBucketsResponse(bucketsInfo)
|
||||
encodedSuccessResponse := encodeResponse(response)
|
||||
// Write headers.
|
||||
setCommonHeaders(w)
|
||||
// Write response.
|
||||
writeSuccessResponse(w, encodedSuccessResponse)
|
||||
}
|
||||
|
||||
// DeleteMultipleObjectsHandler - deletes multiple objects.
|
||||
func (api objectStorageAPI) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
@@ -365,7 +196,7 @@ func (api objectStorageAPI) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
return
|
||||
case authTypeAnonymous:
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
||||
if s3Error := enforceBucketPolicy("s3:DeleteObject", bucket, r.URL); s3Error != ErrNone {
|
||||
if s3Error := enforceBucketPolicy(bucket, "s3:DeleteObject", r.URL); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
@@ -394,16 +225,16 @@ func (api objectStorageAPI) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
deleteXMLBytes := make([]byte, r.ContentLength)
|
||||
|
||||
// Read incoming body XML bytes.
|
||||
_, e := io.ReadFull(r.Body, deleteXMLBytes)
|
||||
if e != nil {
|
||||
errorIf(probe.NewError(e), "DeleteMultipleObjects failed.", nil)
|
||||
if _, err := io.ReadFull(r.Body, deleteXMLBytes); err != nil {
|
||||
errorIf(err, "Unable to read HTTP body.")
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Unmarshal list of keys to be deleted.
|
||||
deleteObjects := &DeleteObjectsRequest{}
|
||||
if e := xml.Unmarshal(deleteXMLBytes, deleteObjects); e != nil {
|
||||
if err := xml.Unmarshal(deleteXMLBytes, deleteObjects); err != nil {
|
||||
errorIf(err, "Unable to unmarshal delete objects request XML.")
|
||||
writeErrorResponse(w, r, ErrMalformedXML, r.URL.Path)
|
||||
return
|
||||
}
|
||||
@@ -418,39 +249,12 @@ func (api objectStorageAPI) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
ObjectName: object.ObjectName,
|
||||
})
|
||||
} else {
|
||||
errorIf(err.Trace(object.ObjectName), "DeleteObject failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case BucketNameInvalid:
|
||||
deleteErrors = append(deleteErrors, DeleteError{
|
||||
Code: errorCodeResponse[ErrInvalidBucketName].Code,
|
||||
Message: errorCodeResponse[ErrInvalidBucketName].Description,
|
||||
Key: object.ObjectName,
|
||||
})
|
||||
case BucketNotFound:
|
||||
deleteErrors = append(deleteErrors, DeleteError{
|
||||
Code: errorCodeResponse[ErrNoSuchBucket].Code,
|
||||
Message: errorCodeResponse[ErrNoSuchBucket].Description,
|
||||
Key: object.ObjectName,
|
||||
})
|
||||
case ObjectNotFound:
|
||||
deleteErrors = append(deleteErrors, DeleteError{
|
||||
Code: errorCodeResponse[ErrNoSuchKey].Code,
|
||||
Message: errorCodeResponse[ErrNoSuchKey].Description,
|
||||
Key: object.ObjectName,
|
||||
})
|
||||
case ObjectNameInvalid:
|
||||
deleteErrors = append(deleteErrors, DeleteError{
|
||||
Code: errorCodeResponse[ErrNoSuchKey].Code,
|
||||
Message: errorCodeResponse[ErrNoSuchKey].Description,
|
||||
Key: object.ObjectName,
|
||||
})
|
||||
default:
|
||||
deleteErrors = append(deleteErrors, DeleteError{
|
||||
Code: errorCodeResponse[ErrInternalError].Code,
|
||||
Message: errorCodeResponse[ErrInternalError].Description,
|
||||
Key: object.ObjectName,
|
||||
})
|
||||
}
|
||||
errorIf(err, "Unable to delete object.")
|
||||
deleteErrors = append(deleteErrors, DeleteError{
|
||||
Code: errorCodeResponse[toAPIErrorCode(err)].Code,
|
||||
Message: errorCodeResponse[toAPIErrorCode(err)].Description,
|
||||
Key: object.ObjectName,
|
||||
})
|
||||
}
|
||||
}
|
||||
// Generate response
|
||||
@@ -465,35 +269,27 @@ func (api objectStorageAPI) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
// PutBucketHandler - PUT Bucket
|
||||
// ----------
|
||||
// This implementation of the PUT operation creates a new bucket for authenticated request
|
||||
func (api objectStorageAPI) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// PutBucket does not support policies, use checkAuth to validate signature.
|
||||
if s3Error := checkAuth(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
// Set http request for signature.
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
return
|
||||
case authTypePresigned, authTypeSigned:
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
// Validate if incoming location constraint is valid, reject
|
||||
// requests which do not follow valid region requirements.
|
||||
if s3Error := isValidLocationConstraint(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
}
|
||||
|
||||
// Make bucket.
|
||||
// Proceed to creating a bucket.
|
||||
err := api.ObjectAPI.MakeBucket(bucket)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "MakeBucket failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case BucketNameInvalid:
|
||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
||||
case BucketExists:
|
||||
writeErrorResponse(w, r, ErrBucketAlreadyExists, r.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
}
|
||||
errorIf(err, "Unable to create a bucket.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
// Make sure to add Location information here only for bucket
|
||||
@@ -501,48 +297,23 @@ func (api objectStorageAPI) PutBucketHandler(w http.ResponseWriter, r *http.Requ
|
||||
writeSuccessResponse(w, nil)
|
||||
}
|
||||
|
||||
func extractHTTPFormValues(reader *multipart.Reader) (io.Reader, map[string]string, *probe.Error) {
|
||||
/// HTML Form values
|
||||
formValues := make(map[string]string)
|
||||
filePart := new(bytes.Buffer)
|
||||
var e error
|
||||
for e == nil {
|
||||
var part *multipart.Part
|
||||
part, e = reader.NextPart()
|
||||
if part != nil {
|
||||
if part.FileName() == "" {
|
||||
buffer, e := ioutil.ReadAll(part)
|
||||
if e != nil {
|
||||
return nil, nil, probe.NewError(e)
|
||||
}
|
||||
formValues[http.CanonicalHeaderKey(part.FormName())] = string(buffer)
|
||||
} else {
|
||||
if _, e := io.Copy(filePart, part); e != nil {
|
||||
return nil, nil, probe.NewError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return filePart, formValues, nil
|
||||
}
|
||||
|
||||
// PostPolicyBucketHandler - POST policy
|
||||
// ----------
|
||||
// This implementation of the POST operation handles object creation with a specified
|
||||
// signature policy in multipart/form-data
|
||||
func (api objectStorageAPI) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Here the parameter is the size of the form data that should
|
||||
// be loaded in memory, the remaining being put in temporary files.
|
||||
reader, e := r.MultipartReader()
|
||||
if e != nil {
|
||||
errorIf(probe.NewError(e), "Unable to initialize multipart reader.", nil)
|
||||
reader, err := r.MultipartReader()
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to initialize multipart reader.")
|
||||
writeErrorResponse(w, r, ErrMalformedPOSTRequest, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
fileBody, formValues, err := extractHTTPFormValues(reader)
|
||||
fileBody, fileName, formValues, err := extractPostPolicyFormValues(reader)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "Unable to parse form values.", nil)
|
||||
errorIf(err, "Unable to parse form values.")
|
||||
writeErrorResponse(w, r, ErrMalformedPOSTRequest, r.URL.Path)
|
||||
return
|
||||
}
|
||||
@@ -550,6 +321,12 @@ func (api objectStorageAPI) PostPolicyBucketHandler(w http.ResponseWriter, r *ht
|
||||
formValues["Bucket"] = bucket
|
||||
object := formValues["Key"]
|
||||
|
||||
if fileName != "" && strings.Contains(object, "${filename}") {
|
||||
// S3 feature to replace ${filename} found in Key form field
|
||||
// by the filename attribute passed in multipart
|
||||
object = strings.Replace(object, "${filename}", fileName, -1)
|
||||
}
|
||||
|
||||
// Verify policy signature.
|
||||
apiErr := doesPolicySignatureMatch(formValues)
|
||||
if apiErr != ErrNone {
|
||||
@@ -560,29 +337,49 @@ func (api objectStorageAPI) PostPolicyBucketHandler(w http.ResponseWriter, r *ht
|
||||
writeErrorResponse(w, r, apiErr, r.URL.Path)
|
||||
return
|
||||
}
|
||||
objInfo, err := api.ObjectAPI.PutObject(bucket, object, -1, fileBody, nil)
|
||||
|
||||
// Save metadata.
|
||||
metadata := make(map[string]string)
|
||||
// Nothing to store right now.
|
||||
|
||||
md5Sum, err := api.ObjectAPI.PutObject(bucket, object, -1, fileBody, metadata)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "PutObject failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case RootPathFull:
|
||||
writeErrorResponse(w, r, ErrRootPathFull, r.URL.Path)
|
||||
case BucketNotFound:
|
||||
writeErrorResponse(w, r, ErrNoSuchBucket, r.URL.Path)
|
||||
case BucketNameInvalid:
|
||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
||||
case BadDigest:
|
||||
writeErrorResponse(w, r, ErrBadDigest, r.URL.Path)
|
||||
case IncompleteBody:
|
||||
writeErrorResponse(w, r, ErrIncompleteBody, r.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
}
|
||||
errorIf(err, "Unable to create object.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
if objInfo.MD5Sum != "" {
|
||||
w.Header().Set("ETag", "\""+objInfo.MD5Sum+"\"")
|
||||
if md5Sum != "" {
|
||||
w.Header().Set("ETag", "\""+md5Sum+"\"")
|
||||
}
|
||||
encodedSuccessResponse := encodeResponse(PostResponse{
|
||||
Location: getObjectLocation(bucket, object), // TODO Full URL is preferred
|
||||
Bucket: bucket,
|
||||
Key: object,
|
||||
ETag: md5Sum,
|
||||
})
|
||||
|
||||
setCommonHeaders(w)
|
||||
|
||||
writeSuccessResponse(w, encodedSuccessResponse)
|
||||
|
||||
// Fetch object info for notifications.
|
||||
objInfo, err := api.ObjectAPI.GetObjectInfo(bucket, object)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to fetch object info for \"%s\"", path.Join(bucket, object))
|
||||
return
|
||||
}
|
||||
|
||||
if eventN.IsBucketNotificationSet(bucket) {
|
||||
// Notify object created event.
|
||||
eventNotify(eventData{
|
||||
Type: ObjectCreatedPost,
|
||||
Bucket: bucket,
|
||||
ObjInfo: objInfo,
|
||||
ReqParams: map[string]string{
|
||||
"sourceIPAddress": r.RemoteAddr,
|
||||
},
|
||||
})
|
||||
}
|
||||
writeSuccessResponse(w, nil)
|
||||
}
|
||||
|
||||
// HeadBucketHandler - HEAD Bucket
|
||||
@@ -591,7 +388,7 @@ func (api objectStorageAPI) PostPolicyBucketHandler(w http.ResponseWriter, r *ht
|
||||
// The operation returns a 200 OK if the bucket exists and you
|
||||
// have permission to access it. Otherwise, the operation might
|
||||
// return responses such as 404 Not Found and 403 Forbidden.
|
||||
func (api objectStorageAPI) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
@@ -602,7 +399,7 @@ func (api objectStorageAPI) HeadBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
case authTypeAnonymous:
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
||||
if s3Error := enforceBucketPolicy("s3:ListBucket", bucket, r.URL); s3Error != ErrNone {
|
||||
if s3Error := enforceBucketPolicy(bucket, "s3:ListBucket", r.URL); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
@@ -613,55 +410,37 @@ func (api objectStorageAPI) HeadBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
}
|
||||
|
||||
_, err := api.ObjectAPI.GetBucketInfo(bucket)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "GetBucketInfo failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case BucketNotFound:
|
||||
writeErrorResponse(w, r, ErrNoSuchBucket, r.URL.Path)
|
||||
case BucketNameInvalid:
|
||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
}
|
||||
if _, err := api.ObjectAPI.GetBucketInfo(bucket); err != nil {
|
||||
errorIf(err, "Unable to fetch bucket info.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
writeSuccessResponse(w, nil)
|
||||
}
|
||||
|
||||
// DeleteBucketHandler - Delete bucket
|
||||
func (api objectStorageAPI) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// DeleteBucket does not support bucket policies, use checkAuth to validate signature.
|
||||
if s3Error := checkAuth(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
return
|
||||
case authTypePresigned, authTypeSigned:
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err := api.ObjectAPI.DeleteBucket(bucket)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "DeleteBucket failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case BucketNotFound:
|
||||
writeErrorResponse(w, r, ErrNoSuchBucket, r.URL.Path)
|
||||
case BucketNotEmpty:
|
||||
writeErrorResponse(w, r, ErrBucketNotEmpty, r.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
}
|
||||
// Attempt to delete bucket.
|
||||
if err := api.ObjectAPI.DeleteBucket(bucket); err != nil {
|
||||
errorIf(err, "Unable to delete a bucket.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete bucket access policy, if present - ignore any errors.
|
||||
removeBucketPolicy(bucket)
|
||||
removeBucketPolicy(bucket, api.ObjectAPI)
|
||||
|
||||
// Delete notification config, if present - ignore any errors.
|
||||
removeNotificationConfig(bucket, api.ObjectAPI)
|
||||
|
||||
// Write success response.
|
||||
writeSuccessNoContent(w)
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Represents the criteria for the filter rule.
|
||||
type filterRule struct {
|
||||
Name string `xml:"Name"`
|
||||
Value string `xml:"Value"`
|
||||
}
|
||||
|
||||
// Collection of filter rules per service config.
|
||||
type keyFilter struct {
|
||||
FilterRules []filterRule `xml:"FilterRule,omitempty"`
|
||||
}
|
||||
|
||||
// Queue SQS configuration.
|
||||
type queueConfig struct {
|
||||
Events []string `xml:"Event"`
|
||||
Filter struct {
|
||||
Key keyFilter `xml:"S3Key,omitempty"`
|
||||
}
|
||||
ID string `xml:"Id"`
|
||||
QueueARN string `xml:"Queue"`
|
||||
}
|
||||
|
||||
// Topic SNS configuration, this is a compliance field not used by minio yet.
|
||||
type topicConfig struct {
|
||||
Events []string `xml:"Event"`
|
||||
Filter struct {
|
||||
Key keyFilter `xml:"S3Key"`
|
||||
}
|
||||
ID string `xml:"Id"`
|
||||
TopicARN string `xml:"Topic"`
|
||||
}
|
||||
|
||||
// Lambda function configuration, this is a compliance field not used by minio yet.
|
||||
type lambdaConfig struct {
|
||||
Events []string `xml:"Event"`
|
||||
Filter struct {
|
||||
Key keyFilter `xml:"S3Key,omitempty"`
|
||||
}
|
||||
ID string `xml:"Id"`
|
||||
LambdaARN string `xml:"CloudFunction"`
|
||||
}
|
||||
|
||||
// Notification configuration structure represents the XML format of
|
||||
// notification configuration of buckets.
|
||||
type notificationConfig struct {
|
||||
XMLName xml.Name `xml:"NotificationConfiguration"`
|
||||
QueueConfigs []queueConfig `xml:"QueueConfiguration"`
|
||||
TopicConfigs []topicConfig `xml:"TopicConfiguration"`
|
||||
LambdaConfigs []lambdaConfig `xml:"CloudFunctionConfiguration"`
|
||||
}
|
||||
|
||||
// Internal error used to signal notifications not set.
|
||||
var errNoSuchNotifications = errors.New("The specified bucket does not have bucket notifications")
|
||||
|
||||
// EventName is AWS S3 event type:
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html
|
||||
type EventName int
|
||||
|
||||
const (
|
||||
// ObjectCreatedPut is s3:ObjectCreated:Put
|
||||
ObjectCreatedPut EventName = iota
|
||||
// ObjectCreatedPost is s3:ObjectCreated:Post
|
||||
ObjectCreatedPost
|
||||
// ObjectCreatedCopy is s3:ObjectCreated:Copy
|
||||
ObjectCreatedCopy
|
||||
// ObjectCreatedCompleteMultipartUpload is s3:ObjectCreated:CompleteMultipartUpload
|
||||
ObjectCreatedCompleteMultipartUpload
|
||||
// ObjectRemovedDelete is s3:ObjectRemoved:Delete
|
||||
ObjectRemovedDelete
|
||||
)
|
||||
|
||||
// Stringer interface for event name.
|
||||
func (eventName EventName) String() string {
|
||||
switch eventName {
|
||||
case ObjectCreatedPut:
|
||||
return "s3:ObjectCreated:Put"
|
||||
case ObjectCreatedPost:
|
||||
return "s3:ObjectCreated:Post"
|
||||
case ObjectCreatedCopy:
|
||||
return "s3:ObjectCreated:Copy"
|
||||
case ObjectCreatedCompleteMultipartUpload:
|
||||
return "s3:ObjectCreated:CompleteMultipartUpload"
|
||||
case ObjectRemovedDelete:
|
||||
return "s3:ObjectRemoved:Delete"
|
||||
default:
|
||||
return "s3:Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Indentity represents the user id, this is a compliance field.
|
||||
type identity struct {
|
||||
PrincipalID string `json:"principalId"`
|
||||
}
|
||||
|
||||
func defaultIdentity() identity {
|
||||
return identity{"minio"}
|
||||
}
|
||||
|
||||
// Notification event bucket metadata.
|
||||
type bucketMeta struct {
|
||||
Name string `json:"name"`
|
||||
OwnerIdentity identity `json:"ownerIdentity"`
|
||||
ARN string `json:"arn"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// Notification event server specific metadata.
|
||||
type eventMeta struct {
|
||||
SchemaVersion string `json:"s3SchemaVersion"`
|
||||
ConfigurationID string `json:"configurationId"`
|
||||
Bucket bucketMeta `json:"bucket"`
|
||||
Object objectMeta `json:"object"`
|
||||
}
|
||||
|
||||
// NotificationEvent represents an Amazon an S3 bucket notification event.
|
||||
type NotificationEvent struct {
|
||||
EventVersion string `json:"eventVersion"`
|
||||
EventSource string `json:"eventSource"`
|
||||
AwsRegion string `json:"awsRegion"`
|
||||
EventTime string `json:"eventTime"`
|
||||
EventName string `json:"eventName"`
|
||||
UserIdentity identity `json:"userIdentity"`
|
||||
RequestParameters map[string]string `json:"requestParameters"`
|
||||
ResponseElements map[string]string `json:"responseElements"`
|
||||
S3 eventMeta `json:"s3"`
|
||||
}
|
||||
|
||||
// Represents the minio topic type and account id's.
|
||||
type arnTopic struct {
|
||||
Type string
|
||||
AccountID string
|
||||
}
|
||||
|
||||
// Stringer for constructing AWS ARN compatible string.
|
||||
func (m arnTopic) String() string {
|
||||
return minioTopic + serverConfig.GetRegion() + ":" + m.AccountID + ":" + m.Type
|
||||
}
|
||||
|
||||
// Represents the minio sqs type and account id's.
|
||||
type arnSQS struct {
|
||||
Type string
|
||||
AccountID string
|
||||
}
|
||||
|
||||
// Stringer for constructing AWS ARN compatible string.
|
||||
func (m arnSQS) String() string {
|
||||
return minioSqs + serverConfig.GetRegion() + ":" + m.AccountID + ":" + m.Type
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const (
|
||||
bucketConfigPrefix = "buckets"
|
||||
bucketNotificationConfig = "notification.xml"
|
||||
)
|
||||
|
||||
// GetBucketNotificationHandler - This implementation of the GET
|
||||
// operation uses the notification subresource to return the
|
||||
// notification configuration of a bucket. If notifications are
|
||||
// not enabled on the bucket, the operation returns an empty
|
||||
// NotificationConfiguration element.
|
||||
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Validate request authorization.
|
||||
if s3Error := checkAuth(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
// Attempt to successfully load notification config.
|
||||
nConfig, err := loadNotificationConfig(bucket, api.ObjectAPI)
|
||||
if err != nil && err != errNoSuchNotifications {
|
||||
errorIf(err, "Unable to read notification configuration.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
// For no notifications we write a dummy XML.
|
||||
if err == errNoSuchNotifications {
|
||||
// Complies with the s3 behavior in this regard.
|
||||
nConfig = ¬ificationConfig{}
|
||||
}
|
||||
notificationBytes, err := xml.Marshal(nConfig)
|
||||
if err != nil {
|
||||
// For any marshalling failure.
|
||||
errorIf(err, "Unable to marshal notification configuration into XML.", err)
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
// Success.
|
||||
writeSuccessResponse(w, notificationBytes)
|
||||
}
|
||||
|
||||
// PutBucketNotificationHandler - Minio notification feature enables
|
||||
// you to receive notifications when certain events happen in your bucket.
|
||||
// Using this API, you can replace an existing notification configuration.
|
||||
// The configuration is an XML file that defines the event types that you
|
||||
// want Minio to publish and the destination where you want Minio to publish
|
||||
// an event notification when it detects an event of the specified type.
|
||||
// By default, your bucket has no event notifications configured. That is,
|
||||
// the notification configuration will be an empty NotificationConfiguration.
|
||||
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Validate request authorization.
|
||||
if s3Error := checkAuth(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
_, err := api.ObjectAPI.GetBucketInfo(bucket)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to bucket info.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// If Content-Length is unknown or zero, deny the request. PutBucketNotification
|
||||
// always needs a Content-Length if incoming request is not chunked.
|
||||
if !contains(r.TransferEncoding, "chunked") {
|
||||
if r.ContentLength == -1 {
|
||||
writeErrorResponse(w, r, ErrMissingContentLength, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Reads the incoming notification configuration.
|
||||
var buffer bytes.Buffer
|
||||
var bufferSize int64
|
||||
if r.ContentLength >= 0 {
|
||||
bufferSize, err = io.CopyN(&buffer, r.Body, r.ContentLength)
|
||||
} else {
|
||||
bufferSize, err = io.Copy(&buffer, r.Body)
|
||||
}
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to read incoming body.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
var notificationCfg notificationConfig
|
||||
// Unmarshal notification bytes.
|
||||
notificationConfigBytes := buffer.Bytes()
|
||||
if err = xml.Unmarshal(notificationConfigBytes, ¬ificationCfg); err != nil {
|
||||
errorIf(err, "Unable to parse notification configuration XML.")
|
||||
writeErrorResponse(w, r, ErrMalformedXML, r.URL.Path)
|
||||
return
|
||||
} // Successfully marshalled notification configuration.
|
||||
|
||||
// Validate unmarshalled bucket notification configuration.
|
||||
if s3Error := validateNotificationConfig(notificationCfg); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Proceed to save notification configuration.
|
||||
notificationConfigPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
||||
_, err = api.ObjectAPI.PutObject(minioMetaBucket, notificationConfigPath, bufferSize, bytes.NewReader(buffer.Bytes()), nil)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to write bucket notification configuration.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Set bucket notification config.
|
||||
eventN.SetBucketNotificationConfig(bucket, ¬ificationCfg)
|
||||
|
||||
// Success.
|
||||
writeSuccessResponse(w, nil)
|
||||
}
|
||||
|
||||
// writeNotification marshals notification message before writing to client.
|
||||
func writeNotification(w http.ResponseWriter, notification map[string][]NotificationEvent) error {
|
||||
// Invalid response writer.
|
||||
if w == nil {
|
||||
return errInvalidArgument
|
||||
}
|
||||
// Invalid notification input.
|
||||
if notification == nil {
|
||||
return errInvalidArgument
|
||||
}
|
||||
// Marshal notification data into XML and write to client.
|
||||
notificationBytes, err := json.Marshal(¬ification)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Add additional CRLF characters for client to
|
||||
// differentiate the individual events properly.
|
||||
_, err = w.Write(append(notificationBytes, crlf...))
|
||||
// Make sure we have flushed, this would set Transfer-Encoding: chunked.
|
||||
w.(http.Flusher).Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CRLF character used for chunked transfer in accordance with HTTP standards.
|
||||
var crlf = []byte("\r\n")
|
||||
|
||||
// sendBucketNotification - writes notification back to client on the response writer
|
||||
// for each notification input, otherwise writes whitespace characters periodically
|
||||
// to keep the connection active. Each notification messages are terminated by CRLF
|
||||
// character. Upon any error received on response writer the for loop exits.
|
||||
//
|
||||
// TODO - do not log for all errors.
|
||||
func sendBucketNotification(w http.ResponseWriter, arnListenerCh <-chan []NotificationEvent) {
|
||||
var dummyEvents = map[string][]NotificationEvent{"Records": nil}
|
||||
// Continuously write to client either timely empty structures
|
||||
// every 5 seconds, or return back the notifications.
|
||||
for {
|
||||
select {
|
||||
case events := <-arnListenerCh:
|
||||
if err := writeNotification(w, map[string][]NotificationEvent{"Records": events}); err != nil {
|
||||
errorIf(err, "Unable to write notification to client.")
|
||||
return
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
if err := writeNotification(w, dummyEvents); err != nil {
|
||||
errorIf(err, "Unable to write notification to client.")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ListenBucketNotificationHandler - list bucket notifications.
|
||||
func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Validate request authorization.
|
||||
if s3Error := checkAuth(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
// Get notification ARN.
|
||||
topicARN := r.URL.Query().Get("notificationARN")
|
||||
if topicARN == "" {
|
||||
writeErrorResponse(w, r, ErrARNNotification, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate if bucket exists.
|
||||
_, err := api.ObjectAPI.GetBucketInfo(bucket)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to bucket info.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
notificationCfg := eventN.GetBucketNotificationConfig(bucket)
|
||||
if notificationCfg == nil {
|
||||
writeErrorResponse(w, r, ErrARNNotification, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Set SNS notifications only if special "listen" sns is set in bucket
|
||||
// notification configs.
|
||||
if !isMinioSNSConfigured(topicARN, notificationCfg.TopicConfigs) {
|
||||
writeErrorResponse(w, r, ErrARNNotification, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Add all common headers.
|
||||
setCommonHeaders(w)
|
||||
|
||||
// Create a new notification event channel.
|
||||
nEventCh := make(chan []NotificationEvent)
|
||||
// Close the listener channel.
|
||||
defer close(nEventCh)
|
||||
|
||||
// Set sns target.
|
||||
eventN.SetSNSTarget(topicARN, nEventCh)
|
||||
// Remove sns listener after the writer has closed or the client disconnected.
|
||||
defer eventN.RemoveSNSTarget(topicARN, nEventCh)
|
||||
|
||||
// Start sending bucket notifications.
|
||||
sendBucketNotification(w, nEventCh)
|
||||
}
|
||||
|
||||
// Removes notification.xml for a given bucket, only used during DeleteBucket.
|
||||
func removeNotificationConfig(bucket string, objAPI ObjectLayer) error {
|
||||
// Verify bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
|
||||
notificationConfigPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
||||
return objAPI.DeleteObject(minioMetaBucket, notificationConfigPath)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Implement a dummy flush writer.
|
||||
type flushWriter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
// Flush writer is a dummy writer compatible with http.Flusher and http.ResponseWriter.
|
||||
func (f *flushWriter) Flush() {}
|
||||
func (f *flushWriter) Write(b []byte) (n int, err error) { return f.Writer.Write(b) }
|
||||
func (f *flushWriter) Header() http.Header { return http.Header{} }
|
||||
func (f *flushWriter) WriteHeader(code int) {}
|
||||
|
||||
func newFlushWriter(writer io.Writer) *flushWriter {
|
||||
return &flushWriter{writer}
|
||||
}
|
||||
|
||||
// Tests write notification code.
|
||||
func TestWriteNotification(t *testing.T) {
|
||||
// Initialize a new test config.
|
||||
root, err := newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize test config %s", err)
|
||||
}
|
||||
defer removeAll(root)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
// Collection of test cases for each event writer.
|
||||
testCases := []struct {
|
||||
writer *flushWriter
|
||||
event map[string][]NotificationEvent
|
||||
err error
|
||||
}{
|
||||
// Invalid input argument with writer `nil` - Test - 1
|
||||
{
|
||||
writer: nil,
|
||||
event: nil,
|
||||
err: errInvalidArgument,
|
||||
},
|
||||
// Invalid input argument with event `nil` - Test - 2
|
||||
{
|
||||
writer: newFlushWriter(ioutil.Discard),
|
||||
event: nil,
|
||||
err: errInvalidArgument,
|
||||
},
|
||||
// Unmarshal and write, validate last 5 bytes. - Test - 3
|
||||
{
|
||||
writer: newFlushWriter(&buffer),
|
||||
event: map[string][]NotificationEvent{
|
||||
"Records": {newNotificationEvent(eventData{
|
||||
Type: ObjectCreatedPut,
|
||||
Bucket: "testbucket",
|
||||
ObjInfo: ObjectInfo{
|
||||
Name: "key",
|
||||
},
|
||||
ReqParams: map[string]string{
|
||||
"ip": "10.1.10.1",
|
||||
}}),
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
// Validates all the testcases for writing notification.
|
||||
for _, testCase := range testCases {
|
||||
err := writeNotification(testCase.writer, testCase.event)
|
||||
if err != testCase.err {
|
||||
t.Errorf("Unable to write notification %s", err)
|
||||
}
|
||||
// Validates if the ending string has 'crlf'
|
||||
if err == nil && !bytes.HasSuffix(buffer.Bytes(), crlf) {
|
||||
buf := buffer.Bytes()[buffer.Len()-5 : 0]
|
||||
t.Errorf("Invalid suffix found from the writer last 5 bytes %s, expected `\r\n`", string(buf))
|
||||
}
|
||||
// Not printing 'buf' on purpose, validates look for string '10.1.10.1'.
|
||||
if err == nil && !bytes.Contains(buffer.Bytes(), []byte("10.1.10.1")) {
|
||||
// Enable when debugging)
|
||||
// fmt.Println(string(buffer.Bytes()))
|
||||
t.Errorf("Requested content couldn't be found, expected `10.1.10.1`")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
// List of valid event types.
|
||||
var suppportedEventTypes = map[string]struct{}{
|
||||
// Object created event types.
|
||||
"s3:ObjectCreated:*": {},
|
||||
"s3:ObjectCreated:Put": {},
|
||||
"s3:ObjectCreated:Post": {},
|
||||
"s3:ObjectCreated:Copy": {},
|
||||
"s3:ObjectCreated:CompleteMultipartUpload": {},
|
||||
// Object removed event types.
|
||||
"s3:ObjectRemoved:*": {},
|
||||
"s3:ObjectRemoved:Delete": {},
|
||||
}
|
||||
|
||||
// checkEvent - checks if an event is supported.
|
||||
func checkEvent(event string) APIErrorCode {
|
||||
_, ok := suppportedEventTypes[event]
|
||||
if !ok {
|
||||
return ErrEventNotification
|
||||
}
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// checkEvents - checks given list of events if all of them are valid.
|
||||
// given if one of them is invalid, this function returns an error.
|
||||
func checkEvents(events []string) APIErrorCode {
|
||||
for _, event := range events {
|
||||
if s3Error := checkEvent(event); s3Error != ErrNone {
|
||||
return s3Error
|
||||
}
|
||||
}
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Valid if filterName is 'prefix'.
|
||||
func isValidFilterNamePrefix(filterName string) bool {
|
||||
return "prefix" == filterName
|
||||
}
|
||||
|
||||
// Valid if filterName is 'suffix'.
|
||||
func isValidFilterNameSuffix(filterName string) bool {
|
||||
return "suffix" == filterName
|
||||
}
|
||||
|
||||
// Is this a valid filterName? - returns true if valid.
|
||||
func isValidFilterName(filterName string) bool {
|
||||
return isValidFilterNamePrefix(filterName) || isValidFilterNameSuffix(filterName)
|
||||
}
|
||||
|
||||
// checkFilterRules - checks given list of filter rules if all of them are valid.
|
||||
func checkFilterRules(filterRules []filterRule) APIErrorCode {
|
||||
ruleSetMap := make(map[string]string)
|
||||
// Validate all filter rules.
|
||||
for _, filterRule := range filterRules {
|
||||
// Unknown filter rule name found, returns an appropriate error.
|
||||
if !isValidFilterName(filterRule.Name) {
|
||||
return ErrFilterNameInvalid
|
||||
}
|
||||
|
||||
// Filter names should not be set twice per notification service
|
||||
// configuration, if found return an appropriate error.
|
||||
if _, ok := ruleSetMap[filterRule.Name]; ok {
|
||||
if isValidFilterNamePrefix(filterRule.Name) {
|
||||
return ErrFilterNamePrefix
|
||||
} else if isValidFilterNameSuffix(filterRule.Name) {
|
||||
return ErrFilterNameSuffix
|
||||
} else {
|
||||
return ErrFilterNameInvalid
|
||||
}
|
||||
}
|
||||
|
||||
// Maximum prefix length can be up to 1,024 characters, validate.
|
||||
if !IsValidObjectPrefix(filterRule.Value) {
|
||||
return ErrFilterPrefixValueInvalid
|
||||
}
|
||||
|
||||
// Set the new rule name to keep track of duplicates.
|
||||
ruleSetMap[filterRule.Name] = filterRule.Value
|
||||
}
|
||||
// Success all prefixes validated.
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// checkQueueARN - check if the queue arn is valid.
|
||||
func checkQueueARN(queueARN string) APIErrorCode {
|
||||
if !strings.HasPrefix(queueARN, minioSqs) {
|
||||
return ErrARNNotification
|
||||
}
|
||||
if !strings.HasPrefix(queueARN, minioSqs+serverConfig.GetRegion()+":") {
|
||||
return ErrRegionNotification
|
||||
}
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// checkTopicARN - check if the topic arn is valid.
|
||||
func checkTopicARN(topicARN string) APIErrorCode {
|
||||
if !strings.HasPrefix(topicARN, minioTopic) {
|
||||
return ErrARNNotification
|
||||
}
|
||||
if !strings.HasPrefix(topicARN, minioTopic+serverConfig.GetRegion()+":") {
|
||||
return ErrRegionNotification
|
||||
}
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Returns true if the topicARN is for an Minio sns listen type.
|
||||
func isMinioSNS(topicARN arnTopic) bool {
|
||||
return strings.HasSuffix(topicARN.Type, snsTypeMinio)
|
||||
}
|
||||
|
||||
// isMinioSNSConfigured - verifies if one topic ARN is valid and is enabled.
|
||||
func isMinioSNSConfigured(topicARN string, topicConfigs []topicConfig) bool {
|
||||
for _, topicConfig := range topicConfigs {
|
||||
// Validate if topic ARN is already enabled.
|
||||
if topicARN == topicConfig.TopicARN {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate if we recognize the queue type.
|
||||
func isValidQueue(sqsARN arnSQS) bool {
|
||||
amqpQ := isAMQPQueue(sqsARN) // Is amqp queue?.
|
||||
elasticQ := isElasticQueue(sqsARN) // Is elastic queue?.
|
||||
redisQ := isRedisQueue(sqsARN) // Is redis queue?.
|
||||
return amqpQ || elasticQ || redisQ
|
||||
}
|
||||
|
||||
// Validate if we recognize the topic type.
|
||||
func isValidTopic(topicARN arnTopic) bool {
|
||||
return isMinioSNS(topicARN) // Is minio topic?.
|
||||
}
|
||||
|
||||
// Validates account id for input queue ARN.
|
||||
func isValidQueueID(queueARN string) bool {
|
||||
// Unmarshals QueueARN into structured object.
|
||||
sqsARN := unmarshalSqsARN(queueARN)
|
||||
// AMQP queue.
|
||||
if isAMQPQueue(sqsARN) {
|
||||
amqpN := serverConfig.GetAMQPNotifyByID(sqsARN.AccountID)
|
||||
return amqpN.Enable && amqpN.URL != ""
|
||||
} else if isElasticQueue(sqsARN) { // Elastic queue.
|
||||
elasticN := serverConfig.GetElasticSearchNotifyByID(sqsARN.AccountID)
|
||||
return elasticN.Enable && elasticN.URL != ""
|
||||
|
||||
} else if isRedisQueue(sqsARN) { // Redis queue.
|
||||
redisN := serverConfig.GetRedisNotifyByID(sqsARN.AccountID)
|
||||
return redisN.Enable && redisN.Addr != ""
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check - validates queue configuration and returns error if any.
|
||||
func checkQueueConfig(qConfig queueConfig) APIErrorCode {
|
||||
// Check queue arn is valid.
|
||||
if s3Error := checkQueueARN(qConfig.QueueARN); s3Error != ErrNone {
|
||||
return s3Error
|
||||
}
|
||||
|
||||
// Unmarshals QueueARN into structured object.
|
||||
sqsARN := unmarshalSqsARN(qConfig.QueueARN)
|
||||
// Validate if sqsARN requested any of the known supported queues.
|
||||
if !isValidQueue(sqsARN) {
|
||||
return ErrARNNotification
|
||||
}
|
||||
|
||||
// Validate if the account ID is correct.
|
||||
if !isValidQueueID(qConfig.QueueARN) {
|
||||
return ErrARNNotification
|
||||
}
|
||||
|
||||
// Check if valid events are set in queue config.
|
||||
if s3Error := checkEvents(qConfig.Events); s3Error != ErrNone {
|
||||
return s3Error
|
||||
}
|
||||
|
||||
// Check if valid filters are set in queue config.
|
||||
if s3Error := checkFilterRules(qConfig.Filter.Key.FilterRules); s3Error != ErrNone {
|
||||
return s3Error
|
||||
}
|
||||
|
||||
// Success.
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Check - validates queue configuration and returns error if any.
|
||||
func checkTopicConfig(tConfig topicConfig) APIErrorCode {
|
||||
// Check queue arn is valid.
|
||||
if s3Error := checkTopicARN(tConfig.TopicARN); s3Error != ErrNone {
|
||||
return s3Error
|
||||
}
|
||||
|
||||
// Unmarshals QueueARN into structured object.
|
||||
topicARN := unmarshalTopicARN(tConfig.TopicARN)
|
||||
// Validate if topicARN requested any of the known supported queues.
|
||||
if !isValidTopic(topicARN) {
|
||||
return ErrARNNotification
|
||||
}
|
||||
|
||||
// Check if valid events are set in queue config.
|
||||
if s3Error := checkEvents(tConfig.Events); s3Error != ErrNone {
|
||||
return s3Error
|
||||
}
|
||||
|
||||
// Check if valid filters are set in queue config.
|
||||
if s3Error := checkFilterRules(tConfig.Filter.Key.FilterRules); s3Error != ErrNone {
|
||||
return s3Error
|
||||
}
|
||||
|
||||
// Success.
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Validates all incoming queue configs, checkQueueConfig validates if the
|
||||
// input fields for each queues is not malformed and has valid configuration
|
||||
// information. If validation fails bucket notifications are not enabled.
|
||||
func validateQueueConfigs(queueConfigs []queueConfig) APIErrorCode {
|
||||
for _, qConfig := range queueConfigs {
|
||||
if s3Error := checkQueueConfig(qConfig); s3Error != ErrNone {
|
||||
return s3Error
|
||||
}
|
||||
}
|
||||
// Success.
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Validates all incoming topic configs, checkTopicConfig validates if the
|
||||
// input fields for each queues is not malformed and has valid configuration
|
||||
// information. If validation fails bucket notifications are not enabled.
|
||||
func validateTopicConfigs(topicConfigs []topicConfig) APIErrorCode {
|
||||
for _, tConfig := range topicConfigs {
|
||||
if s3Error := checkTopicConfig(tConfig); s3Error != ErrNone {
|
||||
return s3Error
|
||||
}
|
||||
}
|
||||
// Success.
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Validates all the bucket notification configuration for their validity,
|
||||
// if one of the config is malformed or has invalid data it is rejected.
|
||||
// Configuration is never applied partially.
|
||||
func validateNotificationConfig(nConfig notificationConfig) APIErrorCode {
|
||||
if s3Error := validateQueueConfigs(nConfig.QueueConfigs); s3Error != ErrNone {
|
||||
return s3Error
|
||||
}
|
||||
if s3Error := validateTopicConfigs(nConfig.TopicConfigs); s3Error != ErrNone {
|
||||
return s3Error
|
||||
}
|
||||
|
||||
// Add validation for other configurations.
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Unmarshals input value of AWS ARN format into minioTopic object.
|
||||
// Returned value represents minio topic type, currently supported are
|
||||
// - listen
|
||||
func unmarshalTopicARN(topicARN string) arnTopic {
|
||||
topic := arnTopic{}
|
||||
if !strings.HasPrefix(topicARN, minioTopic+serverConfig.GetRegion()+":") {
|
||||
return topic
|
||||
}
|
||||
topicType := strings.TrimPrefix(topicARN, minioTopic+serverConfig.GetRegion()+":")
|
||||
switch {
|
||||
case strings.HasSuffix(topicType, snsTypeMinio):
|
||||
topic.Type = snsTypeMinio
|
||||
} // Add more topic here.
|
||||
topic.AccountID = strings.TrimSuffix(topicType, ":"+topic.Type)
|
||||
return topic
|
||||
}
|
||||
|
||||
// Unmarshals input value of AWS ARN format into minioSqs object.
|
||||
// Returned value represents minio sqs types, currently supported are
|
||||
// - amqp
|
||||
// - elasticsearch
|
||||
// - redis
|
||||
func unmarshalSqsARN(queueARN string) (mSqs arnSQS) {
|
||||
mSqs = arnSQS{}
|
||||
if !strings.HasPrefix(queueARN, minioSqs+serverConfig.GetRegion()+":") {
|
||||
return mSqs
|
||||
}
|
||||
sqsType := strings.TrimPrefix(queueARN, minioSqs+serverConfig.GetRegion()+":")
|
||||
switch {
|
||||
case strings.HasSuffix(sqsType, queueTypeAMQP):
|
||||
mSqs.Type = queueTypeAMQP
|
||||
case strings.HasSuffix(sqsType, queueTypeElastic):
|
||||
mSqs.Type = queueTypeElastic
|
||||
case strings.HasSuffix(sqsType, queueTypeRedis):
|
||||
mSqs.Type = queueTypeRedis
|
||||
} // Add more queues here.
|
||||
mSqs.AccountID = strings.TrimSuffix(sqsType, ":"+mSqs.Type)
|
||||
return mSqs
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
// Tests filter name validation.
|
||||
func TestIsValidFilterName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
filterName string
|
||||
status bool
|
||||
}{
|
||||
// Validate if 'prefix' is correct.
|
||||
{
|
||||
filterName: "prefix",
|
||||
status: true,
|
||||
},
|
||||
// Validate if 'suffix' is correct.
|
||||
{
|
||||
filterName: "suffix",
|
||||
status: true,
|
||||
},
|
||||
// Invalid filter name empty string should return false.
|
||||
{
|
||||
filterName: "",
|
||||
status: false,
|
||||
},
|
||||
// Invalid filter name random character should return false.
|
||||
{
|
||||
filterName: "unknown",
|
||||
status: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
status := isValidFilterName(testCase.filterName)
|
||||
if testCase.status != status {
|
||||
t.Errorf("Test %d: Expected \"%t\", got \"%t\"", i+1, testCase.status, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests list of valid and invalid events.
|
||||
func TestValidEvents(t *testing.T) {
|
||||
testCases := []struct {
|
||||
events []string
|
||||
errCode APIErrorCode
|
||||
}{
|
||||
// Return error for unknown event element.
|
||||
{
|
||||
events: []string{
|
||||
"s3:UnknownAPI",
|
||||
},
|
||||
errCode: ErrEventNotification,
|
||||
},
|
||||
// Return success for supported event.
|
||||
{
|
||||
events: []string{
|
||||
"s3:ObjectCreated:Put",
|
||||
},
|
||||
errCode: ErrNone,
|
||||
},
|
||||
// Return success for supported events.
|
||||
{
|
||||
events: []string{
|
||||
"s3:ObjectCreated:*",
|
||||
"s3:ObjectRemoved:*",
|
||||
},
|
||||
errCode: ErrNone,
|
||||
},
|
||||
// Return error for empty event list.
|
||||
{
|
||||
events: []string{""},
|
||||
errCode: ErrEventNotification,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
errCode := checkEvents(testCase.events)
|
||||
if testCase.errCode != errCode {
|
||||
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests topic arn validation.
|
||||
func TestTopicARN(t *testing.T) {
|
||||
rootPath, err := newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
|
||||
testCases := []struct {
|
||||
topicARN string
|
||||
errCode APIErrorCode
|
||||
}{
|
||||
// Valid minio topic with '1' account id.
|
||||
{
|
||||
topicARN: "arn:minio:sns:us-east-1:1:minio",
|
||||
errCode: ErrNone,
|
||||
},
|
||||
// Valid minio topic with '10' account id.
|
||||
{
|
||||
topicARN: "arn:minio:sns:us-east-1:10:minio",
|
||||
errCode: ErrNone,
|
||||
},
|
||||
// Invalid empty queue arn.
|
||||
{
|
||||
topicARN: "",
|
||||
errCode: ErrARNNotification,
|
||||
},
|
||||
// Invalid region 'us-west-1' in queue arn.
|
||||
{
|
||||
topicARN: "arn:minio:sns:us-west-1:1:redis",
|
||||
errCode: ErrRegionNotification,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
errCode := checkTopicARN(testCase.topicARN)
|
||||
if testCase.errCode != errCode {
|
||||
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests queue arn validation.
|
||||
func TestQueueARN(t *testing.T) {
|
||||
rootPath, err := newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
|
||||
testCases := []struct {
|
||||
queueARN string
|
||||
errCode APIErrorCode
|
||||
}{
|
||||
// Valid redis queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:redis",
|
||||
errCode: ErrNone,
|
||||
},
|
||||
// Valid elasticsearch queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:elasticsearch",
|
||||
errCode: ErrNone,
|
||||
},
|
||||
// Valid amqp queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:amqp",
|
||||
errCode: ErrNone,
|
||||
},
|
||||
// Invalid empty queue arn.
|
||||
{
|
||||
queueARN: "",
|
||||
errCode: ErrARNNotification,
|
||||
},
|
||||
// Invalid region 'us-west-1' in queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-west-1:1:redis",
|
||||
errCode: ErrRegionNotification,
|
||||
},
|
||||
}
|
||||
|
||||
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 topic arn.
|
||||
func TestUnmarshalTopicARN(t *testing.T) {
|
||||
rootPath, err := newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
|
||||
testCases := []struct {
|
||||
topicARN string
|
||||
Type string
|
||||
}{
|
||||
// Valid minio topic arn.
|
||||
{
|
||||
topicARN: "arn:minio:sns:us-east-1:1:listen",
|
||||
Type: "listen",
|
||||
},
|
||||
// Invalid empty topic arn.
|
||||
{
|
||||
topicARN: "",
|
||||
Type: "",
|
||||
},
|
||||
// Invalid region 'us-west-1' in topic arn.
|
||||
{
|
||||
topicARN: "arn:minio:sns:us-west-1:1:listen",
|
||||
Type: "",
|
||||
},
|
||||
// Partial topic arn.
|
||||
{
|
||||
topicARN: "arn:minio:sns:",
|
||||
Type: "",
|
||||
},
|
||||
// Invalid topic service value.
|
||||
{
|
||||
topicARN: "arn:minio:sns:us-east-1:1:*",
|
||||
Type: "",
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
topic := unmarshalTopicARN(testCase.topicARN)
|
||||
if testCase.Type != topic.Type {
|
||||
t.Errorf("Test %d: Expected \"%s\", got \"%s\"", i+1, testCase.Type, topic.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test unmarshal queue arn.
|
||||
func TestUnmarshalSqsARN(t *testing.T) {
|
||||
rootPath, err := newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
|
||||
testCases := []struct {
|
||||
queueARN string
|
||||
Type string
|
||||
}{
|
||||
// Valid redis queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:redis",
|
||||
Type: "redis",
|
||||
},
|
||||
// Valid elasticsearch queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:elasticsearch",
|
||||
Type: "elasticsearch",
|
||||
},
|
||||
// Valid amqp queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:amqp",
|
||||
Type: "amqp",
|
||||
},
|
||||
// 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:",
|
||||
Type: "",
|
||||
},
|
||||
// Invalid queue service value.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:*",
|
||||
Type: "",
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+54
-38
@@ -18,18 +18,17 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
mux "github.com/gorilla/mux"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
)
|
||||
|
||||
// maximum supported access policy size.
|
||||
const maxAccessPolicySize = 20 * 1024 * 1024 // 20KiB.
|
||||
const maxAccessPolicySize = 20 * 1024 // 20KiB.
|
||||
|
||||
// Verify if a given action is valid for the url path based on the
|
||||
// existing bucket access policy.
|
||||
@@ -66,23 +65,31 @@ func bucketPolicyMatchStatement(action string, resource string, conditions map[s
|
||||
// Verify if given action matches with policy statement.
|
||||
func bucketPolicyActionMatch(action string, statement policyStatement) bool {
|
||||
for _, policyAction := range statement.Actions {
|
||||
// Policy action can be a regex, validate the action with matching string.
|
||||
matched, e := regexp.MatchString(policyAction, action)
|
||||
fatalIf(probe.NewError(e), "Invalid pattern, please verify the pattern string.", nil)
|
||||
if matched {
|
||||
if matched := actionMatch(policyAction, action); matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Match function matches wild cards in 'pattern' for resource.
|
||||
func resourceMatch(pattern, resource string) bool {
|
||||
return wildcard.MatchExtended(pattern, resource)
|
||||
}
|
||||
|
||||
// Match function matches wild cards in 'pattern' for action.
|
||||
func actionMatch(pattern, action string) bool {
|
||||
return wildcard.Match(pattern, action)
|
||||
}
|
||||
|
||||
// Verify if given resource matches with policy statement.
|
||||
func bucketPolicyResourceMatch(resource string, statement policyStatement) bool {
|
||||
for _, presource := range statement.Resources {
|
||||
matched, e := regexp.MatchString(presource, strings.TrimPrefix(resource, "/"))
|
||||
fatalIf(probe.NewError(e), "Invalid pattern, please verify the pattern string.", nil)
|
||||
// For any path matches, we return quickly and the let the caller continue.
|
||||
if matched {
|
||||
for _, resourcep := range statement.Resources {
|
||||
// the resource rule for object could contain "*" wild card.
|
||||
// the requested object can be given access based on the already set bucket policy if
|
||||
// the match is successful.
|
||||
// More info: http://docs.aws.amazon.com/AmazonS3/latest/dev/s3-arn-format.html .
|
||||
if matched := resourceMatch(resourcep, resource); matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -127,10 +134,9 @@ func bucketPolicyConditionMatch(conditions map[string]string, statement policySt
|
||||
// -----------------
|
||||
// This implementation of the PUT operation uses the policy
|
||||
// subresource to add to or replace a policy on a bucket
|
||||
func (api objectStorageAPI) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
@@ -161,32 +167,31 @@ func (api objectStorageAPI) PutBucketPolicyHandler(w http.ResponseWriter, r *htt
|
||||
// Read access policy up to maxAccessPolicySize.
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
|
||||
// bucket policies are limited to 20KB in size, using a limit reader.
|
||||
bucketPolicyBuf, e := ioutil.ReadAll(io.LimitReader(r.Body, maxAccessPolicySize))
|
||||
if e != nil {
|
||||
errorIf(probe.NewError(e).Trace(bucket), "Reading policy failed.", nil)
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
policyBytes, err := ioutil.ReadAll(io.LimitReader(r.Body, maxAccessPolicySize))
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to read from client.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse bucket policy.
|
||||
bucketPolicy, e := parseBucketPolicy(bucketPolicyBuf)
|
||||
if e != nil {
|
||||
errorIf(probe.NewError(e), "Unable to parse bucket policy.", nil)
|
||||
var policy = &bucketPolicy{}
|
||||
err = parseBucketPolicy(bytes.NewReader(policyBytes), policy)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to parse bucket policy.")
|
||||
writeErrorResponse(w, r, ErrInvalidPolicyDocument, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse check bucket policy.
|
||||
if s3Error := checkBucketPolicy(bucket, bucketPolicy); s3Error != ErrNone {
|
||||
if s3Error := checkBucketPolicyResources(bucket, policy); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Save bucket policy.
|
||||
err := writeBucketPolicy(bucket, bucketPolicyBuf)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(bucket, string(bucketPolicyBuf)), "SaveBucketPolicy failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
if err = writeBucketPolicy(bucket, api.ObjectAPI, bytes.NewReader(policyBytes), int64(len(policyBytes))); err != nil {
|
||||
errorIf(err, "Unable to write bucket policy.")
|
||||
switch err.(type) {
|
||||
case BucketNameInvalid:
|
||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
||||
default:
|
||||
@@ -194,6 +199,11 @@ func (api objectStorageAPI) PutBucketPolicyHandler(w http.ResponseWriter, r *htt
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Set the bucket policy in memory.
|
||||
globalBucketPolicies.SetBucketPolicy(bucket, policy)
|
||||
|
||||
// Success.
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
@@ -201,7 +211,7 @@ func (api objectStorageAPI) PutBucketPolicyHandler(w http.ResponseWriter, r *htt
|
||||
// -----------------
|
||||
// This implementation of the DELETE operation uses the policy
|
||||
// subresource to add to remove a policy on a bucket.
|
||||
func (api objectStorageAPI) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
@@ -218,10 +228,9 @@ func (api objectStorageAPI) DeleteBucketPolicyHandler(w http.ResponseWriter, r *
|
||||
}
|
||||
|
||||
// Delete bucket access policy.
|
||||
err := removeBucketPolicy(bucket)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(bucket), "DeleteBucketPolicy failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
if err := removeBucketPolicy(bucket, api.ObjectAPI); err != nil {
|
||||
errorIf(err, "Unable to remove bucket policy.")
|
||||
switch err.(type) {
|
||||
case BucketNameInvalid:
|
||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
||||
case BucketPolicyNotFound:
|
||||
@@ -231,6 +240,11 @@ func (api objectStorageAPI) DeleteBucketPolicyHandler(w http.ResponseWriter, r *
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Remove bucket policy.
|
||||
globalBucketPolicies.RemoveBucketPolicy(bucket)
|
||||
|
||||
// Success.
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
@@ -238,7 +252,7 @@ func (api objectStorageAPI) DeleteBucketPolicyHandler(w http.ResponseWriter, r *
|
||||
// -----------------
|
||||
// This operation uses the policy
|
||||
// subresource to return the policy of a specified bucket.
|
||||
func (api objectStorageAPI) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
@@ -255,10 +269,10 @@ func (api objectStorageAPI) GetBucketPolicyHandler(w http.ResponseWriter, r *htt
|
||||
}
|
||||
|
||||
// Read bucket access policy.
|
||||
p, err := readBucketPolicy(bucket)
|
||||
policy, err := readBucketPolicy(bucket, api.ObjectAPI)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(bucket), "GetBucketPolicy failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
errorIf(err, "Unable to read bucket policy.")
|
||||
switch err.(type) {
|
||||
case BucketNameInvalid:
|
||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
||||
case BucketPolicyNotFound:
|
||||
@@ -268,5 +282,7 @@ func (api objectStorageAPI) GetBucketPolicyHandler(w http.ResponseWriter, r *htt
|
||||
}
|
||||
return
|
||||
}
|
||||
io.Copy(w, bytes.NewReader(p))
|
||||
|
||||
// Write to client.
|
||||
fmt.Fprint(w, policy)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests validate Bucket policy resource matcher.
|
||||
func TestBucketPolicyResourceMatch(t *testing.T) {
|
||||
|
||||
// generates statement with given resource..
|
||||
generateStatement := func(resource string) policyStatement {
|
||||
statement := policyStatement{}
|
||||
statement.Resources = []string{resource}
|
||||
return statement
|
||||
}
|
||||
|
||||
// generates resource prefix.
|
||||
generateResource := func(bucketName, objectName string) string {
|
||||
return AWSResourcePrefix + bucketName + "/" + objectName
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
resourceToMatch string
|
||||
statement policyStatement
|
||||
expectedResourceMatch bool
|
||||
}{
|
||||
// Test case 1-4.
|
||||
// Policy with resource ending with bucket/* allows access to all objects inside the given bucket.
|
||||
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true},
|
||||
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true},
|
||||
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true},
|
||||
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true},
|
||||
// Test case - 5.
|
||||
// Policy with resource ending with bucket/oo* should not allow access to bucket/output.txt.
|
||||
{generateResource("minio-bucket", "output.txt"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/oo*")), false},
|
||||
// Test case - 6.
|
||||
// Policy with resource ending with bucket/oo* should allow access to bucket/ootput.txt.
|
||||
{generateResource("minio-bucket", "ootput.txt"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/oo*")), true},
|
||||
// Test case - 7.
|
||||
// Policy with resource ending with bucket/oo* allows access to all subfolders starting with "oo" inside given bucket.
|
||||
{generateResource("minio-bucket", "oop-bucket/my-file"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/oo*")), true},
|
||||
// Test case - 8.
|
||||
{generateResource("minio-bucket", "Asia/India/1.pjg"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/Asia/Japan/*")), false},
|
||||
// Test case - 9.
|
||||
{generateResource("minio-bucket", "Asia/India/1.pjg"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/Asia/Japan/*")), false},
|
||||
// Test case - 10.
|
||||
// Proves that the name space is flat.
|
||||
{generateResource("minio-bucket", "Africa/Bihar/India/design_info.doc/Bihar"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix,
|
||||
"minio-bucket"+"/*/India/*/Bihar")), true},
|
||||
// Test case - 11.
|
||||
// Proves that the name space is flat.
|
||||
{generateResource("minio-bucket", "Asia/China/India/States/Bihar/output.txt"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix,
|
||||
"minio-bucket"+"/*/India/*/Bihar/*")), true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
actualResourceMatch := bucketPolicyResourceMatch(testCase.resourceToMatch, testCase.statement)
|
||||
if testCase.expectedResourceMatch != actualResourceMatch {
|
||||
t.Errorf("Test %d: Expected Resource match to be `%v`, but instead found it to be `%v`", i+1, testCase.expectedResourceMatch, actualResourceMatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBucketPolicyActionMatch - Test validates whether given action on the
|
||||
// bucket/object matches the allowed actions in policyStatement.
|
||||
// This test preserves the allowed actions for all 3 sets of policies, that is read-write,read-only, write-only.
|
||||
// The intention of the test is to catch any changes made to allowed action for on eof the above 3 major policy groups mentioned.
|
||||
func TestBucketPolicyActionMatch(t *testing.T) {
|
||||
bucketName := getRandomBucketName()
|
||||
objectPrefix := "test-object"
|
||||
|
||||
testCases := []struct {
|
||||
action string
|
||||
statement policyStatement
|
||||
expectedResult bool
|
||||
}{
|
||||
// s3:GetBucketLocation is the action necessary to be present in the bucket policy to allow
|
||||
// fetching of bucket location on an Anonymous/unsigned request.
|
||||
|
||||
//r ead-write bucket policy is expected to allow GetBucketLocation operation on an anonymous request (Test case - 1).
|
||||
{"s3:GetBucketLocation", getReadWriteBucketStatement(bucketName, objectPrefix), true},
|
||||
// write-only bucket policy is expected to allow GetBucketLocation operation on an anonymous request (Test case - 2).
|
||||
{"s3:GetBucketLocation", getWriteOnlyBucketStatement(bucketName, objectPrefix), true},
|
||||
// read-only bucket policy is expected to allow GetBucketLocation operation on an anonymous request (Test case - 3).
|
||||
{"s3:GetBucketLocation", getReadOnlyBucketStatement(bucketName, objectPrefix), true},
|
||||
|
||||
// Any of the Object level access permissions shouldn't allow for GetBucketLocation operation on an Anonymous/unsigned request (Test cases 4-6).
|
||||
{"s3:GetBucketLocation", getReadWriteObjectStatement(bucketName, objectPrefix), false},
|
||||
{"s3:GetBucketLocation", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
|
||||
{"s3:GetBucketLocation", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
||||
|
||||
// s3:ListBucketMultipartUploads is the action necessary to be present in the bucket policy to allow
|
||||
// Listing of multipart uploads in a given bucket for an Anonymous/unsigned request.
|
||||
|
||||
//read-write bucket policy is expected to allow ListBucketMultipartUploads operation on an anonymous request (Test case 7).
|
||||
{"s3:ListBucketMultipartUploads", getReadWriteBucketStatement(bucketName, objectPrefix), true},
|
||||
// write-only bucket policy is expected to allow ListBucketMultipartUploads operation on an anonymous request (Test case 8).
|
||||
{"s3:ListBucketMultipartUploads", getWriteOnlyBucketStatement(bucketName, objectPrefix), true},
|
||||
// read-only bucket policy is expected to not allow ListBucketMultipartUploads operation on an anonymous request (Test case 9).
|
||||
// the allowed actions in read-only bucket statement are "s3:GetBucketLocation","s3:ListBucket",
|
||||
// this should not allow for ListBucketMultipartUploads operations.
|
||||
{"s3:ListBucketMultipartUploads", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
|
||||
|
||||
// Any of the object level policy will not allow for s3:ListBucketMultipartUploads (Test cases 10-12).
|
||||
{"s3:ListBucketMultipartUploads", getReadWriteObjectStatement(bucketName, objectPrefix), false},
|
||||
{"s3:ListBucketMultipartUploads", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
|
||||
{"s3:ListBucketMultipartUploads", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
||||
|
||||
// s3:ListBucket is the action necessary to be present in the bucket policy to allow
|
||||
// listing of all objects inside a given bucket on an Anonymous/unsigned request.
|
||||
|
||||
// Cases for testing ListBucket access for different Bucket level access permissions.
|
||||
// read-only bucket policy is expected to allow ListBucket operation on an anonymous request (Test case 13).
|
||||
{"s3:ListBucket", getReadOnlyBucketStatement(bucketName, objectPrefix), true},
|
||||
// read-write bucket policy is expected to allow ListBucket operation on an anonymous request (Test case 14).
|
||||
{"s3:ListBucket", getReadWriteBucketStatement(bucketName, objectPrefix), true},
|
||||
// write-only bucket policy is expected to not allow ListBucket operation on an anonymous request (Test case 15).
|
||||
// the allowed actions in write-only bucket statement are "s3:GetBucketLocation", "s3:ListBucketMultipartUploads",
|
||||
// this should not allow for ListBucket operations.
|
||||
{"s3:ListBucket", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
|
||||
|
||||
// Cases for testing ListBucket access for different Object level access permissions (Test cases 16-18).
|
||||
// Any of the Object level access permissions shouldn't allow for ListBucket operation on an Anonymous/unsigned request.
|
||||
{"s3:ListBucket", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
||||
{"s3:ListBucket", getReadWriteObjectStatement(bucketName, objectPrefix), false},
|
||||
{"s3:ListBucket", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
|
||||
|
||||
// s3:DeleteObject is the action necessary to be present in the bucket policy to allow
|
||||
// deleting/removal of objects inside a given bucket for an Anonymous/unsigned request.
|
||||
|
||||
// Cases for testing DeleteObject access for different Bucket level access permissions (Test cases 19-21).
|
||||
// Any of the Bucket level access permissions shouldn't allow for DeleteObject operation on an Anonymous/unsigned request.
|
||||
{"s3:DeleteObject", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
|
||||
{"s3:DeleteObject", getReadWriteBucketStatement(bucketName, objectPrefix), false},
|
||||
{"s3:DeleteObject", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
|
||||
|
||||
// Cases for testing DeleteObject access for different Object level access permissions (Test cases 22).
|
||||
// read-only bucket policy is expected to not allow Delete Object operation on an anonymous request.
|
||||
{"s3:DeleteObject", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
||||
// read-write bucket policy is expected to allow Delete Bucket operation on an anonymous request (Test cases 23).
|
||||
{"s3:DeleteObject", getReadWriteObjectStatement(bucketName, objectPrefix), true},
|
||||
// write-only bucket policy is expected to allow Delete Object operation on an anonymous request (Test cases 24).
|
||||
{"s3:DeleteObject", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
|
||||
|
||||
// s3:AbortMultipartUpload is the action necessary to be present in the bucket policy to allow
|
||||
// cancelling or abortion of an already initiated multipart upload operation for an Anonymous/unsigned request.
|
||||
|
||||
// Cases for testing AbortMultipartUpload access for different Bucket level access permissions (Test cases 25-27).
|
||||
// Any of the Bucket level access permissions shouldn't allow for AbortMultipartUpload operation on an Anonymous/unsigned request.
|
||||
{"s3:AbortMultipartUpload", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
|
||||
{"s3:AbortMultipartUpload", getReadWriteBucketStatement(bucketName, objectPrefix), false},
|
||||
{"s3:AbortMultipartUpload", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
|
||||
|
||||
// Cases for testing AbortMultipartUpload access for different Object level access permissions.
|
||||
// read-only object policy is expected to not allow AbortMultipartUpload operation on an anonymous request (Test case 28).
|
||||
{"s3:AbortMultipartUpload", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
||||
// read-write object policy is expected to allow AbortMultipartUpload operation on an anonymous request (Test case 29).
|
||||
{"s3:AbortMultipartUpload", getReadWriteObjectStatement(bucketName, objectPrefix), true},
|
||||
// write-only object policy is expected to allow AbortMultipartUpload operation on an anonymous request (Test case 30).
|
||||
{"s3:AbortMultipartUpload", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
|
||||
|
||||
// s3:PutObject is the action necessary to be present in the bucket policy to allow
|
||||
// uploading of an object for an Anonymous/unsigned request.
|
||||
|
||||
// Cases for testing PutObject access for different Bucket level access permissions (Test cases 31-33).
|
||||
// Any of the Bucket level access permissions shouldn't allow for PutObject operation on an Anonymous/unsigned request.
|
||||
{"s3:PutObject", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
|
||||
{"s3:PutObject", getReadWriteBucketStatement(bucketName, objectPrefix), false},
|
||||
{"s3:PutObject", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
|
||||
|
||||
// Cases for testing PutObject access for different Object level access permissions.
|
||||
// read-only object policy is expected to not allow PutObject operation on an anonymous request (Test case 34).
|
||||
{"s3:PutObject", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
||||
// read-write object policy is expected to allow PutObject operation on an anonymous request (Test case 35).
|
||||
{"s3:PutObject", getReadWriteObjectStatement(bucketName, objectPrefix), true},
|
||||
// write-only object policy is expected to allow PutObject operation on an anonymous request (Test case 36).
|
||||
{"s3:PutObject", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
|
||||
|
||||
// s3:GetObject is the action necessary to be present in the bucket policy to allow
|
||||
// downloading of an object for an Anonymous/unsigned request.
|
||||
|
||||
// Cases for testing GetObject access for different Bucket level access permissions (Test cases 37-39).
|
||||
// Any of the Bucket level access permissions shouldn't allow for GetObject operation on an Anonymous/unsigned request.
|
||||
{"s3:GetObject", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
|
||||
{"s3:GetObject", getReadWriteBucketStatement(bucketName, objectPrefix), false},
|
||||
{"s3:GetObject", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
|
||||
|
||||
// Cases for testing GetObject access for different Object level access permissions.
|
||||
// read-only bucket policy is expected to allow downloading of an Object on an anonymous request (Test case 40).
|
||||
{"s3:GetObject", getReadOnlyObjectStatement(bucketName, objectPrefix), true},
|
||||
// read-write bucket policy is expected to allow downloading of an Object on an anonymous request (Test case 41).
|
||||
{"s3:GetObject", getReadWriteObjectStatement(bucketName, objectPrefix), true},
|
||||
// write-only bucket policy is expected to not allow downloading of an Object on an anonymous request (Test case 42).
|
||||
{"s3:GetObject", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
|
||||
|
||||
// s3:ListMultipartUploadParts is the action necessary to be present in the bucket policy to allow
|
||||
// Listing of uploaded parts for an Anonymous/unsigned request.
|
||||
|
||||
// Any of the Bucket level access permissions shouldn't allow for ListMultipartUploadParts operation on an Anonymous/unsigned request.
|
||||
// read-only bucket policy is expected to not allow ListMultipartUploadParts operation on an anonymous request (Test cases 43-45).
|
||||
{"s3:ListMultipartUploadParts", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
|
||||
{"s3:ListMultipartUploadParts", getReadWriteBucketStatement(bucketName, objectPrefix), false},
|
||||
{"s3:ListMultipartUploadParts", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
|
||||
|
||||
// read-only object policy is expected to not allow ListMultipartUploadParts operation on an anonymous request (Test case 46).
|
||||
{"s3:ListMultipartUploadParts", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
||||
// read-write object policy is expected to allow ListMultipartUploadParts operation on an anonymous request (Test case 47).
|
||||
{"s3:ListMultipartUploadParts", getReadWriteObjectStatement(bucketName, objectPrefix), true},
|
||||
// write-only object policy is expected to allow ListMultipartUploadParts operation on an anonymous request (Test case 48).
|
||||
{"s3:ListMultipartUploadParts", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
actualResult := bucketPolicyActionMatch(testCase.action, testCase.statement)
|
||||
if testCase.expectedResult != actualResult {
|
||||
t.Errorf("Test %d: Expected the result to be `%v`, but instead found it to be `%v`", i+1, testCase.expectedResult, actualResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for calling Put Bucket Policy HTTP handler tests for both XL multiple disks and single node setup.
|
||||
func TestPutBucketPolicyHandler(t *testing.T) {
|
||||
ExecObjectLayerTest(t, testPutBucketPolicyHandler)
|
||||
}
|
||||
|
||||
// testPutBucketPolicyHandler - Test for Bucket policy end point.
|
||||
// TODO: Add exhaustive cases with various combination of statement fields.
|
||||
func testPutBucketPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// get random bucket name.
|
||||
bucketName := getRandomBucketName()
|
||||
// Create bucket.
|
||||
err := obj.MakeBucket(bucketName)
|
||||
if err != nil {
|
||||
// failed to create newbucket, abort.
|
||||
t.Fatalf("%s : %s", instanceType, err)
|
||||
}
|
||||
// Register the API end points with XL/FS object layer.
|
||||
apiRouter := initTestAPIEndPoints(obj, []string{"PutBucketPolicy"})
|
||||
// initialize the server and obtain the credentials and root.
|
||||
// credentials are necessary to sign the HTTP request.
|
||||
rootPath, err := newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root folder after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
|
||||
credentials := serverConfig.GetCredential()
|
||||
|
||||
// template for constructing HTTP request body for PUT bucket policy.
|
||||
bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetBucketLocation","s3:ListBucket"],"Resource":["arn:aws:s3:::%s"]},{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::%s/this*"]}]}`
|
||||
|
||||
// test cases with sample input and expected output.
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
accessKey string
|
||||
secretKey string
|
||||
// expected Response.
|
||||
expectedRespStatus int
|
||||
}{
|
||||
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, http.StatusNoContent},
|
||||
}
|
||||
|
||||
// Iterating over the test cases, calling the function under test and asserting the response.
|
||||
for i, testCase := range testCases {
|
||||
// obtain the put bucket policy request body.
|
||||
bucketPolicyStr := fmt.Sprintf(bucketPolicyTemplate, testCase.bucketName, testCase.bucketName)
|
||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||
rec := httptest.NewRecorder()
|
||||
// construct HTTP request for PUT bucket policy endpoint.
|
||||
req, err := newTestSignedRequest("PUT", getPutPolicyURL("", testCase.bucketName),
|
||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testCase.accessKey, testCase.secretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||
}
|
||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler.
|
||||
// Call the ServeHTTP to execute the handler.
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
if rec.Code != testCase.expectedRespStatus {
|
||||
t.Errorf("Test %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for calling Get Bucket Policy HTTP handler tests for both XL multiple disks and single node setup.
|
||||
func TestGetBucketPolicyHandler(t *testing.T) {
|
||||
ExecObjectLayerTest(t, testGetBucketPolicyHandler)
|
||||
}
|
||||
|
||||
// testGetBucketPolicyHandler - Test for end point which fetches the access policy json of the given bucket.
|
||||
// TODO: Add exhaustive cases with various combination of statement fields.
|
||||
func testGetBucketPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// get random bucket name.
|
||||
bucketName := getRandomBucketName()
|
||||
// Create bucket.
|
||||
err := obj.MakeBucket(bucketName)
|
||||
if err != nil {
|
||||
// failed to create newbucket, abort.
|
||||
t.Fatalf("%s : %s", instanceType, err)
|
||||
}
|
||||
// Register the API end points with XL/FS object layer.
|
||||
// Registering only the PutBucketPolicy and GetBucketPolicy handlers.
|
||||
apiRouter := initTestAPIEndPoints(obj, []string{"PutBucketPolicy", "GetBucketPolicy"})
|
||||
// initialize the server and obtain the credentials and root.
|
||||
// credentials are necessary to sign the HTTP request.
|
||||
rootPath, err := newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root folder after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
|
||||
credentials := serverConfig.GetCredential()
|
||||
|
||||
// template for constructing HTTP request body for PUT bucket policy.
|
||||
bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetBucketLocation","s3:ListBucket"],"Resource":["arn:aws:s3:::%s"]},{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::%s/this*"]}]}`
|
||||
|
||||
// Writing bucket policy before running test on GetBucketPolicy.
|
||||
putTestPolicies := []struct {
|
||||
bucketName string
|
||||
accessKey string
|
||||
secretKey string
|
||||
// expected Response.
|
||||
expectedRespStatus int
|
||||
}{
|
||||
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, http.StatusNoContent},
|
||||
}
|
||||
|
||||
// Iterating over the cases and writing the bucket policy.
|
||||
// its required to write the policies first before running tests on GetBucketPolicy.
|
||||
for i, testPolicy := range putTestPolicies {
|
||||
// obtain the put bucket policy request body.
|
||||
bucketPolicyStr := fmt.Sprintf(bucketPolicyTemplate, testPolicy.bucketName, testPolicy.bucketName)
|
||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||
rec := httptest.NewRecorder()
|
||||
// construct HTTP request for PUT bucket policy endpoint.
|
||||
req, err := newTestSignedRequest("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||
}
|
||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler.
|
||||
// Call the ServeHTTP to execute the handler.
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
if rec.Code != testPolicy.expectedRespStatus {
|
||||
t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testPolicy.expectedRespStatus, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// test cases with inputs and expected result for GetBucketPolicyHandler.
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
accessKey string
|
||||
secretKey string
|
||||
// expected output.
|
||||
expectedBucketPolicy string
|
||||
expectedRespStatus int
|
||||
}{
|
||||
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, bucketPolicyTemplate, http.StatusOK},
|
||||
}
|
||||
// Iterating over the cases, fetching the policy and validating the response.
|
||||
for i, testCase := range testCases {
|
||||
// expected bucket policy json string.
|
||||
expectedBucketPolicyStr := fmt.Sprintf(testCase.expectedBucketPolicy, testCase.bucketName, testCase.bucketName)
|
||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||
rec := httptest.NewRecorder()
|
||||
// construct HTTP request for PUT bucket policy endpoint.
|
||||
req, err := newTestSignedRequest("GET", getGetPolicyURL("", testCase.bucketName),
|
||||
0, nil, testCase.accessKey, testCase.secretKey)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||
}
|
||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
||||
// Call the ServeHTTP to execute the handler, GetBucketPolicyHandler handles the request.
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
// Assert the response code with the expected status.
|
||||
if rec.Code != testCase.expectedRespStatus {
|
||||
t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, rec.Code)
|
||||
}
|
||||
// read the response body.
|
||||
bucketPolicyReadBuf, err := ioutil.ReadAll(rec.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: %s: Failed parsing response body: <ERROR> %v", i+1, instanceType, err)
|
||||
}
|
||||
// Verify whether the bucket policy fetched is same as the one inserted.
|
||||
if expectedBucketPolicyStr != string(bucketPolicyReadBuf) {
|
||||
t.Errorf("Test %d: %s: Bucket policy differs from expected value.", i+1, instanceType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for calling Delete Bucket Policy HTTP handler tests for both XL multiple disks and single node setup.
|
||||
func TestDeleteBucketPolicyHandler(t *testing.T) {
|
||||
ExecObjectLayerTest(t, testDeleteBucketPolicyHandler)
|
||||
}
|
||||
|
||||
// testDeleteBucketPolicyHandler - Test for Delete bucket policy end point.
|
||||
// TODO: Add exhaustive cases with various combination of statement fields.
|
||||
func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// get random bucket name.
|
||||
bucketName := getRandomBucketName()
|
||||
// Create bucket.
|
||||
err := obj.MakeBucket(bucketName)
|
||||
if err != nil {
|
||||
// failed to create newbucket, abort.
|
||||
t.Fatalf("%s : %s", instanceType, err)
|
||||
}
|
||||
|
||||
// Register the API end points with XL/FS object layer.
|
||||
// Registering PutBucketPolicy and DeleteBucketPolicy handlers.
|
||||
apiRouter := initTestAPIEndPoints(obj, []string{"PutBucketPolicy", "DeleteBucketPolicy"})
|
||||
|
||||
// initialize the server and obtain the credentials and root.
|
||||
// credentials are necessary to sign the HTTP request.
|
||||
rootPath, err := newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root folder after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
|
||||
credentials := serverConfig.GetCredential()
|
||||
|
||||
// template for constructing HTTP request body for PUT bucket policy.
|
||||
bucketPolicyTemplate := `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:GetBucketLocation",
|
||||
"s3:ListBucket"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
"Resource": [
|
||||
"arn:aws:s3:::%s"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:GetObject"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
"Resource": [
|
||||
"arn:aws:s3:::%s/this*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// Writing bucket policy before running test on DeleteBucketPolicy.
|
||||
putTestPolicies := []struct {
|
||||
bucketName string
|
||||
accessKey string
|
||||
secretKey string
|
||||
// expected Response.
|
||||
expectedRespStatus int
|
||||
}{
|
||||
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, http.StatusNoContent},
|
||||
}
|
||||
|
||||
// Iterating over the cases and writing the bucket policy.
|
||||
// its required to write the policies first before running tests on GetBucketPolicy.
|
||||
for i, testPolicy := range putTestPolicies {
|
||||
// obtain the put bucket policy request body.
|
||||
bucketPolicyStr := fmt.Sprintf(bucketPolicyTemplate, testPolicy.bucketName, testPolicy.bucketName)
|
||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||
rec := httptest.NewRecorder()
|
||||
// construct HTTP request for PUT bucket policy endpoint.
|
||||
req, err := newTestSignedRequest("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||
}
|
||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
||||
// Call the ServeHTTP to execute the handler.
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
if rec.Code != testPolicy.expectedRespStatus {
|
||||
t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testPolicy.expectedRespStatus, rec.Code)
|
||||
}
|
||||
}
|
||||
// testcases with input and expected output for DeleteBucketPolicyHandler.
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
accessKey string
|
||||
secretKey string
|
||||
// expected Response.
|
||||
expectedRespStatus int
|
||||
}{
|
||||
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, http.StatusNoContent},
|
||||
}
|
||||
// Iterating over the cases and deleting the bucket policy and then asserting response.
|
||||
for i, testCase := range testCases {
|
||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||
rec := httptest.NewRecorder()
|
||||
// construct HTTP request for Delete bucket policy endpoint.
|
||||
req, err := newTestSignedRequest("DELETE", getDeletePolicyURL("", testCase.bucketName),
|
||||
0, nil, testCase.accessKey, testCase.secretKey)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||
}
|
||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
||||
// Call the ServeHTTP to execute the handler, DeleteBucketPolicyHandler handles the request.
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
// Assert the response code with the expected status.
|
||||
if rec.Code != testCase.expectedRespStatus {
|
||||
t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, rec.Code)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const policyJSON = "policy.json"
|
||||
|
||||
func getBucketFromPolicyPath(oldPolicyPath string) string {
|
||||
bucketPrefix, _ := filepath.Split(oldPolicyPath)
|
||||
_, bucketName := filepath.Split(strings.TrimSuffix(bucketPrefix, slashSeparator))
|
||||
return bucketName
|
||||
}
|
||||
|
||||
func cleanupOldBucketPolicyConfigs() error {
|
||||
// Get old bucket policy config directory.
|
||||
oldBucketsConfigDir, err := getOldBucketsConfigPath()
|
||||
fatalIf(err, "Unable to fetch buckets config path to migrate bucket policy")
|
||||
|
||||
// Recursively remove configDir/buckets/ - old bucket policy config location.
|
||||
// N B This is called only if all bucket policies were successfully migrated.
|
||||
return os.RemoveAll(oldBucketsConfigDir)
|
||||
}
|
||||
|
||||
func migrateBucketPolicyConfig(objAPI ObjectLayer) error {
|
||||
// Get old bucket policy config directory.
|
||||
oldBucketsConfigDir, err := getOldBucketsConfigPath()
|
||||
fatalIf(err, "Unable to fetch buckets config path to migrate bucket policy")
|
||||
|
||||
// Check if config directory holding bucket policy exists before
|
||||
// migration.
|
||||
_, err = os.Stat(oldBucketsConfigDir)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// WalkFunc that migrates access-policy.json to
|
||||
// .minio.sys/buckets/bucketName/policy.json on all disks.
|
||||
migrateBucketPolicy := func(policyPath string, fileInfo os.FileInfo, err error) error {
|
||||
// policyFile - e.g /configDir/sample-bucket/access-policy.json
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Skip entries that aren't bucket policy files.
|
||||
if fileInfo.Name() != "access-policy.json" {
|
||||
return nil
|
||||
}
|
||||
// Get bucketName from old policy file path.
|
||||
bucketName := getBucketFromPolicyPath(policyPath)
|
||||
// Read bucket policy config from old location.
|
||||
policyBytes, err := ioutil.ReadFile(policyPath)
|
||||
fatalIf(err, "Unable to read bucket policy to migrate bucket policy", policyPath)
|
||||
newPolicyPath := retainSlash(bucketConfigPrefix) + retainSlash(bucketName) + policyJSON
|
||||
// Erasure code the policy config to all the disks.
|
||||
_, err = objAPI.PutObject(minioMetaBucket, newPolicyPath, int64(len(policyBytes)), bytes.NewReader(policyBytes), nil)
|
||||
fatalIf(err, "Unable to write bucket policy during migration.", newPolicyPath)
|
||||
return nil
|
||||
}
|
||||
return filepath.Walk(oldBucketsConfigDir, migrateBucketPolicy)
|
||||
}
|
||||
+91
-55
@@ -22,7 +22,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"io"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
@@ -34,6 +35,8 @@ const (
|
||||
|
||||
// supportedActionMap - lists all the actions supported by minio.
|
||||
var supportedActionMap = map[string]struct{}{
|
||||
"*": {},
|
||||
"s3:*": {},
|
||||
"s3:GetObject": {},
|
||||
"s3:ListBucket": {},
|
||||
"s3:PutObject": {},
|
||||
@@ -44,6 +47,19 @@ var supportedActionMap = map[string]struct{}{
|
||||
"s3:ListMultipartUploadParts": {},
|
||||
}
|
||||
|
||||
// supported Conditions type.
|
||||
var supportedConditionsType = map[string]struct{}{
|
||||
"StringEquals": {},
|
||||
"StringNotEquals": {},
|
||||
}
|
||||
|
||||
// Validate s3:prefix, s3:max-keys are present if not
|
||||
// supported keys for the conditions.
|
||||
var supportedConditionsKey = map[string]struct{}{
|
||||
"s3:prefix": {},
|
||||
"s3:max-keys": {},
|
||||
}
|
||||
|
||||
// User - canonical users list.
|
||||
type policyUser struct {
|
||||
AWS []string
|
||||
@@ -56,15 +72,25 @@ type policyStatement struct {
|
||||
Principal policyUser `json:"Principal"`
|
||||
Actions []string `json:"Action"`
|
||||
Resources []string `json:"Resource"`
|
||||
Conditions map[string]map[string]string `json:"Condition"`
|
||||
Conditions map[string]map[string]string `json:"Condition,omitempty"`
|
||||
}
|
||||
|
||||
// BucketPolicy - minio policy collection
|
||||
type BucketPolicy struct {
|
||||
// bucketPolicy - collection of various bucket policy statements.
|
||||
type bucketPolicy struct {
|
||||
Version string // date in 0000-00-00 format
|
||||
Statements []policyStatement `json:"Statement"`
|
||||
}
|
||||
|
||||
// Stringer implementation for the bucket policies.
|
||||
func (b bucketPolicy) String() string {
|
||||
bbytes, err := json.Marshal(&b)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to unmarshal bucket policy into JSON %#v", b)
|
||||
return ""
|
||||
}
|
||||
return string(bbytes)
|
||||
}
|
||||
|
||||
// supportedEffectMap - supported effects.
|
||||
var supportedEffectMap = map[string]struct{}{
|
||||
"Allow": {},
|
||||
@@ -130,52 +156,50 @@ func isValidPrincipals(principals []string) (err error) {
|
||||
err = errors.New("Principal cannot be empty.")
|
||||
return err
|
||||
}
|
||||
var ok bool
|
||||
for _, principal := range principals {
|
||||
// Minio does not support or implement IAM, "*" is the only valid value.
|
||||
if principal == "*" {
|
||||
ok = true
|
||||
continue
|
||||
// Amazon s3 doc on principals: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Principal
|
||||
if principal != "*" {
|
||||
err = fmt.Errorf("Unsupported principal style found: ‘%s’, please validate your policy document.", principal)
|
||||
return err
|
||||
}
|
||||
ok = false
|
||||
}
|
||||
if !ok {
|
||||
err = errors.New("Unsupported principal style found: ‘" + strings.Join(principals, " ") + "’, please validate your policy document.")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidConditions - are valid conditions.
|
||||
func isValidConditions(conditions map[string]map[string]string) (err error) {
|
||||
// Returns true if string 'a' is found in the list.
|
||||
findString := func(a string, list []string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
conditionKeyVal := make(map[string][]string)
|
||||
// Verify conditions should be valid.
|
||||
if len(conditions) > 0 {
|
||||
// Validate if stringEquals, stringNotEquals are present
|
||||
// if not throw an error.
|
||||
_, stringEqualsOK := conditions["StringEquals"]
|
||||
_, stringNotEqualsOK := conditions["StringNotEquals"]
|
||||
if !stringEqualsOK && !stringNotEqualsOK {
|
||||
err = fmt.Errorf("Unsupported condition type found: ‘%s’, please validate your policy document.", conditions)
|
||||
// Validate if stringEquals, stringNotEquals are present
|
||||
// if not throw an error.
|
||||
for conditionType := range conditions {
|
||||
_, validType := supportedConditionsType[conditionType]
|
||||
if !validType {
|
||||
err = fmt.Errorf("Unsupported condition type '%s', please validate your policy document.", conditionType)
|
||||
return err
|
||||
}
|
||||
// Validate s3:prefix, s3:max-keys are present if not
|
||||
// throw an error.
|
||||
if len(conditions["StringEquals"]) > 0 {
|
||||
_, s3PrefixOK := conditions["StringEquals"]["s3:prefix"]
|
||||
_, s3MaxKeysOK := conditions["StringEquals"]["s3:max-keys"]
|
||||
if !s3PrefixOK && !s3MaxKeysOK {
|
||||
err = fmt.Errorf("Unsupported condition keys found: ‘%s’, please validate your policy document.",
|
||||
conditions["StringEquals"])
|
||||
for key := range conditions[conditionType] {
|
||||
_, validKey := supportedConditionsKey[key]
|
||||
if !validKey {
|
||||
err = fmt.Errorf("Unsupported condition key '%s', please validate your policy document.", conditionType)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(conditions["StringNotEquals"]) > 0 {
|
||||
_, s3PrefixOK := conditions["StringNotEquals"]["s3:prefix"]
|
||||
_, s3MaxKeysOK := conditions["StringNotEquals"]["s3:max-keys"]
|
||||
if !s3PrefixOK && !s3MaxKeysOK {
|
||||
err = fmt.Errorf("Unsupported condition keys found: ‘%s’, please validate your policy document.",
|
||||
conditions["StringNotEquals"])
|
||||
conditionArray, ok := conditionKeyVal[key]
|
||||
if ok && findString(conditions[conditionType][key], conditionArray) {
|
||||
err = fmt.Errorf("Ambigious condition values for key '%s', please validate your policy document.", key)
|
||||
return err
|
||||
}
|
||||
conditionKeyVal[key] = append(conditionKeyVal[key], conditions[conditionType][key])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -189,8 +213,18 @@ var invalidPrefixActions = map[string]struct{}{
|
||||
// Add actions which do not honor prefixes.
|
||||
}
|
||||
|
||||
// checkBucketPolicy validates unmarshalled bucket policy structure.
|
||||
func checkBucketPolicy(bucket string, bucketPolicy BucketPolicy) APIErrorCode {
|
||||
// resourcePrefix - provides the prefix removing any wildcards.
|
||||
func resourcePrefix(resource string) string {
|
||||
if strings.HasSuffix(resource, "*") {
|
||||
resource = strings.TrimSuffix(resource, "*")
|
||||
}
|
||||
return path.Clean(resource)
|
||||
}
|
||||
|
||||
// checkBucketPolicyResources validates Resources in unmarshalled bucket policy structure.
|
||||
// First valation of Resources done for given set of Actions.
|
||||
// Later its validated for recursive Resources.
|
||||
func checkBucketPolicyResources(bucket string, bucketPolicy *bucketPolicy) APIErrorCode {
|
||||
// Validate statements for special actions and collect resources
|
||||
// for others to validate nesting.
|
||||
var resourceMap = make(map[string]struct{})
|
||||
@@ -205,9 +239,9 @@ func checkBucketPolicy(bucket string, bucketPolicy BucketPolicy) APIErrorCode {
|
||||
return ErrMalformedPolicy
|
||||
}
|
||||
} else {
|
||||
// For all other actions validate if prefix begins
|
||||
// with bucket, if not reject them.
|
||||
if !strings.HasPrefix(resourcePrefix, bucket) {
|
||||
// For all other actions validate if resourcePrefix begins
|
||||
// with bucket name, if not reject them.
|
||||
if strings.Split(resourcePrefix, "/")[0] != bucket {
|
||||
return ErrMalformedPolicy
|
||||
}
|
||||
// All valid resources collect them separately to verify nesting.
|
||||
@@ -219,7 +253,7 @@ func checkBucketPolicy(bucket string, bucketPolicy BucketPolicy) APIErrorCode {
|
||||
|
||||
var resources []string
|
||||
for resource := range resourceMap {
|
||||
resources = append(resources, resource)
|
||||
resources = append(resources, resourcePrefix(resource))
|
||||
}
|
||||
|
||||
// Sort strings as shorter first.
|
||||
@@ -228,13 +262,13 @@ func checkBucketPolicy(bucket string, bucketPolicy BucketPolicy) APIErrorCode {
|
||||
for len(resources) > 1 {
|
||||
var resource string
|
||||
resource, resources = resources[0], resources[1:]
|
||||
resourceRegex := regexp.MustCompile(resource)
|
||||
// Loop through all resources, if one of them matches with
|
||||
// previous shorter one, it means we have detected
|
||||
// nesting. Reject such rules.
|
||||
for _, otherResource := range resources {
|
||||
if resourceRegex.MatchString(otherResource) {
|
||||
return ErrMalformedPolicy
|
||||
// Common prefix reject such rules.
|
||||
if strings.HasPrefix(otherResource, resource) {
|
||||
return ErrPolicyNesting
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,44 +279,46 @@ func checkBucketPolicy(bucket string, bucketPolicy BucketPolicy) APIErrorCode {
|
||||
|
||||
// parseBucketPolicy - parses and validates if bucket policy is of
|
||||
// proper JSON and follows allowed restrictions with policy standards.
|
||||
func parseBucketPolicy(bucketPolicyBuf []byte) (policy BucketPolicy, err error) {
|
||||
if err = json.Unmarshal(bucketPolicyBuf, &policy); err != nil {
|
||||
return BucketPolicy{}, err
|
||||
func parseBucketPolicy(bucketPolicyReader io.Reader, policy *bucketPolicy) (err error) {
|
||||
// Parse bucket policy reader.
|
||||
decoder := json.NewDecoder(bucketPolicyReader)
|
||||
if err = decoder.Decode(&policy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Policy version cannot be empty.
|
||||
if len(policy.Version) == 0 {
|
||||
err = errors.New("Policy version cannot be empty.")
|
||||
return BucketPolicy{}, err
|
||||
return err
|
||||
}
|
||||
|
||||
// Policy statements cannot be empty.
|
||||
if len(policy.Statements) == 0 {
|
||||
err = errors.New("Policy statement cannot be empty.")
|
||||
return BucketPolicy{}, err
|
||||
return err
|
||||
}
|
||||
|
||||
// Loop through all policy statements and validate entries.
|
||||
for _, statement := range policy.Statements {
|
||||
// Statement effect should be valid.
|
||||
if err := isValidEffect(statement.Effect); err != nil {
|
||||
return BucketPolicy{}, err
|
||||
return err
|
||||
}
|
||||
// Statement principal should be supported format.
|
||||
if err := isValidPrincipals(statement.Principal.AWS); err != nil {
|
||||
return BucketPolicy{}, err
|
||||
return err
|
||||
}
|
||||
// Statement actions should be valid.
|
||||
if err := isValidActions(statement.Actions); err != nil {
|
||||
return BucketPolicy{}, err
|
||||
return err
|
||||
}
|
||||
// Statement resources should be valid.
|
||||
if err := isValidResources(statement.Resources); err != nil {
|
||||
return BucketPolicy{}, err
|
||||
return err
|
||||
}
|
||||
// Statement conditions should be valid.
|
||||
if err := isValidConditions(statement.Conditions); err != nil {
|
||||
return BucketPolicy{}, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,5 +339,5 @@ func parseBucketPolicy(bucketPolicyBuf []byte) (policy BucketPolicy, err error)
|
||||
policy.Statements = append(denyStatements, allowStatements...)
|
||||
|
||||
// Return successfully parsed policy structure.
|
||||
return policy, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,695 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
readWriteBucketActions = []string{
|
||||
"s3:GetBucketLocation",
|
||||
"s3:ListBucket",
|
||||
"s3:ListBucketMultipartUploads",
|
||||
// Add more bucket level read-write actions here.
|
||||
}
|
||||
readWriteObjectActions = []string{
|
||||
"s3:AbortMultipartUpload",
|
||||
"s3:DeleteObject",
|
||||
"s3:GetObject",
|
||||
"s3:ListMultipartUploadParts",
|
||||
"s3:PutObject",
|
||||
// Add more object level read-write actions here.
|
||||
}
|
||||
)
|
||||
|
||||
// Write only actions.
|
||||
var (
|
||||
writeOnlyBucketActions = []string{
|
||||
"s3:GetBucketLocation",
|
||||
"s3:ListBucketMultipartUploads",
|
||||
// Add more bucket level write actions here.
|
||||
}
|
||||
writeOnlyObjectActions = []string{
|
||||
"s3:AbortMultipartUpload",
|
||||
"s3:DeleteObject",
|
||||
"s3:ListMultipartUploadParts",
|
||||
"s3:PutObject",
|
||||
// Add more object level write actions here.
|
||||
}
|
||||
)
|
||||
|
||||
// Read only actions.
|
||||
var (
|
||||
readOnlyBucketActions = []string{
|
||||
"s3:GetBucketLocation",
|
||||
"s3:ListBucket",
|
||||
// Add more bucket level read actions here.
|
||||
}
|
||||
readOnlyObjectActions = []string{
|
||||
"s3:GetObject",
|
||||
// Add more object level read actions here.
|
||||
}
|
||||
)
|
||||
|
||||
// Obtain bucket statement for read-write bucketPolicy.
|
||||
func getReadWriteObjectStatement(bucketName, objectPrefix string) policyStatement {
|
||||
objectResourceStatement := policyStatement{}
|
||||
objectResourceStatement.Effect = "Allow"
|
||||
objectResourceStatement.Principal.AWS = []string{"*"}
|
||||
objectResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")}
|
||||
objectResourceStatement.Actions = readWriteObjectActions
|
||||
return objectResourceStatement
|
||||
}
|
||||
|
||||
// Obtain object statement for read-write bucketPolicy.
|
||||
func getReadWriteBucketStatement(bucketName, objectPrefix string) policyStatement {
|
||||
bucketResourceStatement := policyStatement{}
|
||||
bucketResourceStatement.Effect = "Allow"
|
||||
bucketResourceStatement.Principal.AWS = []string{"*"}
|
||||
bucketResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)}
|
||||
bucketResourceStatement.Actions = readWriteBucketActions
|
||||
return bucketResourceStatement
|
||||
}
|
||||
|
||||
// Obtain statements for read-write bucketPolicy.
|
||||
func getReadWriteStatement(bucketName, objectPrefix string) []policyStatement {
|
||||
statements := []policyStatement{}
|
||||
// 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{}
|
||||
bucketResourceStatement.Effect = "Allow"
|
||||
bucketResourceStatement.Principal.AWS = []string{"*"}
|
||||
bucketResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)}
|
||||
bucketResourceStatement.Actions = readOnlyBucketActions
|
||||
return bucketResourceStatement
|
||||
}
|
||||
|
||||
// Obtain object statement for read only bucketPolicy.
|
||||
func getReadOnlyObjectStatement(bucketName, objectPrefix string) policyStatement {
|
||||
objectResourceStatement := policyStatement{}
|
||||
objectResourceStatement.Effect = "Allow"
|
||||
objectResourceStatement.Principal.AWS = []string{"*"}
|
||||
objectResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")}
|
||||
objectResourceStatement.Actions = readOnlyObjectActions
|
||||
return objectResourceStatement
|
||||
}
|
||||
|
||||
// Obtain statements for read only bucketPolicy.
|
||||
func getReadOnlyStatement(bucketName, objectPrefix string) []policyStatement {
|
||||
statements := []policyStatement{}
|
||||
// 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 {
|
||||
|
||||
bucketResourceStatement := policyStatement{}
|
||||
bucketResourceStatement.Effect = "Allow"
|
||||
bucketResourceStatement.Principal.AWS = []string{"*"}
|
||||
bucketResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)}
|
||||
bucketResourceStatement.Actions = writeOnlyBucketActions
|
||||
return bucketResourceStatement
|
||||
}
|
||||
|
||||
// Obtain object statements for write only bucketPolicy.
|
||||
func getWriteOnlyObjectStatement(bucketName, objectPrefix string) policyStatement {
|
||||
objectResourceStatement := policyStatement{}
|
||||
objectResourceStatement.Effect = "Allow"
|
||||
objectResourceStatement.Principal.AWS = []string{"*"}
|
||||
objectResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")}
|
||||
objectResourceStatement.Actions = writeOnlyObjectActions
|
||||
return objectResourceStatement
|
||||
}
|
||||
|
||||
// Obtain statements for write only bucketPolicy.
|
||||
func getWriteOnlyStatement(bucketName, objectPrefix string) []policyStatement {
|
||||
statements := []policyStatement{}
|
||||
// Write only policy.
|
||||
// Save the write only policy.
|
||||
statements = append(statements, getWriteOnlyBucketStatement(bucketName, objectPrefix), getWriteOnlyBucketStatement(bucketName, objectPrefix))
|
||||
return statements
|
||||
}
|
||||
|
||||
// Tests validate Action validator.
|
||||
func TestIsValidActions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
// input.
|
||||
actions []string
|
||||
// expected output.
|
||||
err error
|
||||
// flag indicating whether the test should pass.
|
||||
shouldPass bool
|
||||
}{
|
||||
// Inputs with unsupported Action.
|
||||
// Test case - 1.
|
||||
// "s3:ListObject" is an invalid Action.
|
||||
{[]string{"s3:GetObject", "s3:ListObject", "s3:RemoveObject"}, errors.New("Unsupported action found: ‘s3:ListObject’, please validate your policy document."), false},
|
||||
// Test case - 2.
|
||||
// Empty Actions.
|
||||
{[]string{}, errors.New("Action list cannot be empty."), false},
|
||||
// Test case - 3.
|
||||
// "s3:DeleteEverything"" is an invalid Action.
|
||||
{[]string{"s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:DeleteEverything"},
|
||||
errors.New("Unsupported action found: ‘s3:DeleteEverything’, please validate your policy document."), false},
|
||||
|
||||
// Inputs with valid Action.
|
||||
// Test Case - 4.
|
||||
{[]string{"s3:*", "*", "s3:GetObject", "s3:ListBucket",
|
||||
"s3:PutObject", "s3:GetBucketLocation", "s3:DeleteObject", "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts"}, nil, true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
err := isValidActions(testCase.actions)
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
|
||||
}
|
||||
if err == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error())
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if err != nil && !testCase.shouldPass {
|
||||
if err.Error() != testCase.err.Error() {
|
||||
t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.err.Error(), err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Tests validate Effect validator.
|
||||
func TestIsValidEffect(t *testing.T) {
|
||||
testCases := []struct {
|
||||
// input.
|
||||
effect string
|
||||
// expected output.
|
||||
err error
|
||||
// flag indicating whether the test should pass.
|
||||
shouldPass bool
|
||||
}{
|
||||
// Inputs with unsupported Effect.
|
||||
// Test case - 1.
|
||||
{"DontAllow", errors.New("Unsupported Effect found: ‘DontAllow’, please validate your policy document."), false},
|
||||
// Test case - 2.
|
||||
{"NeverAllow", errors.New("Unsupported Effect found: ‘NeverAllow’, please validate your policy document."), false},
|
||||
// Test case - 3.
|
||||
{"AllowAlways", errors.New("Unsupported Effect found: ‘AllowAlways’, please validate your policy document."), false},
|
||||
|
||||
// Inputs with valid Effect.
|
||||
// Test Case - 4.
|
||||
{"Allow", nil, true},
|
||||
// Test Case - 5.
|
||||
{"Deny", nil, true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
err := isValidEffect(testCase.effect)
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
|
||||
}
|
||||
if err == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error())
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if err != nil && !testCase.shouldPass {
|
||||
if err.Error() != testCase.err.Error() {
|
||||
t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.err.Error(), err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests validate Resources validator.
|
||||
func TestIsValidResources(t *testing.T) {
|
||||
testCases := []struct {
|
||||
// input.
|
||||
resources []string
|
||||
// expected output.
|
||||
err error
|
||||
// flag indicating whether the test should pass.
|
||||
shouldPass bool
|
||||
}{
|
||||
// Inputs with unsupported Action.
|
||||
// Test case - 1.
|
||||
// Empty Resources.
|
||||
{[]string{}, errors.New("Resource list cannot be empty."), false},
|
||||
// Test case - 2.
|
||||
// A valid resource should have prefix "arn:aws:s3:::".
|
||||
{[]string{"my-resource"}, errors.New("Unsupported resource style found: ‘my-resource’, please validate your policy document."), false},
|
||||
// Test case - 3.
|
||||
// A Valid resource should have bucket name followed by "arn:aws:s3:::".
|
||||
{[]string{"arn:aws:s3:::"}, errors.New("Invalid resource style found: ‘arn:aws:s3:::’, please validate your policy document."), false},
|
||||
// Test Case - 4.
|
||||
// Valid resource shouldn't have slash('/') followed by "arn:aws:s3:::".
|
||||
{[]string{"arn:aws:s3:::/"}, errors.New("Invalid resource style found: ‘arn:aws:s3:::/’, please validate your policy document."), false},
|
||||
|
||||
// Test cases with valid Resources.
|
||||
{[]string{"arn:aws:s3:::my-bucket"}, nil, true},
|
||||
{[]string{"arn:aws:s3:::my-bucket/Asia/*"}, nil, true},
|
||||
{[]string{"arn:aws:s3:::my-bucket/Asia/India/*"}, nil, true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
err := isValidResources(testCase.resources)
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
|
||||
}
|
||||
if err == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error())
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if err != nil && !testCase.shouldPass {
|
||||
if err.Error() != testCase.err.Error() {
|
||||
t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.err.Error(), err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests validate principals validator.
|
||||
func TestIsValidPrincipals(t *testing.T) {
|
||||
testCases := []struct {
|
||||
// input.
|
||||
principals []string
|
||||
// expected output.
|
||||
err error
|
||||
// flag indicating whether the test should pass.
|
||||
shouldPass bool
|
||||
}{
|
||||
// Inputs with unsupported Principals.
|
||||
// Test case - 1.
|
||||
// Empty Principals list.
|
||||
{[]string{}, errors.New("Principal cannot be empty."), false},
|
||||
// Test case - 2.
|
||||
// "*" is the only valid principal.
|
||||
{[]string{"my-principal"}, errors.New("Unsupported principal style found: ‘my-principal’, please validate your policy document."), false},
|
||||
// Test case - 3.
|
||||
{[]string{"*", "111122233"}, errors.New("Unsupported principal style found: ‘111122233’, please validate your policy document."), false},
|
||||
|
||||
// Test case - 4.
|
||||
// Test case with valid principal value.
|
||||
{[]string{"*"}, nil, true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
err := isValidPrincipals(testCase.principals)
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
|
||||
}
|
||||
if err == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error())
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if err != nil && !testCase.shouldPass {
|
||||
if err.Error() != testCase.err.Error() {
|
||||
t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.err.Error(), err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests validate policyStatement condition validator.
|
||||
func TestIsValidConditions(t *testing.T) {
|
||||
// returns empty conditions map.
|
||||
setEmptyConditions := func() map[string]map[string]string {
|
||||
return make(map[string]map[string]string)
|
||||
}
|
||||
|
||||
// returns map with the "StringEquals" set to empty map.
|
||||
setEmptyStringEquals := func() map[string]map[string]string {
|
||||
emptyMap := make(map[string]string)
|
||||
conditions := make(map[string]map[string]string)
|
||||
conditions["StringEquals"] = emptyMap
|
||||
return conditions
|
||||
|
||||
}
|
||||
|
||||
// returns map with the "StringNotEquals" set to empty map.
|
||||
setEmptyStringNotEquals := func() map[string]map[string]string {
|
||||
emptyMap := make(map[string]string)
|
||||
conditions := make(map[string]map[string]string)
|
||||
conditions["StringNotEquals"] = emptyMap
|
||||
return conditions
|
||||
|
||||
}
|
||||
// Generate conditions.
|
||||
generateConditions := func(key1, key2, value string) map[string]map[string]string {
|
||||
innerMap := make(map[string]string)
|
||||
innerMap[key2] = value
|
||||
conditions := make(map[string]map[string]string)
|
||||
conditions[key1] = innerMap
|
||||
return conditions
|
||||
}
|
||||
|
||||
// generate ambigious conditions.
|
||||
generateAmbigiousConditions := func() map[string]map[string]string {
|
||||
innerMap := make(map[string]string)
|
||||
innerMap["s3:prefix"] = "Asia/"
|
||||
conditions := make(map[string]map[string]string)
|
||||
conditions["StringEquals"] = innerMap
|
||||
conditions["StringNotEquals"] = innerMap
|
||||
return conditions
|
||||
}
|
||||
|
||||
// generate valid and non valid type in the condition map.
|
||||
generateValidInvalidConditions := func() map[string]map[string]string {
|
||||
innerMap := make(map[string]string)
|
||||
innerMap["s3:prefix"] = "Asia/"
|
||||
conditions := make(map[string]map[string]string)
|
||||
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]string {
|
||||
innerMapValid := make(map[string]string)
|
||||
innerMapValid["s3:prefix"] = "Asia/"
|
||||
innerMapInValid := make(map[string]string)
|
||||
innerMapInValid["s3:invalid"] = "Asia/"
|
||||
conditions := make(map[string]map[string]string)
|
||||
conditions["StringEquals"] = innerMapValid
|
||||
conditions["StringEquals"] = innerMapInValid
|
||||
return conditions
|
||||
}
|
||||
|
||||
// List of Conditions used for test cases.
|
||||
testConditions := []map[string]map[string]string{
|
||||
generateConditions("StringValues", "s3:max-keys", "100"),
|
||||
generateConditions("StringEquals", "s3:Object", "100"),
|
||||
generateAmbigiousConditions(),
|
||||
generateValidInvalidConditions(),
|
||||
generateValidInvalidConditionKeys(),
|
||||
setEmptyConditions(),
|
||||
setEmptyStringEquals(),
|
||||
setEmptyStringNotEquals(),
|
||||
generateConditions("StringEquals", "s3:prefix", "Asia/"),
|
||||
generateConditions("StringEquals", "s3:max-keys", "100"),
|
||||
generateConditions("StringNotEquals", "s3:prefix", "Asia/"),
|
||||
generateConditions("StringNotEquals", "s3:max-keys", "100"),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
inputCondition map[string]map[string]string
|
||||
// expected result.
|
||||
expectedErr error
|
||||
// flag indicating whether test should pass.
|
||||
shouldPass bool
|
||||
}{
|
||||
// Malformed conditions.
|
||||
// Test case - 1.
|
||||
// "StringValues" is an invalid type.
|
||||
{testConditions[0], fmt.Errorf("Unsupported condition type 'StringValues', " +
|
||||
"please validate your policy document."), false},
|
||||
// Test case - 2.
|
||||
// "s3:Object" is an invalid key.
|
||||
{testConditions[1], fmt.Errorf("Unsupported condition key " +
|
||||
"'StringEquals', please validate your policy document."), false},
|
||||
// Test case - 3.
|
||||
// Test case with Ambigious conditions set.
|
||||
{testConditions[2], fmt.Errorf("Ambigious condition values for key 's3:prefix', " +
|
||||
"please validate your policy document."), false},
|
||||
// Test case - 4.
|
||||
// Test case with valid and invalid condition types.
|
||||
{testConditions[3], fmt.Errorf("Unsupported condition type 'InvalidType', " +
|
||||
"please validate your policy document."), false},
|
||||
// Test case - 5.
|
||||
// Test case with valid and invalid condition keys.
|
||||
{testConditions[4], fmt.Errorf("Unsupported condition key 'StringEquals', " +
|
||||
"please validate your policy document."), false},
|
||||
// Test cases with valid conditions.
|
||||
// Test case - 6.
|
||||
{testConditions[5], nil, true},
|
||||
// Test case - 7.
|
||||
{testConditions[6], nil, true},
|
||||
// Test case - 8.
|
||||
{testConditions[7], nil, true},
|
||||
// Test case - 9.
|
||||
{testConditions[8], nil, true},
|
||||
// Test case - 10.
|
||||
{testConditions[9], nil, true},
|
||||
// Test case - 11.
|
||||
{testConditions[10], nil, true},
|
||||
// Test case 10.
|
||||
{testConditions[11], nil, true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
actualErr := isValidConditions(testCase.inputCondition)
|
||||
if actualErr != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, actualErr.Error())
|
||||
}
|
||||
if actualErr == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.expectedErr.Error())
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if actualErr != nil && !testCase.shouldPass {
|
||||
if actualErr.Error() != testCase.expectedErr.Error() {
|
||||
t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.expectedErr.Error(), actualErr.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
statements[0].Actions = []string{"s3:DeleteObject", "s3:PutObject"}
|
||||
return statements
|
||||
}
|
||||
// contracting policy statement with recursive resources.
|
||||
// should result in ErrMalformedPolicy
|
||||
setRecurseResource := func(statements []policyStatement) []policyStatement {
|
||||
statements[0].Resources = []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 {
|
||||
statements[0].Resources = []string{"arn:aws:s3:::minio-bucket/op*", "arn:aws:s3:::minio-bucket/oo*"}
|
||||
return statements
|
||||
}
|
||||
|
||||
// List of bucketPolicy used for tests.
|
||||
bucketAccessPolicies := []bucketPolicy{
|
||||
// bucketPolicy - 1.
|
||||
// Contains valid read only policy statement.
|
||||
{Version: "1.0", Statements: getReadOnlyStatement("minio-bucket", "")},
|
||||
// bucketPolicy - 2.
|
||||
// Contains valid read-write only policy statement.
|
||||
{Version: "1.0", Statements: getReadWriteStatement("minio-bucket", "Asia/")},
|
||||
// bucketPolicy - 3.
|
||||
// Contains valid write only policy statement.
|
||||
{Version: "1.0", Statements: getWriteOnlyStatement("minio-bucket", "Asia/India/")},
|
||||
// bucketPolicy - 4.
|
||||
// Contains invalidPrefixActions.
|
||||
// Since resourcePrefix is not to the bucket-name, it return ErrMalformedPolicy.
|
||||
{Version: "1.0", Statements: getReadOnlyStatement("minio-bucket-fail", "Asia/India/")},
|
||||
// bucketPolicy - 5.
|
||||
// constructing policy statement without invalidPrefixActions (check bucket-policy-parser.go).
|
||||
// but bucket part of the resource is not equal to the bucket name.
|
||||
// this results in return of ErrMalformedPolicy.
|
||||
{Version: "1.0", Statements: setValidPrefixActions(getWriteOnlyStatement("minio-bucket-fail", "Asia/India/"))},
|
||||
// bucketPolicy - 6.
|
||||
// contracting policy statement with recursive resources.
|
||||
// should result in ErrMalformedPolicy
|
||||
{Version: "1.0", Statements: setRecurseResource(setValidPrefixActions(getWriteOnlyStatement("minio-bucket", "")))},
|
||||
// BucketPolciy - 7.
|
||||
// constructing policy statement with non recursive but
|
||||
// lexically close resources.
|
||||
// should result in ErrNone.
|
||||
{Version: "1.0", Statements: setResourceLexical(setValidPrefixActions(getWriteOnlyStatement("minio-bucket", "oo")))},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
inputPolicy bucketPolicy
|
||||
// expected results.
|
||||
apiErrCode APIErrorCode
|
||||
// Flag indicating whether the test should pass.
|
||||
shouldPass bool
|
||||
}{
|
||||
// Test case - 1.
|
||||
{bucketAccessPolicies[0], ErrNone, true},
|
||||
// Test case - 2.
|
||||
{bucketAccessPolicies[1], ErrNone, true},
|
||||
// Test case - 3.
|
||||
{bucketAccessPolicies[2], ErrNone, true},
|
||||
// Test case - 4.
|
||||
// contains invalidPrefixActions (check bucket-policy-parser.go).
|
||||
// Resource prefix will not be equal to the bucket name in this case.
|
||||
{bucketAccessPolicies[3], ErrMalformedPolicy, false},
|
||||
// Test case - 5.
|
||||
// actions contain invalidPrefixActions (check bucket-policy-parser.go).
|
||||
// Resource prefix bucket part is not equal to the bucket name in this case.
|
||||
{bucketAccessPolicies[4], ErrMalformedPolicy, false},
|
||||
// Test case - 6.
|
||||
// contracting policy statement with recursive resources.
|
||||
// should result in ErrPolicyNesting.
|
||||
{bucketAccessPolicies[5], ErrPolicyNesting, false},
|
||||
// Test case - 7.
|
||||
// constructing policy statement with lexically close
|
||||
// characters.
|
||||
// should result in ErrNone.
|
||||
{bucketAccessPolicies[6], ErrNone, true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
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)
|
||||
}
|
||||
if apiErrCode == ErrNone && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to fail with ErrCode %v, but passed instead", i+1, testCase.apiErrCode)
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if apiErrCode != ErrNone && !testCase.shouldPass {
|
||||
if testCase.apiErrCode != apiErrCode {
|
||||
t.Errorf("Test %d: Expected to fail with error code %v, but instead failed with error code %v", i+1, testCase.apiErrCode, apiErrCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests validate parsing of BucketAccessPolicy.
|
||||
func TestParseBucketPolicy(t *testing.T) {
|
||||
// set Unsupported Actions.
|
||||
setUnsupportedActions := func(statements []policyStatement) []policyStatement {
|
||||
// "s3:DeleteEverything"" is an Unsupported Action.
|
||||
statements[0].Actions = []string{"s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:DeleteEverything"}
|
||||
return statements
|
||||
}
|
||||
// set unsupported Effect.
|
||||
setUnsupportedEffect := func(statements []policyStatement) []policyStatement {
|
||||
// Effect "Don't allow" is Unsupported.
|
||||
statements[0].Effect = "DontAllow"
|
||||
return statements
|
||||
}
|
||||
// set unsupported principals.
|
||||
setUnsupportedPrincipals := func(statements []policyStatement) []policyStatement {
|
||||
// "User1111"" is an Unsupported Principal.
|
||||
statements[0].Principal.AWS = []string{"*", "User1111"}
|
||||
return statements
|
||||
}
|
||||
// set unsupported Resources.
|
||||
setUnsupportedResources := func(statements []policyStatement) []policyStatement {
|
||||
// "s3:DeleteEverything"" is an Unsupported Action.
|
||||
statements[0].Resources = []string{"my-resource"}
|
||||
return statements
|
||||
}
|
||||
// List of bucketPolicy used for test cases.
|
||||
bucketAccesPolicies := []bucketPolicy{
|
||||
// bucketPolicy - 0.
|
||||
// bucketPolicy statement empty.
|
||||
{Version: "1.0"},
|
||||
// bucketPolicy - 1.
|
||||
// bucketPolicy version empty.
|
||||
{Version: "", Statements: []policyStatement{}},
|
||||
// bucketPolicy - 2.
|
||||
// Readonly bucketPolicy.
|
||||
{Version: "1.0", Statements: getReadOnlyStatement("minio-bucket", "")},
|
||||
// bucketPolicy - 3.
|
||||
// Read-Write bucket policy.
|
||||
{Version: "1.0", Statements: getReadWriteStatement("minio-bucket", "Asia/")},
|
||||
// bucketPolicy - 4.
|
||||
// Write only bucket policy.
|
||||
{Version: "1.0", Statements: getWriteOnlyStatement("minio-bucket", "Asia/India/")},
|
||||
// bucketPolicy - 5.
|
||||
// bucketPolicy statement contains unsupported action.
|
||||
{Version: "1.0", Statements: setUnsupportedActions(getReadOnlyStatement("minio-bucket", ""))},
|
||||
// bucketPolicy - 6.
|
||||
// bucketPolicy statement contains unsupported Effect.
|
||||
{Version: "1.0", Statements: setUnsupportedEffect(getReadWriteStatement("minio-bucket", "Asia/"))},
|
||||
// bucketPolicy - 7.
|
||||
// bucketPolicy statement contains unsupported Principal.
|
||||
{Version: "1.0", Statements: setUnsupportedPrincipals(getWriteOnlyStatement("minio-bucket", "Asia/India/"))},
|
||||
// bucketPolicy - 8.
|
||||
// bucketPolicy statement contains unsupported Resource.
|
||||
{Version: "1.0", Statements: setUnsupportedResources(getWriteOnlyStatement("minio-bucket", "Asia/India/"))},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
inputPolicy bucketPolicy
|
||||
// expected results.
|
||||
expectedPolicy bucketPolicy
|
||||
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},
|
||||
// Test case - 2.
|
||||
// bucketPolicy version empty.
|
||||
{bucketAccesPolicies[1], bucketPolicy{}, errors.New("Policy version cannot be empty."), false},
|
||||
// Test case - 3.
|
||||
// Readonly bucketPolicy.
|
||||
{bucketAccesPolicies[2], bucketAccesPolicies[2], nil, true},
|
||||
// Test case - 4.
|
||||
// Read-Write bucket policy.
|
||||
{bucketAccesPolicies[3], bucketAccesPolicies[3], nil, true},
|
||||
// Test case - 5.
|
||||
// Write only bucket policy.
|
||||
{bucketAccesPolicies[4], bucketAccesPolicies[4], nil, true},
|
||||
// Test case - 6.
|
||||
// bucketPolicy statement contains unsupported action.
|
||||
{bucketAccesPolicies[5], bucketAccesPolicies[5], fmt.Errorf("Unsupported action found: ‘s3:DeleteEverything’, please validate your policy document."), false},
|
||||
// Test case - 7.
|
||||
// bucketPolicy statement contains unsupported Effect.
|
||||
{bucketAccesPolicies[6], bucketAccesPolicies[6], fmt.Errorf("Unsupported Effect found: ‘DontAllow’, please validate your policy document."), false},
|
||||
// Test case - 8.
|
||||
// bucketPolicy statement contains unsupported Principal.
|
||||
{bucketAccesPolicies[7], bucketAccesPolicies[7], fmt.Errorf("Unsupported principal style found: ‘User1111’, please validate your policy document."), false},
|
||||
// Test case - 9.
|
||||
// bucketPolicy statement contains unsupported Resource.
|
||||
{bucketAccesPolicies[8], bucketAccesPolicies[8], fmt.Errorf("Unsupported resource style found: ‘my-resource’, please validate your policy document."), false},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
var buffer bytes.Buffer
|
||||
encoder := json.NewEncoder(&buffer)
|
||||
err := encoder.Encode(testCase.inputPolicy)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Couldn't Marshal bucket policy %s", i+1, err)
|
||||
}
|
||||
|
||||
var actualAccessPolicy = &bucketPolicy{}
|
||||
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())
|
||||
}
|
||||
if err == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.err.Error())
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if err != nil && !testCase.shouldPass {
|
||||
if err.Error() != testCase.err.Error() {
|
||||
t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\"", i+1, testCase.err.Error(), err.Error())
|
||||
}
|
||||
}
|
||||
// Test passes as expected, but the output values are verified for correctness here.
|
||||
if err == nil && testCase.shouldPass {
|
||||
if testCase.expectedPolicy.String() != actualAccessPolicy.String() {
|
||||
t.Errorf("Test %d: The expected statements from resource statement generator doesn't match the actual statements", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
-101
@@ -17,135 +17,164 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"path"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// getBucketsConfigPath - get buckets path.
|
||||
func getBucketsConfigPath() (string, *probe.Error) {
|
||||
// Variable represents bucket policies in memory.
|
||||
var globalBucketPolicies *bucketPolicies
|
||||
|
||||
// Global bucket policies list, policies are enforced on each bucket looking
|
||||
// through the policies here.
|
||||
type bucketPolicies struct {
|
||||
rwMutex *sync.RWMutex
|
||||
|
||||
// Collection of 'bucket' policies.
|
||||
bucketPolicyConfigs map[string]*bucketPolicy
|
||||
}
|
||||
|
||||
// Fetch bucket policy for a given bucket.
|
||||
func (bp bucketPolicies) GetBucketPolicy(bucket string) *bucketPolicy {
|
||||
bp.rwMutex.RLock()
|
||||
defer bp.rwMutex.RUnlock()
|
||||
return bp.bucketPolicyConfigs[bucket]
|
||||
}
|
||||
|
||||
// Set a new bucket policy for a bucket, this operation will overwrite
|
||||
// any previous bucketpolicies for the bucket.
|
||||
func (bp *bucketPolicies) SetBucketPolicy(bucket string, policy *bucketPolicy) error {
|
||||
bp.rwMutex.Lock()
|
||||
defer bp.rwMutex.Unlock()
|
||||
if policy == nil {
|
||||
return errors.New("invalid argument")
|
||||
}
|
||||
bp.bucketPolicyConfigs[bucket] = policy
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove bucket policy for a bucket, from in-memory map.
|
||||
func (bp *bucketPolicies) RemoveBucketPolicy(bucket string) {
|
||||
bp.rwMutex.Lock()
|
||||
defer bp.rwMutex.Unlock()
|
||||
delete(bp.bucketPolicyConfigs, bucket)
|
||||
}
|
||||
|
||||
// Loads all bucket policies from persistent layer.
|
||||
func loadAllBucketPolicies(objAPI ObjectLayer) (policies map[string]*bucketPolicy, err error) {
|
||||
// List buckets to proceed loading all notification configuration.
|
||||
buckets, err := objAPI.ListBuckets()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policies = make(map[string]*bucketPolicy)
|
||||
// Loads bucket policy.
|
||||
for _, bucket := range buckets {
|
||||
var policy *bucketPolicy
|
||||
policy, err = readBucketPolicy(bucket.Name, objAPI)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case BucketPolicyNotFound:
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
policies[bucket.Name] = policy
|
||||
}
|
||||
|
||||
// Success.
|
||||
return policies, nil
|
||||
|
||||
}
|
||||
|
||||
// Intialize all bucket policies.
|
||||
func initBucketPolicies(objAPI ObjectLayer) error {
|
||||
if objAPI == nil {
|
||||
return errInvalidArgument
|
||||
}
|
||||
// Read all bucket policies.
|
||||
policies, err := loadAllBucketPolicies(objAPI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
globalBucketPolicies = &bucketPolicies{
|
||||
rwMutex: &sync.RWMutex{},
|
||||
bucketPolicyConfigs: policies,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getOldBucketsConfigPath - get old buckets config path. (Only used for migrating old bucket policies)
|
||||
func getOldBucketsConfigPath() (string, error) {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
return "", err.Trace()
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(configPath, "buckets"), nil
|
||||
return path.Join(configPath, "buckets"), nil
|
||||
}
|
||||
|
||||
// createBucketsConfigPath - create buckets directory.
|
||||
func createBucketsConfigPath() *probe.Error {
|
||||
bucketsConfigPath, err := getBucketsConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e := os.MkdirAll(bucketsConfigPath, 0700); e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBucketConfigPath - get bucket config path.
|
||||
func getBucketConfigPath(bucket string) (string, *probe.Error) {
|
||||
bucketsConfigPath, err := getBucketsConfigPath()
|
||||
if err != nil {
|
||||
return "", err.Trace()
|
||||
}
|
||||
return filepath.Join(bucketsConfigPath, bucket), nil
|
||||
}
|
||||
|
||||
// createBucketConfigPath - create bucket config directory.
|
||||
func createBucketConfigPath(bucket string) *probe.Error {
|
||||
bucketConfigPath, err := getBucketConfigPath(bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e := os.MkdirAll(bucketConfigPath, 0700); e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readBucketPolicy - read bucket policy.
|
||||
func readBucketPolicy(bucket string) ([]byte, *probe.Error) {
|
||||
// readBucketPolicy - reads bucket policy for an input bucket, returns BucketPolicyNotFound
|
||||
// if bucket policy is not found. This function also parses the bucket policy into an object.
|
||||
func readBucketPolicy(bucket string, objAPI ObjectLayer) (*bucketPolicy, error) {
|
||||
// Verify bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return nil, probe.NewError(BucketNameInvalid{Bucket: bucket})
|
||||
return nil, BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
|
||||
bucketConfigPath, err := getBucketConfigPath(bucket)
|
||||
policyPath := pathJoin(bucketConfigPrefix, bucket, policyJSON)
|
||||
objInfo, err := objAPI.GetObjectInfo(minioMetaBucket, policyPath)
|
||||
if err != nil {
|
||||
return nil, err.Trace()
|
||||
}
|
||||
|
||||
// Get policy file.
|
||||
bucketPolicyFile := filepath.Join(bucketConfigPath, "access-policy.json")
|
||||
if _, e := os.Stat(bucketPolicyFile); e != nil {
|
||||
if os.IsNotExist(e) {
|
||||
return nil, probe.NewError(BucketPolicyNotFound{Bucket: bucket})
|
||||
if _, ok := err.(ObjectNotFound); ok {
|
||||
return nil, BucketPolicyNotFound{Bucket: bucket}
|
||||
}
|
||||
return nil, probe.NewError(e)
|
||||
return nil, err
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
err = objAPI.GetObject(minioMetaBucket, policyPath, 0, objInfo.Size, &buffer)
|
||||
if err != nil {
|
||||
if _, ok := err.(ObjectNotFound); ok {
|
||||
return nil, BucketPolicyNotFound{Bucket: bucket}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// Parse the saved policy.
|
||||
var policy = &bucketPolicy{}
|
||||
err = parseBucketPolicy(&buffer, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
accessPolicyBytes, e := ioutil.ReadFile(bucketPolicyFile)
|
||||
if e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
return accessPolicyBytes, nil
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// removeBucketPolicy - remove bucket policy.
|
||||
func removeBucketPolicy(bucket string) *probe.Error {
|
||||
// removeBucketPolicy - removes any previously written bucket policy. Returns BucketPolicyNotFound
|
||||
// if no policies are found.
|
||||
func removeBucketPolicy(bucket string, objAPI ObjectLayer) error {
|
||||
// Verify bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return probe.NewError(BucketNameInvalid{Bucket: bucket})
|
||||
return BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
|
||||
bucketConfigPath, err := getBucketConfigPath(bucket)
|
||||
if err != nil {
|
||||
return err.Trace(bucket)
|
||||
}
|
||||
|
||||
// Get policy file.
|
||||
bucketPolicyFile := filepath.Join(bucketConfigPath, "access-policy.json")
|
||||
if _, e := os.Stat(bucketPolicyFile); e != nil {
|
||||
if os.IsNotExist(e) {
|
||||
return probe.NewError(BucketPolicyNotFound{Bucket: bucket})
|
||||
policyPath := pathJoin(bucketConfigPrefix, bucket, policyJSON)
|
||||
if err := objAPI.DeleteObject(minioMetaBucket, policyPath); err != nil {
|
||||
if _, ok := err.(ObjectNotFound); ok {
|
||||
return BucketPolicyNotFound{Bucket: bucket}
|
||||
}
|
||||
return probe.NewError(e)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeBucketPolicy - save bucket policy.
|
||||
func writeBucketPolicy(bucket string, accessPolicyBytes []byte) *probe.Error {
|
||||
// writeBucketPolicy - save all bucket policies.
|
||||
func writeBucketPolicy(bucket string, objAPI ObjectLayer, reader io.Reader, size int64) error {
|
||||
// Verify if bucket path legal
|
||||
if !IsValidBucketName(bucket) {
|
||||
return probe.NewError(BucketNameInvalid{Bucket: bucket})
|
||||
return BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
|
||||
// Create bucket config path.
|
||||
if err := createBucketConfigPath(bucket); err != nil {
|
||||
return err.Trace()
|
||||
}
|
||||
|
||||
bucketConfigPath, err := getBucketConfigPath(bucket)
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
}
|
||||
|
||||
// Get policy file.
|
||||
bucketPolicyFile := filepath.Join(bucketConfigPath, "access-policy.json")
|
||||
if _, e := os.Stat(bucketPolicyFile); e != nil {
|
||||
if !os.IsNotExist(e) {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Write bucket policy.
|
||||
if e := ioutil.WriteFile(bucketPolicyFile, accessPolicyBytes, 0600); e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
|
||||
return nil
|
||||
policyPath := pathJoin(bucketConfigPrefix, bucket, policyJSON)
|
||||
_, err := objAPI.PutObject(minioMetaBucket, policyPath, size, reader, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
+6
-1
@@ -16,13 +16,18 @@
|
||||
|
||||
package main
|
||||
|
||||
// DO NOT EDIT THIS FILE DIRECTLY. These are build-time constants
|
||||
// set through ‘buildscripts/gen-ldflags.go’.
|
||||
var (
|
||||
// minioGOPATH - GOPATH value at the time of build.
|
||||
minioGOPATH = ""
|
||||
|
||||
// minioVersion - version time.RFC3339.
|
||||
minioVersion = "DEVELOPMENT.GOGET"
|
||||
// minioReleaseTag - release tag in TAG.%Y-%m-%dT%H-%M-%SZ.
|
||||
minioReleaseTag = "DEVELOPMENT.GOGET"
|
||||
// minioCommitID - latest commit id.
|
||||
minioCommitID = "DEVELOPMENT.GOGET"
|
||||
// minioShortCommitID - first 12 characters from mcCommitID
|
||||
// minioShortCommitID - first 12 characters from minioCommitID.
|
||||
minioShortCommitID = minioCommitID[:12]
|
||||
)
|
||||
|
||||
@@ -212,7 +212,7 @@ main() {
|
||||
echo "Please install them "
|
||||
echo "${MISSING}"
|
||||
echo
|
||||
echo "Follow https://github.com/minio/minio/blob/master/INSTALLGO.md for further instructions"
|
||||
echo "Follow https://docs.minio.io/docs/how-to-install-golang for further instructions"
|
||||
exit 1
|
||||
fi
|
||||
echo "Done"
|
||||
|
||||
@@ -28,10 +28,11 @@ import (
|
||||
|
||||
func genLDFlags(version string) string {
|
||||
var ldflagsStr string
|
||||
ldflagsStr = "-X main.minioVersion=" + version + " "
|
||||
ldflagsStr = ldflagsStr + "-X main.minioReleaseTag=" + releaseTag(version) + " "
|
||||
ldflagsStr = ldflagsStr + "-X main.minioCommitID=" + commitID() + " "
|
||||
ldflagsStr = ldflagsStr + "-X main.minioShortCommitID=" + commitID()[:12]
|
||||
ldflagsStr = "-X main.minioVersion=" + version
|
||||
ldflagsStr += " -X main.minioReleaseTag=" + releaseTag(version)
|
||||
ldflagsStr += " -X main.minioCommitID=" + commitID()
|
||||
ldflagsStr += " -X main.minioShortCommitID=" + commitID()[:12]
|
||||
ldflagsStr += " -X main.minioGOPATH=" + os.Getenv("GOPATH")
|
||||
return ldflagsStr
|
||||
}
|
||||
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
echo "" > coverage.txt
|
||||
|
||||
for d in $(go list ./... | grep -v vendor); do
|
||||
go test -coverprofile=profile.out -covermode=atomic $d
|
||||
if [ -f profile.out ]; then
|
||||
cat profile.out >> coverage.txt
|
||||
rm profile.out
|
||||
fi
|
||||
done
|
||||
@@ -19,37 +19,32 @@ package main
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/minio/go-homedir"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// createCertsPath create certs path.
|
||||
func createCertsPath() *probe.Error {
|
||||
func createCertsPath() error {
|
||||
certsPath, err := getCertsPath()
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(certsPath, 0700); err != nil {
|
||||
return probe.NewError(err)
|
||||
}
|
||||
return nil
|
||||
return os.MkdirAll(certsPath, 0700)
|
||||
}
|
||||
|
||||
// getCertsPath get certs path.
|
||||
func getCertsPath() (string, *probe.Error) {
|
||||
homeDir, e := homedir.Dir()
|
||||
if e != nil {
|
||||
return "", probe.NewError(e)
|
||||
func getCertsPath() (string, error) {
|
||||
var certsPath string
|
||||
configDir, err := getConfigPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
certsPath := filepath.Join(homeDir, globalMinioCertsDir)
|
||||
certsPath = filepath.Join(configDir, globalMinioCertsDir)
|
||||
return certsPath, nil
|
||||
}
|
||||
|
||||
// mustGetCertsPath must get certs path.
|
||||
func mustGetCertsPath() string {
|
||||
certsPath, err := getCertsPath()
|
||||
fatalIf(err.Trace(), "Unable to retrieve certs path.", nil)
|
||||
fatalIf(err, "Failed to get certificate path.")
|
||||
return certsPath
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Make sure we have a valid certs path.
|
||||
func TestGetCertsPath(t *testing.T) {
|
||||
path, err := getCertsPath()
|
||||
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")
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
file = mustGetKeyFile()
|
||||
if !strings.Contains(file, globalMinioKeyFile) {
|
||||
t.Errorf("KeyFile does not contain %s", globalMinioKeyFile)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Make sure that none of the other processes are listening on the
|
||||
// specified port on any of the interfaces.
|
||||
//
|
||||
// On linux if a process is listening on 127.0.0.1:9000 then Listen()
|
||||
// on ":9000" fails with the error "port already in use".
|
||||
// However on Mac OSX Listen() on ":9000" falls back to the IPv6 address.
|
||||
// This causes confusion on Mac OSX that minio server is not reachable
|
||||
// on 127.0.0.1 even though minio server is running. So before we start
|
||||
// the minio server we make sure that the port is free on each tcp network.
|
||||
func checkPortAvailability(port int) error {
|
||||
network := [3]string{"tcp", "tcp4", "tcp6"}
|
||||
for _, n := range network {
|
||||
l, err := net.Listen(n, fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// look for error so we don't have dangling connection
|
||||
if err = l.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests for port availability logic written for server startup sequence.
|
||||
func TestCheckPortAvailability(t *testing.T) {
|
||||
tests := []struct {
|
||||
port int
|
||||
}{
|
||||
{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: %d. Error: %v", test.port, err)
|
||||
}
|
||||
|
||||
// Now use the ports and check again
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", 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 != "windows" {
|
||||
t.Fatalf("checkPortAvailability should fail for port: %d. Error: %v", test.port, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-8
@@ -26,19 +26,15 @@ var commandsTree = newTrie()
|
||||
|
||||
// Collection of minio flags currently supported.
|
||||
var globalFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "quiet, q",
|
||||
Usage: "Suppress chatty console output.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Enable debugging output.",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "config-dir, C",
|
||||
Value: mustGetConfigPath(),
|
||||
Usage: "Path to configuration folder.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Usage: "Suppress chatty output.",
|
||||
},
|
||||
}
|
||||
|
||||
// registerCommand registers a cli command.
|
||||
|
||||
+139
-28
@@ -22,7 +22,6 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
@@ -33,40 +32,40 @@ func migrateConfig() {
|
||||
migrateV2ToV3()
|
||||
// Migrate version '3' to '4'.
|
||||
migrateV3ToV4()
|
||||
// Migrate version '4' to '5'.
|
||||
migrateV4ToV5()
|
||||
// Migrate version '5' to '6.
|
||||
migrateV5ToV6()
|
||||
}
|
||||
|
||||
// Version '1' is not supported anymore and deprecated, safe to delete.
|
||||
func purgeV1() {
|
||||
cv1, err := loadConfigV1()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err.ToGoError()) {
|
||||
return
|
||||
}
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
fatalIf(err.Trace(), "Unable to load config version ‘1’.", nil)
|
||||
fatalIf(err, "Unable to load config version ‘1’.")
|
||||
|
||||
if cv1.Version == "1" {
|
||||
console.Println("Unsupported config version ‘1’ found, removed successfully.")
|
||||
console.Println("Removed unsupported config version ‘1’.")
|
||||
/// Purge old fsUsers.json file
|
||||
configPath, err := getConfigPath()
|
||||
fatalIf(err.Trace(), "Unable to retrieve config path.", nil)
|
||||
fatalIf(err, "Unable to retrieve config path.")
|
||||
|
||||
configFile := filepath.Join(configPath, "fsUsers.json")
|
||||
os.RemoveAll(configFile)
|
||||
removeAll(configFile)
|
||||
}
|
||||
fatalIf(probe.NewError(errors.New("")), "Unexpected version found ‘"+cv1.Version+"’, cannot migrate.", nil)
|
||||
fatalIf(errors.New(""), "Failed to migrate unrecognized config version ‘"+cv1.Version+"’.")
|
||||
}
|
||||
|
||||
// Version '2' to '3' config migration adds new fields and re-orders
|
||||
// previous fields. Simplifies config for future additions.
|
||||
func migrateV2ToV3() {
|
||||
cv2, err := loadConfigV2()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err.ToGoError()) {
|
||||
return
|
||||
}
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
fatalIf(err.Trace(), "Unable to load config version ‘2’.", nil)
|
||||
fatalIf(err, "Unable to load config version ‘2’.")
|
||||
if cv2.Version != "2" {
|
||||
return
|
||||
}
|
||||
@@ -78,6 +77,10 @@ func migrateV2ToV3() {
|
||||
SecretAccessKey: cv2.Credentials.SecretAccessKey,
|
||||
}
|
||||
srvConfig.Region = cv2.Credentials.Region
|
||||
if srvConfig.Region == "" {
|
||||
// Region needs to be set for AWS Signature V4.
|
||||
srvConfig.Region = "us-east-1"
|
||||
}
|
||||
srvConfig.Logger.Console = consoleLogger{
|
||||
Enable: true,
|
||||
Level: "fatal",
|
||||
@@ -99,14 +102,14 @@ func migrateV2ToV3() {
|
||||
srvConfig.Logger.Syslog = slogger
|
||||
|
||||
qc, err := quick.New(srvConfig)
|
||||
fatalIf(err.Trace(), "Unable to initialize config.", nil)
|
||||
fatalIf(err, "Unable to initialize config.")
|
||||
|
||||
configFile, err := getConfigFile()
|
||||
fatalIf(err.Trace(), "Unable to get config file.", nil)
|
||||
fatalIf(err, "Unable to get config file.")
|
||||
|
||||
// Migrate the config.
|
||||
err = qc.Save(configFile)
|
||||
fatalIf(err.Trace(), "Migrating from version ‘"+cv2.Version+"’ to ‘"+srvConfig.Version+"’ failed.", nil)
|
||||
fatalIf(err, "Failed to migrate config from ‘"+cv2.Version+"’ to ‘"+srvConfig.Version+"’ failed.")
|
||||
|
||||
console.Println("Migration from version ‘" + cv2.Version + "’ to ‘" + srvConfig.Version + "’ completed successfully.")
|
||||
}
|
||||
@@ -116,32 +119,140 @@ func migrateV2ToV3() {
|
||||
// the config for future additions.
|
||||
func migrateV3ToV4() {
|
||||
cv3, err := loadConfigV3()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err.ToGoError()) {
|
||||
return
|
||||
}
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
fatalIf(err.Trace(), "Unable to load config version ‘3’.", nil)
|
||||
fatalIf(err, "Unable to load config version ‘3’.")
|
||||
if cv3.Version != "3" {
|
||||
return
|
||||
}
|
||||
|
||||
// Save only the new fields, ignore the rest.
|
||||
srvConfig := &serverConfigV4{}
|
||||
srvConfig.Version = globalMinioConfigVersion
|
||||
srvConfig := &configV4{}
|
||||
srvConfig.Version = "4"
|
||||
srvConfig.Credential = cv3.Credential
|
||||
srvConfig.Region = cv3.Region
|
||||
if srvConfig.Region == "" {
|
||||
// Region needs to be set for AWS Signature Version 4.
|
||||
srvConfig.Region = "us-east-1"
|
||||
}
|
||||
srvConfig.Logger.Console = cv3.Logger.Console
|
||||
srvConfig.Logger.File = cv3.Logger.File
|
||||
srvConfig.Logger.Syslog = cv3.Logger.Syslog
|
||||
|
||||
qc, err := quick.New(srvConfig)
|
||||
fatalIf(err.Trace(), "Unable to initialize the quick config.", nil)
|
||||
fatalIf(err, "Unable to initialize the quick config.")
|
||||
configFile, err := getConfigFile()
|
||||
fatalIf(err.Trace(), "Unable to get config file.", nil)
|
||||
fatalIf(err, "Unable to get config file.")
|
||||
|
||||
err = qc.Save(configFile)
|
||||
fatalIf(err.Trace(), "Migrating from version ‘"+cv3.Version+"’ to ‘"+srvConfig.Version+"’ failed.", nil)
|
||||
fatalIf(err, "Failed to migrate config from ‘"+cv3.Version+"’ to ‘"+srvConfig.Version+"’ failed.")
|
||||
|
||||
console.Println("Migration from version ‘" + cv3.Version + "’ to ‘" + srvConfig.Version + "’ completed successfully.")
|
||||
}
|
||||
|
||||
// Version '5' to '6' migrates config, removes previous fields related
|
||||
// to backend types and server address. This change further simplifies
|
||||
// the config for future additions.
|
||||
func migrateV5ToV6() {
|
||||
cv5, err := loadConfigV5()
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
fatalIf(err, "Unable to load config version ‘5’.")
|
||||
if cv5.Version != "5" {
|
||||
return
|
||||
}
|
||||
|
||||
// Save only the new fields, ignore the rest.
|
||||
srvConfig := &serverConfigV6{}
|
||||
srvConfig.Version = globalMinioConfigVersion
|
||||
srvConfig.Credential = cv5.Credential
|
||||
srvConfig.Region = cv5.Region
|
||||
if srvConfig.Region == "" {
|
||||
// Region needs to be set for AWS Signature Version 4.
|
||||
srvConfig.Region = "us-east-1"
|
||||
}
|
||||
srvConfig.Logger.Console = cv5.Logger.Console
|
||||
srvConfig.Logger.File = cv5.Logger.File
|
||||
srvConfig.Logger.Syslog = cv5.Logger.Syslog
|
||||
srvConfig.Notify.AMQP = map[string]amqpNotify{
|
||||
"1": {
|
||||
Enable: cv5.Logger.AMQP.Enable,
|
||||
URL: cv5.Logger.AMQP.URL,
|
||||
Exchange: cv5.Logger.AMQP.Exchange,
|
||||
RoutingKey: cv5.Logger.AMQP.RoutingKey,
|
||||
Mandatory: cv5.Logger.AMQP.Mandatory,
|
||||
Immediate: cv5.Logger.AMQP.Immediate,
|
||||
Durable: cv5.Logger.AMQP.Durable,
|
||||
Internal: cv5.Logger.AMQP.Internal,
|
||||
NoWait: cv5.Logger.AMQP.NoWait,
|
||||
AutoDeleted: cv5.Logger.AMQP.AutoDeleted,
|
||||
},
|
||||
}
|
||||
srvConfig.Notify.ElasticSearch = map[string]elasticSearchNotify{
|
||||
"1": {
|
||||
Enable: cv5.Logger.ElasticSearch.Enable,
|
||||
URL: cv5.Logger.ElasticSearch.URL,
|
||||
Index: cv5.Logger.ElasticSearch.Index,
|
||||
},
|
||||
}
|
||||
srvConfig.Notify.Redis = map[string]redisNotify{
|
||||
"1": {
|
||||
Enable: cv5.Logger.Redis.Enable,
|
||||
Addr: cv5.Logger.Redis.Addr,
|
||||
Password: cv5.Logger.Redis.Password,
|
||||
Key: cv5.Logger.Redis.Key,
|
||||
},
|
||||
}
|
||||
|
||||
qc, err := quick.New(srvConfig)
|
||||
fatalIf(err, "Unable to initialize the quick config.")
|
||||
configFile, err := getConfigFile()
|
||||
fatalIf(err, "Unable to get config file.")
|
||||
|
||||
err = qc.Save(configFile)
|
||||
fatalIf(err, "Failed to migrate config from ‘"+cv5.Version+"’ to ‘"+srvConfig.Version+"’ failed.")
|
||||
|
||||
console.Println("Migration from version ‘" + cv5.Version + "’ to ‘" + srvConfig.Version + "’ completed successfully.")
|
||||
}
|
||||
|
||||
// Version '4' to '5' migrates config, removes previous fields related
|
||||
// to backend types and server address. This change further simplifies
|
||||
// the config for future additions.
|
||||
func migrateV4ToV5() {
|
||||
cv4, err := loadConfigV4()
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
fatalIf(err, "Unable to load config version ‘4’.")
|
||||
if cv4.Version != "4" {
|
||||
return
|
||||
}
|
||||
|
||||
// Save only the new fields, ignore the rest.
|
||||
srvConfig := &configV5{}
|
||||
srvConfig.Version = globalMinioConfigVersion
|
||||
srvConfig.Credential = cv4.Credential
|
||||
srvConfig.Region = cv4.Region
|
||||
if srvConfig.Region == "" {
|
||||
// Region needs to be set for AWS Signature Version 4.
|
||||
srvConfig.Region = "us-east-1"
|
||||
}
|
||||
srvConfig.Logger.Console = cv4.Logger.Console
|
||||
srvConfig.Logger.File = cv4.Logger.File
|
||||
srvConfig.Logger.Syslog = cv4.Logger.Syslog
|
||||
srvConfig.Logger.AMQP.Enable = false
|
||||
srvConfig.Logger.ElasticSearch.Enable = false
|
||||
srvConfig.Logger.Redis.Enable = false
|
||||
|
||||
qc, err := quick.New(srvConfig)
|
||||
fatalIf(err, "Unable to initialize the quick config.")
|
||||
configFile, err := getConfigFile()
|
||||
fatalIf(err, "Unable to get config file.")
|
||||
|
||||
err = qc.Save(configFile)
|
||||
fatalIf(err, "Failed to migrate config from ‘"+cv4.Version+"’ to ‘"+srvConfig.Version+"’ failed.")
|
||||
|
||||
console.Println("Migration from version ‘" + cv4.Version + "’ to ‘" + srvConfig.Version + "’ completed successfully.")
|
||||
}
|
||||
|
||||
+159
-31
@@ -4,7 +4,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
@@ -16,25 +15,25 @@ type configV1 struct {
|
||||
}
|
||||
|
||||
// loadConfigV1 load config
|
||||
func loadConfigV1() (*configV1, *probe.Error) {
|
||||
func loadConfigV1() (*configV1, error) {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
return nil, err.Trace()
|
||||
return nil, err
|
||||
}
|
||||
configFile := filepath.Join(configPath, "fsUsers.json")
|
||||
if _, err := os.Stat(configFile); err != nil {
|
||||
return nil, probe.NewError(err)
|
||||
if _, err = os.Stat(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a := &configV1{}
|
||||
a.Version = "1"
|
||||
qc, err := quick.New(a)
|
||||
c := &configV1{}
|
||||
c.Version = "1"
|
||||
qc, err := quick.New(c)
|
||||
if err != nil {
|
||||
return nil, err.Trace()
|
||||
return nil, err
|
||||
}
|
||||
if err := qc.Load(configFile); err != nil {
|
||||
return nil, err.Trace()
|
||||
return nil, err
|
||||
}
|
||||
return qc.Data().(*configV1), nil
|
||||
return c, nil
|
||||
}
|
||||
|
||||
/////////////////// Config V2 ///////////////////
|
||||
@@ -60,24 +59,24 @@ type configV2 struct {
|
||||
}
|
||||
|
||||
// loadConfigV2 load config version '2'.
|
||||
func loadConfigV2() (*configV2, *probe.Error) {
|
||||
func loadConfigV2() (*configV2, error) {
|
||||
configFile, err := getConfigFile()
|
||||
if err != nil {
|
||||
return nil, err.Trace()
|
||||
return nil, err
|
||||
}
|
||||
if _, err := os.Stat(configFile); err != nil {
|
||||
return nil, probe.NewError(err)
|
||||
if _, err = os.Stat(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a := &configV2{}
|
||||
a.Version = "2"
|
||||
qc, err := quick.New(a)
|
||||
c := &configV2{}
|
||||
c.Version = "2"
|
||||
qc, err := quick.New(c)
|
||||
if err != nil {
|
||||
return nil, err.Trace()
|
||||
return nil, err
|
||||
}
|
||||
if err := qc.Load(configFile); err != nil {
|
||||
return nil, err.Trace()
|
||||
return nil, err
|
||||
}
|
||||
return qc.Data().(*configV2), nil
|
||||
return c, nil
|
||||
}
|
||||
|
||||
/////////////////// Config V3 ///////////////////
|
||||
@@ -127,22 +126,151 @@ type configV3 struct {
|
||||
}
|
||||
|
||||
// loadConfigV3 load config version '3'.
|
||||
func loadConfigV3() (*configV3, *probe.Error) {
|
||||
func loadConfigV3() (*configV3, error) {
|
||||
configFile, err := getConfigFile()
|
||||
if err != nil {
|
||||
return nil, err.Trace()
|
||||
return nil, err
|
||||
}
|
||||
if _, err := os.Stat(configFile); err != nil {
|
||||
return nil, probe.NewError(err)
|
||||
if _, err = os.Stat(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a := &configV3{}
|
||||
a.Version = "3"
|
||||
qc, err := quick.New(a)
|
||||
c := &configV3{}
|
||||
c.Version = "3"
|
||||
qc, err := quick.New(c)
|
||||
if err != nil {
|
||||
return nil, err.Trace()
|
||||
return nil, err
|
||||
}
|
||||
if err := qc.Load(configFile); err != nil {
|
||||
return nil, err.Trace()
|
||||
return nil, err
|
||||
}
|
||||
return qc.Data().(*configV3), nil
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// logger type representing version '4' logger config.
|
||||
type loggerV4 struct {
|
||||
Console struct {
|
||||
Enable bool `json:"enable"`
|
||||
Level string `json:"level"`
|
||||
} `json:"console"`
|
||||
File struct {
|
||||
Enable bool `json:"enable"`
|
||||
Filename string `json:"fileName"`
|
||||
Level string `json:"level"`
|
||||
} `json:"file"`
|
||||
Syslog struct {
|
||||
Enable bool `json:"enable"`
|
||||
Addr string `json:"address"`
|
||||
Level string `json:"level"`
|
||||
} `json:"syslog"`
|
||||
}
|
||||
|
||||
// configV4 server configuration version '4'.
|
||||
type configV4 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger loggerV4 `json:"logger"`
|
||||
}
|
||||
|
||||
// loadConfigV4 load config version '4'.
|
||||
func loadConfigV4() (*configV4, error) {
|
||||
configFile, err := getConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = os.Stat(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &configV4{}
|
||||
c.Version = "4"
|
||||
qc, err := quick.New(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := qc.Load(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// logger type representing version '5' logger config.
|
||||
type loggerV5 struct {
|
||||
Console struct {
|
||||
Enable bool `json:"enable"`
|
||||
Level string `json:"level"`
|
||||
} `json:"console"`
|
||||
File struct {
|
||||
Enable bool `json:"enable"`
|
||||
Filename string `json:"fileName"`
|
||||
Level string `json:"level"`
|
||||
} `json:"file"`
|
||||
Syslog struct {
|
||||
Enable bool `json:"enable"`
|
||||
Addr string `json:"address"`
|
||||
Level string `json:"level"`
|
||||
} `json:"syslog"`
|
||||
AMQP struct {
|
||||
Enable bool `json:"enable"`
|
||||
Level string `json:"level"`
|
||||
URL string `json:"url"`
|
||||
Exchange string `json:"exchange"`
|
||||
RoutingKey string `json:"routingKey"`
|
||||
ExchangeType string `json:"exchangeType"`
|
||||
Mandatory bool `json:"mandatory"`
|
||||
Immediate bool `json:"immediate"`
|
||||
Durable bool `json:"durable"`
|
||||
Internal bool `json:"internal"`
|
||||
NoWait bool `json:"noWait"`
|
||||
AutoDeleted bool `json:"autoDeleted"`
|
||||
} `json:"amqp"`
|
||||
ElasticSearch struct {
|
||||
Enable bool `json:"enable"`
|
||||
Level string `json:"level"`
|
||||
URL string `json:"url"`
|
||||
Index string `json:"index"`
|
||||
} `json:"elasticsearch"`
|
||||
Redis struct {
|
||||
Enable bool `json:"enable"`
|
||||
Level string `json:"level"`
|
||||
Addr string `json:"address"`
|
||||
Password string `json:"password"`
|
||||
Key string `json:"key"`
|
||||
} `json:"redis"`
|
||||
}
|
||||
|
||||
// configV5 server configuration version '5'.
|
||||
type configV5 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger loggerV5 `json:"logger"`
|
||||
}
|
||||
|
||||
// loadConfigV5 load config version '5'.
|
||||
func loadConfigV5() (*configV5, error) {
|
||||
configFile, err := getConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = os.Stat(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &configV5{}
|
||||
c.Version = "5"
|
||||
qc, err := quick.New(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := qc.Load(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
+93
-45
@@ -20,12 +20,11 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
// serverConfigV4 server configuration version '4'.
|
||||
type serverConfigV4 struct {
|
||||
// serverConfigV6 server configuration version '5'.
|
||||
type serverConfigV6 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
@@ -35,74 +34,71 @@ type serverConfigV4 struct {
|
||||
// Additional error logging configuration.
|
||||
Logger logger `json:"logger"`
|
||||
|
||||
// Notification queue configuration.
|
||||
Notify notifier `json:"notify"`
|
||||
|
||||
// Read Write mutex.
|
||||
rwMutex *sync.RWMutex
|
||||
}
|
||||
|
||||
// initConfig - initialize server config. config version (called only once).
|
||||
func initConfig() *probe.Error {
|
||||
func initConfig() error {
|
||||
if !isConfigFileExists() {
|
||||
srvCfg := &serverConfigV4{}
|
||||
// Initialize server config.
|
||||
srvCfg := &serverConfigV6{}
|
||||
srvCfg.Version = globalMinioConfigVersion
|
||||
srvCfg.Region = "us-east-1"
|
||||
srvCfg.Credential = mustGenAccessKeys()
|
||||
|
||||
// Enable console logger by default on a fresh run.
|
||||
srvCfg.Logger.Console = consoleLogger{
|
||||
Enable: true,
|
||||
Level: "fatal",
|
||||
}
|
||||
srvCfg.rwMutex = &sync.RWMutex{}
|
||||
|
||||
// Create config path.
|
||||
err := createConfigPath()
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
}
|
||||
|
||||
// Create certs path.
|
||||
err = createCertsPath()
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the new config globally.
|
||||
serverConfig = srvCfg
|
||||
|
||||
// Save config into file.
|
||||
err = serverConfig.Save()
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
}
|
||||
return nil
|
||||
return serverConfig.Save()
|
||||
}
|
||||
configFile, err := getConfigFile()
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
return err
|
||||
}
|
||||
if _, e := os.Stat(configFile); err != nil {
|
||||
return probe.NewError(e)
|
||||
if _, err = os.Stat(configFile); err != nil {
|
||||
return err
|
||||
}
|
||||
srvCfg := &serverConfigV4{}
|
||||
srvCfg := &serverConfigV6{}
|
||||
srvCfg.Version = globalMinioConfigVersion
|
||||
srvCfg.rwMutex = &sync.RWMutex{}
|
||||
qc, err := quick.New(srvCfg)
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
return err
|
||||
}
|
||||
if err := qc.Load(configFile); err != nil {
|
||||
return err.Trace()
|
||||
return err
|
||||
}
|
||||
// Save the loaded config globally.
|
||||
serverConfig = qc.Data().(*serverConfigV4)
|
||||
serverConfig = srvCfg
|
||||
// Set the version properly after the unmarshalled json is loaded.
|
||||
serverConfig.Version = globalMinioConfigVersion
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// serverConfig server config.
|
||||
var serverConfig *serverConfigV4
|
||||
var serverConfig *serverConfigV6
|
||||
|
||||
// GetVersion get current config version.
|
||||
func (s serverConfigV4) GetVersion() string {
|
||||
func (s serverConfigV6) GetVersion() string {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Version
|
||||
@@ -110,98 +106,150 @@ func (s serverConfigV4) GetVersion() string {
|
||||
|
||||
/// Logger related.
|
||||
|
||||
func (s *serverConfigV6) SetAMQPNotifyByID(accountID string, amqpn amqpNotify) {
|
||||
s.rwMutex.Lock()
|
||||
defer s.rwMutex.Unlock()
|
||||
s.Notify.AMQP[accountID] = amqpn
|
||||
}
|
||||
|
||||
func (s serverConfigV6) GetAMQP() map[string]amqpNotify {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Notify.AMQP
|
||||
}
|
||||
|
||||
// GetAMQPNotify get current AMQP logger.
|
||||
func (s serverConfigV6) GetAMQPNotifyByID(accountID string) amqpNotify {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Notify.AMQP[accountID]
|
||||
}
|
||||
|
||||
func (s *serverConfigV6) SetElasticSearchNotifyByID(accountID string, esNotify elasticSearchNotify) {
|
||||
s.rwMutex.Lock()
|
||||
defer s.rwMutex.Unlock()
|
||||
s.Notify.ElasticSearch[accountID] = esNotify
|
||||
}
|
||||
|
||||
func (s serverConfigV6) GetElasticSearch() map[string]elasticSearchNotify {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Notify.ElasticSearch
|
||||
}
|
||||
|
||||
// GetElasticSearchNotify get current ElasicSearch logger.
|
||||
func (s serverConfigV6) GetElasticSearchNotifyByID(accountID string) elasticSearchNotify {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Notify.ElasticSearch[accountID]
|
||||
}
|
||||
|
||||
func (s *serverConfigV6) SetRedisNotifyByID(accountID string, rNotify redisNotify) {
|
||||
s.rwMutex.Lock()
|
||||
defer s.rwMutex.Unlock()
|
||||
s.Notify.Redis[accountID] = rNotify
|
||||
}
|
||||
|
||||
func (s serverConfigV6) GetRedis() map[string]redisNotify {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Notify.Redis
|
||||
}
|
||||
|
||||
// GetRedisNotify get current Redis logger.
|
||||
func (s serverConfigV6) GetRedisNotifyByID(accountID string) redisNotify {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Notify.Redis[accountID]
|
||||
}
|
||||
|
||||
// SetFileLogger set new file logger.
|
||||
func (s *serverConfigV4) SetFileLogger(flogger fileLogger) {
|
||||
func (s *serverConfigV6) SetFileLogger(flogger fileLogger) {
|
||||
s.rwMutex.Lock()
|
||||
defer s.rwMutex.Unlock()
|
||||
s.Logger.File = flogger
|
||||
}
|
||||
|
||||
// GetFileLogger get current file logger.
|
||||
func (s serverConfigV4) GetFileLogger() fileLogger {
|
||||
func (s serverConfigV6) GetFileLogger() fileLogger {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Logger.File
|
||||
}
|
||||
|
||||
// SetConsoleLogger set new console logger.
|
||||
func (s *serverConfigV4) SetConsoleLogger(clogger consoleLogger) {
|
||||
func (s *serverConfigV6) SetConsoleLogger(clogger consoleLogger) {
|
||||
s.rwMutex.Lock()
|
||||
defer s.rwMutex.Unlock()
|
||||
s.Logger.Console = clogger
|
||||
}
|
||||
|
||||
// GetConsoleLogger get current console logger.
|
||||
func (s serverConfigV4) GetConsoleLogger() consoleLogger {
|
||||
func (s serverConfigV6) GetConsoleLogger() consoleLogger {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Logger.Console
|
||||
}
|
||||
|
||||
// SetSyslogLogger set new syslog logger.
|
||||
func (s *serverConfigV4) SetSyslogLogger(slogger syslogLogger) {
|
||||
func (s *serverConfigV6) SetSyslogLogger(slogger syslogLogger) {
|
||||
s.rwMutex.Lock()
|
||||
defer s.rwMutex.Unlock()
|
||||
s.Logger.Syslog = slogger
|
||||
}
|
||||
|
||||
// GetSyslogLogger get current syslog logger.
|
||||
func (s *serverConfigV4) GetSyslogLogger() syslogLogger {
|
||||
func (s *serverConfigV6) GetSyslogLogger() syslogLogger {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Logger.Syslog
|
||||
}
|
||||
|
||||
// SetRegion set new region.
|
||||
func (s *serverConfigV4) SetRegion(region string) {
|
||||
func (s *serverConfigV6) SetRegion(region string) {
|
||||
s.rwMutex.Lock()
|
||||
defer s.rwMutex.Unlock()
|
||||
s.Region = region
|
||||
}
|
||||
|
||||
// GetRegion get current region.
|
||||
func (s serverConfigV4) GetRegion() string {
|
||||
func (s serverConfigV6) GetRegion() string {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Region
|
||||
}
|
||||
|
||||
// SetCredentials set new credentials.
|
||||
func (s *serverConfigV4) SetCredential(creds credential) {
|
||||
func (s *serverConfigV6) SetCredential(creds credential) {
|
||||
s.rwMutex.Lock()
|
||||
defer s.rwMutex.Unlock()
|
||||
s.Credential = creds
|
||||
}
|
||||
|
||||
// GetCredentials get current credentials.
|
||||
func (s serverConfigV4) GetCredential() credential {
|
||||
func (s serverConfigV6) GetCredential() credential {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Credential
|
||||
}
|
||||
|
||||
// Save config.
|
||||
func (s serverConfigV4) Save() *probe.Error {
|
||||
func (s serverConfigV6) Save() error {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
|
||||
// get config file.
|
||||
configFile, err := getConfigFile()
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
return err
|
||||
}
|
||||
|
||||
// initialize quick.
|
||||
qc, err := quick.New(&s)
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
return err
|
||||
}
|
||||
|
||||
// Save config file.
|
||||
if err := qc.Save(configFile); err != nil {
|
||||
return err.Trace()
|
||||
}
|
||||
|
||||
// Return success.
|
||||
return nil
|
||||
return qc.Save(configFile)
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/minio/go-homedir"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// configPath for custom config path only for testing purposes
|
||||
@@ -33,13 +32,13 @@ func setGlobalConfigPath(configPath string) {
|
||||
}
|
||||
|
||||
// getConfigPath get server config path
|
||||
func getConfigPath() (string, *probe.Error) {
|
||||
func getConfigPath() (string, error) {
|
||||
if customConfigPath != "" {
|
||||
return customConfigPath, nil
|
||||
}
|
||||
homeDir, e := homedir.Dir()
|
||||
if e != nil {
|
||||
return "", probe.NewError(e)
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
configPath := filepath.Join(homeDir, globalMinioConfigDir)
|
||||
return configPath, nil
|
||||
@@ -48,27 +47,24 @@ func getConfigPath() (string, *probe.Error) {
|
||||
// mustGetConfigPath must get server config path.
|
||||
func mustGetConfigPath() string {
|
||||
configPath, err := getConfigPath()
|
||||
fatalIf(err.Trace(), "Unable to get config path.", nil)
|
||||
fatalIf(err, "Unable to get config path.")
|
||||
return configPath
|
||||
}
|
||||
|
||||
// createConfigPath create server config path.
|
||||
func createConfigPath() *probe.Error {
|
||||
func createConfigPath() error {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(configPath, 0700); err != nil {
|
||||
return probe.NewError(err)
|
||||
}
|
||||
return nil
|
||||
return os.MkdirAll(configPath, 0700)
|
||||
}
|
||||
|
||||
// isConfigFileExists - returns true if config file exists.
|
||||
func isConfigFileExists() bool {
|
||||
st, e := os.Stat(mustGetConfigFile())
|
||||
st, err := os.Stat(mustGetConfigFile())
|
||||
// If file exists and is regular return true.
|
||||
if e == nil && st.Mode().IsRegular() {
|
||||
if err == nil && st.Mode().IsRegular() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -77,16 +73,16 @@ func isConfigFileExists() bool {
|
||||
// mustGetConfigFile must get server config file.
|
||||
func mustGetConfigFile() string {
|
||||
configFile, err := getConfigFile()
|
||||
fatalIf(err.Trace(), "Unable to get config file.", nil)
|
||||
fatalIf(err, "Unable to get config file.")
|
||||
|
||||
return configFile
|
||||
}
|
||||
|
||||
// getConfigFile get server config file.
|
||||
func getConfigFile() (string, *probe.Error) {
|
||||
func getConfigFile() (string, error) {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
return "", err.Trace()
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(configPath, globalMinioConfigFile), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Standard cross domain policy information located at https://s3.amazonaws.com/crossdomain.xml
|
||||
var crossDomainXML = `<?xml version="1.0"?><!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd"><cross-domain-policy><allow-access-from domain="*" secure="false" /></cross-domain-policy>`
|
||||
|
||||
// Cross domain policy implements http.Handler interface, implementing a custom ServerHTTP.
|
||||
type crossDomainPolicy struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
// A cross-domain policy file is an XML document that grants a web client, such as Adobe Flash Player
|
||||
// or Adobe Acrobat (though not necessarily limited to these), permission to handle data across domains.
|
||||
// When clients request content hosted on a particular source domain and that content make requests
|
||||
// directed towards a domain other than its own, the remote domain needs to host a cross-domain
|
||||
// policy file that grants access to the source domain, allowing the client to continue the transaction.
|
||||
func setCrossDomainPolicy(h http.Handler) http.Handler {
|
||||
return crossDomainPolicy{handler: h}
|
||||
}
|
||||
|
||||
func (c crossDomainPolicy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Look for 'crossdomain.xml' in the incoming request.
|
||||
switch r.URL.Path {
|
||||
case "/crossdomain.xml":
|
||||
// Write the standard cross domain policy xml.
|
||||
w.Write([]byte(crossDomainXML))
|
||||
// Request completed, no need to serve to other handlers.
|
||||
return
|
||||
}
|
||||
// Continue to serve the request further.
|
||||
c.handler.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test minimum function which calculates the minimal value in a list of integers
|
||||
func TestMinimum(t *testing.T) {
|
||||
type testCase struct {
|
||||
listval []int
|
||||
expected int
|
||||
pass bool
|
||||
}
|
||||
testCases := []testCase{
|
||||
{listval: []int{3, 4, 15}, expected: 3},
|
||||
{listval: []int{}, expected: math.MaxInt32},
|
||||
}
|
||||
// Validate all the test cases.
|
||||
for i, tt := range testCases {
|
||||
val := minimum(tt.listval)
|
||||
if val != tt.expected {
|
||||
t.Errorf("Test %d:, Expected %d, got %d", i+1, tt.expected, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test DamerauLevenshtein which calculates the difference distance between two words
|
||||
func TestDamerauLevenshtein(t *testing.T) {
|
||||
type testCase struct {
|
||||
word1 string
|
||||
word2 string
|
||||
distance int
|
||||
}
|
||||
testCases := []testCase{
|
||||
{word1: "", word2: "", distance: 0},
|
||||
{word1: "a", word2: "a", distance: 0},
|
||||
{word1: "a", word2: "b", distance: 1},
|
||||
{word1: "rm", word2: "tm", distance: 1},
|
||||
{word1: "version", word2: "evrsion", distance: 1},
|
||||
{word1: "version", word2: "bersio", distance: 2},
|
||||
}
|
||||
// Validate all the test cases.
|
||||
for i, tt := range testCases {
|
||||
d := DamerauLevenshteinDistance(tt.word1, tt.word2)
|
||||
if d != tt.distance {
|
||||
t.Errorf("Test %d:, Expected %d, got %d", i+1, tt.distance, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
|
||||
# usage: ./benchcmp.sh <commit-sha1> <commit-sha2>
|
||||
# Exit on any non zero return value on execution of a command.
|
||||
set -e
|
||||
|
||||
# path of benchcmp.
|
||||
benchcmp=${GOPATH}/bin/benchcmp
|
||||
|
||||
# function which runs the benchmark comparison.
|
||||
RunBenchCmp () {
|
||||
# Path for storing output of benchmark at commit 1.
|
||||
commit1Bench=/tmp/minio-$1.bench
|
||||
# Path for storing output of benchmark at commit 2.
|
||||
commit2Bench=/tmp/minio-$2.bench
|
||||
# switch to commit $1.
|
||||
git checkout $1
|
||||
# Check if the benchmark results for given commit 1 already exists.
|
||||
# Benchmarks are time/resource consuming operations, run only if the the results doesn't exist.
|
||||
if [[ ! -f $commit1Bench ]]
|
||||
then
|
||||
|
||||
echo "Running benchmarks at $1"
|
||||
go test -run=NONE -bench=. | tee $commit1Bench
|
||||
fi
|
||||
# get back to the commit from which it was started.
|
||||
git checkout -
|
||||
echo "Checking into commit $2"
|
||||
# switch to commit $2
|
||||
git checkout $2
|
||||
# Check if the benchmark results for given commit 2 already exists.
|
||||
# Benchmarks are time/resource consuming operations, run only if the the results doesn't exist.
|
||||
if [[ ! -f $commit2Bench ]]
|
||||
then
|
||||
# Running benchmarks at $2.
|
||||
echo "Running benchmarks at $2"
|
||||
go test -run=NONE -bench=. | tee $commit2Bench
|
||||
fi
|
||||
|
||||
# get back to the commit from which it was started.
|
||||
git checkout -
|
||||
# Comparing the benchmarks.
|
||||
echo "Running benchmark comparison between $1 and $2 ..."
|
||||
$benchcmp $commit1Bench $commit2Bench
|
||||
echo "Done."
|
||||
}
|
||||
|
||||
# check if 2 commit SHA's of snapshots of code for which benchmp has to be done is provided.
|
||||
if [ ! $# -eq 2 ]
|
||||
then
|
||||
# exit if commit SHA's are not provided.
|
||||
echo $#
|
||||
echo "Need Commit SHA's of 2 snapshots to be supplied to run benchmark comparison."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if benchcmp exists.
|
||||
if [[ -x "$benchcmp" ]]
|
||||
then
|
||||
RunBenchCmp $1 $2
|
||||
else
|
||||
# install benchcmp if doesnt't exist.
|
||||
echo "fetching Benchcmp..."
|
||||
go get -u golang.org/x/tools/cmd/benchcmp
|
||||
echo "Done."
|
||||
RunBenchCmp $1 $2
|
||||
fi
|
||||
Vendored
+50
@@ -0,0 +1,50 @@
|
||||
[Unit]
|
||||
Description=Minio
|
||||
Documentation=http://minio.io
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
AssertPathExists=/etc/minio/
|
||||
AssertFileIsExecutable=/usr/share/minio/bin/minio
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/usr/share/minio/
|
||||
|
||||
User=minio
|
||||
Group=minio
|
||||
|
||||
PermissionsStartOnly=true
|
||||
|
||||
EnvironmentFile=-/etc/default/minio
|
||||
ExecStartPre=/bin/bash -c "[ -n \"${MINIO_VOLUMES}\" ] || echo \"Variable MINIO_VOLUMES not set in /etc/defaults/minio\""
|
||||
ExecStartPre=/bin/bash -c "(for i in $MINIO_VOLUMES; do if [ ! -d $i ]; then echo \"Directory $i of variable MINIO_VOLUMES isn't an existing directory\" && false; exit; fi; done)"
|
||||
|
||||
ExecStartPre=-/bin/chown -R root:minio /etc/minio/
|
||||
ExecStartPre=-/bin/chmod -R 660 /etc/minio/
|
||||
ExecStartPre=-/bin/chmod -R ug+X /etc/minio/
|
||||
|
||||
ExecStart=/usr/share/minio/bin/minio \
|
||||
--config-dir "/etc/minio/" \
|
||||
server \
|
||||
$MINIO_OPTS \
|
||||
$MINIO_VOLUMES
|
||||
|
||||
StandardOutput=journal
|
||||
StandardError=inherit
|
||||
|
||||
# Specifies the maximum file descriptor number that can be opened by this process
|
||||
LimitNOFILE=65536
|
||||
|
||||
# Disable timeout logic and wait until process is stopped
|
||||
TimeoutStopSec=0
|
||||
|
||||
# SIGTERM signal is used to stop Minio
|
||||
KillSignal=SIGTERM
|
||||
|
||||
SendSIGKILL=no
|
||||
|
||||
SuccessExitStatus=0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
# Built for ${project.name}-${project.version} (${project.name})
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
# Readme
|
||||
Service script for minio service for systemd.
|
||||
|
||||
# Installation
|
||||
```
|
||||
mkdir /etc/minio/
|
||||
mkdir -p /usr/share/minio/bin/
|
||||
wget -O /usr/share/minio/bin/minio http://...
|
||||
chmod +x /usr/share/minio/bin/minio
|
||||
```
|
||||
|
||||
Create minio user.
|
||||
```
|
||||
useradd minio
|
||||
```
|
||||
|
||||
Create default configuration. Don't forget to update MINIO_VOLUMES with the correct path(s).
|
||||
```
|
||||
cat <<EOT >> /etc/default/minio
|
||||
MINIO_OPTS="--address :9000"
|
||||
MINIO_VOLUMES="/tmp/minio/"
|
||||
EOT
|
||||
```
|
||||
|
||||
# Systemctl
|
||||
|
||||
Put minio.service in /etc/systemd/system/
|
||||
```
|
||||
curl https://raw.githubusercontent.com/minio/minio/master/dist/linux-systemd/minio.service > /etc/systemd/system/
|
||||
```
|
||||
|
||||
Enable startup on boot
|
||||
```
|
||||
systemctl enable minio.service
|
||||
```
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# use "make dockerimage" to build
|
||||
FROM alpine:3.3
|
||||
RUN apk add --no-cache ca-certificates bash
|
||||
ADD minio.dockerimage /minio
|
||||
ADD export /export
|
||||
EXPOSE 9000
|
||||
COPY start.sh /start.sh
|
||||
ENTRYPOINT ["/start.sh"]
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
## Fix this check when we arrive at XL.
|
||||
if [ -z "$1" ]; then
|
||||
echo "Invalid arguments"
|
||||
echo "Usage: <export_dir>"
|
||||
exit 1
|
||||
else
|
||||
/bin/mkdir -p "$1" && /minio server "$1"
|
||||
fi
|
||||
+21
-10
@@ -1,28 +1,39 @@
|
||||
### Run Minio docker image
|
||||
# Minio Docker Quickstart Guide [](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Test Minio Docker Container
|
||||
|
||||
## 1. Test Minio Docker Container
|
||||
Minio generates new access and secret keys each time you run this command. Container state is lost after you end this session. This mode is only intended for testing purpose.
|
||||
```bash
|
||||
docker run -p 9000:9000 minio/minio server /export
|
||||
|
||||
```sh
|
||||
|
||||
docker run -p 9000:9000 minio/minio /export
|
||||
|
||||
```
|
||||
|
||||
## Run Minio Docker Container
|
||||
## 2. Run Minio Docker Container
|
||||
|
||||
Minio container requires a persistent volume to store configuration and application data. Following command maps local persistent directories from the host OS to virtual config `~/.minio` and export `/export` directories.
|
||||
|
||||
```bash
|
||||
```sh
|
||||
|
||||
docker run -p 9000:9000 --name minio1 \
|
||||
-v /mnt/export/minio1:/export \
|
||||
-v /mnt/config/minio1:/root/.minio \
|
||||
minio/minio server /export
|
||||
minio/minio /export
|
||||
|
||||
```
|
||||
|
||||
## Custom Access and Secret Keys
|
||||
## 3. Custom Access and Secret Keys
|
||||
|
||||
To override Minio's auto-generated keys, you may pass secret and access keys explicitly as environment variables. Minio server also allows regular strings as access and secret keys.
|
||||
```bash
|
||||
|
||||
```sh
|
||||
|
||||
docker run -p 9000:9000 --name minio1 \
|
||||
-e "MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" \
|
||||
-e "MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \
|
||||
-v /mnt/export/minio1:/export \
|
||||
-v /mnt/config/minio1:/root/.minio \
|
||||
minio/minio server /export
|
||||
minio/minio /export
|
||||
|
||||
```
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
# Minio FreeBSD Quickstart Guide [](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
### Minio with ZFS backend - FreeBSD
|
||||
|
||||
This example assumes that you have a FreeBSD 10.x running
|
||||
|
||||
#### Step 1.
|
||||
|
||||
As root on the FreeBSD edit `/etc/rc.conf`
|
||||
|
||||
```sh
|
||||
|
||||
zfs_enable="YES"
|
||||
|
||||
```
|
||||
|
||||
Start ZFS service
|
||||
|
||||
```sh
|
||||
|
||||
# service zfs start
|
||||
|
||||
```
|
||||
|
||||
```sh
|
||||
|
||||
# dd if=/dev/zero of=/zfs bs=1M count=4000
|
||||
|
||||
```
|
||||
|
||||
Configure a loopback device on the `/zfs` file.
|
||||
|
||||
```sh
|
||||
|
||||
# mdconfig -a -t vnode -f /zfs
|
||||
|
||||
```
|
||||
|
||||
Create zfs pool
|
||||
|
||||
```sh
|
||||
|
||||
# zpool create minio-example /dev/md0
|
||||
|
||||
```
|
||||
|
||||
```sh
|
||||
|
||||
# df /minio-example
|
||||
Filesystem 512-blocks Used Avail Capacity Mounted on
|
||||
minio-example 7872440 38 7872402 0% /minio-example
|
||||
|
||||
```
|
||||
|
||||
Verify if it is writable
|
||||
|
||||
```sh
|
||||
|
||||
# touch /minio-example/testfile
|
||||
# ls -l /minio-example/testfile
|
||||
-rw-r--r-- 1 root wheel 0 Apr 26 00:51 /minio-example/testfile
|
||||
|
||||
```
|
||||
|
||||
Now you have successfully created a ZFS pool for further reading please refer to [ZFS Quickstart Guide](https://www.freebsd.org/doc/handbook/zfs-quickstart.html)
|
||||
|
||||
However, this pool is not taking advantage of any ZFS features, so let's create a ZFS filesytem on this pool with compression enabled. ZFS supports many compression algorithms: lzjb, gzip, zle, lz4. LZ4 is often the most performant algorithm in terms of compression of data versus system overhead.
|
||||
|
||||
|
||||
```sh
|
||||
|
||||
# zfs create minio-example/compressed-objects
|
||||
# zfs set compression=lz4 minio-example/compressed-objects
|
||||
|
||||
```
|
||||
|
||||
To keep monitoring your pool use
|
||||
|
||||
```sh
|
||||
|
||||
# zpool status
|
||||
pool: minio-example
|
||||
state: ONLINE
|
||||
scan: none requested
|
||||
config:
|
||||
|
||||
NAME STATE READ WRITE CKSUM
|
||||
minio-example ONLINE 0 0 0
|
||||
md0 ONLINE 0 0 0
|
||||
|
||||
errors: No known data errors
|
||||
|
||||
```
|
||||
|
||||
#### Step 2.
|
||||
|
||||
Now start minio server on the ``/minio-example/compressed-objects``, change the permissions such that this directory is accessibly by a normal user
|
||||
|
||||
```sh
|
||||
|
||||
# chown -R minio-user:minio-user /minio-example/compressed-objects
|
||||
|
||||
```
|
||||
|
||||
Now login as ``minio-user`` and start minio server.
|
||||
|
||||
```sh
|
||||
|
||||
$ curl https://dl.minio.io/server/minio/release/freebsd-amd64/minio > minio
|
||||
$ chmod 755 minio
|
||||
$ ./minio server /minio-example/compressed-objects
|
||||
|
||||
|
||||
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
Region: us-east-1
|
||||
|
||||
Browser Access:
|
||||
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
|
||||
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
|
||||
$ mc config host add myminio http://localhost:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
|
||||
Object API (Amazon S3 compatible):
|
||||
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
|
||||
Java: https://docs.minio.io/docs/java-client-quickstart-guide
|
||||
Python: https://docs.minio.io/docs/python-client-quickstart-guide
|
||||
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
|
||||
|
||||
```
|
||||
|
||||
Point your browser to http://localhost:9000 and login with the credentials displayed on the command line.
|
||||
|
||||
Now you have a S3 compatible server running on top of your ZFS backend which transparently provides disk level compression for your uploaded objects.
|
||||
|
||||
Thanks for using Minio, awaiting feedback :-)
|
||||
|
||||
|
||||
#### Building Minio Server From Source
|
||||
|
||||
It is possible to build the minio server from source on FreeBSD. To do this we will used the golang distribution provided by the FreeBSD pkg infrastructure.
|
||||
|
||||
First we will need to install golang as well as GNU make:
|
||||
|
||||
```sh
|
||||
|
||||
$ sudo pkg install go gmake
|
||||
Updating FreeBSD repository catalogue..
|
||||
FreeBSD repository is up-to-date.
|
||||
All repositories are up-to-date.
|
||||
The following 1 package(s) will be affected (of 0 checked):
|
||||
|
||||
New packages to be INSTALLED:
|
||||
go: 1.6.2,1
|
||||
gmake: 4.1_2
|
||||
|
||||
The process will require 261 MiB more space.
|
||||
40 MiB to be downloaded.
|
||||
|
||||
Proceed with this action? [y/N]: y
|
||||
Fetching go-1.6.2,1.txz: 100% 40 MiB 10.4MB/s 00:04
|
||||
Fetching gmake-4.1_2.txz: 100% 363 KiB 372.2kB/s 00:01
|
||||
Checking integrity... done (0 conflicting)
|
||||
[1/2] Installing go-1.6.2,1...
|
||||
[1/2] Extracting go-1.6.2,1: 100%
|
||||
[2/2] Installing gmake-4.1_2...
|
||||
[2/2] Extracting gmake-4.1_2: 100%
|
||||
```
|
||||
|
||||
Next we need to configure our environment for golang. Insert the following lines into your ~/.profile file, and be sure to source the file before proceeding to the next step:
|
||||
|
||||
|
||||
```sh
|
||||
|
||||
GOPATH=$HOME/golang; export GOPATH
|
||||
GOROOT=/usr/local/go/; export GOROOT
|
||||
|
||||
```
|
||||
|
||||
Now we can proceed with the normal build process of minio server as found [here](https://github.com/nomadlogic/minio/blob/master/CONTRIBUTING.md). The only caveat is we need to specify gmake (GNU make) when building minio server as the current Makefile is not BSD make compatible:
|
||||
|
||||
```sh
|
||||
|
||||
$ mkdir -p $GOPATH/src/github.com/minio
|
||||
$ cd $GOPATH/src/github.com/minio
|
||||
$ git clone <paste saved URL for personal forked minio repo>
|
||||
$ cd minio
|
||||
$ gmake
|
||||
|
||||
```
|
||||
|
||||
From here you can start the server as you would with a precompiled minio server build.
|
||||
@@ -0,0 +1,6 @@
|
||||
## Backends
|
||||
|
||||
Minio currently implements two types of backends namely.
|
||||
|
||||
- Filesystem layer (fs).
|
||||
- ErasureCode layer (XL).
|
||||
@@ -0,0 +1,25 @@
|
||||
### Backend format `fs.json`
|
||||
|
||||
```go
|
||||
// objectPartInfo Info of each part kept in the multipart metadata
|
||||
// file after CompleteMultipartUpload() is called.
|
||||
type objectPartInfo struct {
|
||||
Number int `json:"number"`
|
||||
Name string `json:"name"`
|
||||
ETag string `json:"etag"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// A fsMetaV1 represents a metadata header mapping keys to sets of values.
|
||||
type fsMetaV1 struct {
|
||||
Version string `json:"version"`
|
||||
Format string `json:"format"`
|
||||
Minio struct {
|
||||
Release string `json:"release"`
|
||||
} `json:"minio"`
|
||||
// Metadata map for current object `fs.json`.
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
Parts []objectPartInfo `json:"parts,omitempty"`
|
||||
}
|
||||
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"format": "fs",
|
||||
"version": "1"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"format": "fs",
|
||||
"minio": {
|
||||
"release": "DEVELOPMENT.GOGET"
|
||||
},
|
||||
"meta": {
|
||||
"content-type": "binary/octet-stream",
|
||||
"content-encoding": "gzip"
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"number": 1,
|
||||
"name": "part.1",
|
||||
"size": 29,
|
||||
"eTag": "",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": "1",
|
||||
"format": "fs",
|
||||
"uploadIds": [
|
||||
{
|
||||
"uploadID": "id",
|
||||
"startTime": "time",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
### Backend format `xl.json`
|
||||
|
||||
```go
|
||||
// objectPartInfo Info of each part kept in the multipart metadata
|
||||
// file after CompleteMultipartUpload() is called.
|
||||
type objectPartInfo struct {
|
||||
Number int `json:"number"`
|
||||
Name string `json:"name"`
|
||||
ETag string `json:"etag"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// checkSumInfo - carries checksums of individual scattered parts per disk.
|
||||
type checkSumInfo struct {
|
||||
Name string `json:"name"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
// erasureInfo - carries erasure coding related information, block
|
||||
// distribution and checksums.
|
||||
type erasureInfo struct {
|
||||
Algorithm string `json:"algorithm"`
|
||||
DataBlocks int `json:"data"`
|
||||
ParityBlocks int `json:"parity"`
|
||||
BlockSize int64 `json:"blockSize"`
|
||||
Index int `json:"index"`
|
||||
Distribution []int `json:"distribution"`
|
||||
Checksum []checkSumInfo `json:"checksum,omitempty"`
|
||||
}
|
||||
|
||||
// statInfo - carries stat information of the object.
|
||||
type statInfo struct {
|
||||
Size int64 `json:"size"` // Size of the object `xl.json`.
|
||||
ModTime time.Time `json:"modTime"` // ModTime of the object `xl.json`.
|
||||
}
|
||||
|
||||
// A xlMetaV1 represents `xl.json` metadata header.
|
||||
type xlMetaV1 struct {
|
||||
Version string `json:"version"` // Version of the current `xl.json`.
|
||||
Format string `json:"format"` // Format of the current `xl.json`.
|
||||
Stat statInfo `json:"stat"` // Stat of the current object `xl.json`.
|
||||
// Erasure coded info for the current object `xl.json`.
|
||||
Erasure erasureInfo `json:"erasure"`
|
||||
// Minio release tag for current object `xl.json`.
|
||||
Minio struct {
|
||||
Release string `json:"release"`
|
||||
} `json:"minio"`
|
||||
// Metadata map for current object `xl.json`.
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
// Captures all the individual object `xl.json`.
|
||||
Parts []objectPartInfo `json:"parts,omitempty"`
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"xl": {
|
||||
"jbod": [
|
||||
"8aa2b1bc-0e5a-49e0-8221-05228336b040",
|
||||
"3467a69b-0266-478a-9e10-e819447e4545",
|
||||
"d4a4505b-4e4f-4864-befd-4f36adb0bc66",
|
||||
"592b6583-ca26-47af-b991-ba6d097e34e8",
|
||||
"c7ef69f0-dbf5-4c0e-b167-d30a441bad7e",
|
||||
"f0b36ea3-fe96-4f2b-bced-22c7f33e0e0c",
|
||||
"b83abf39-e39d-4e7b-8e16-6f9953455a48",
|
||||
"7d63dfc9-5441-4243-bd36-de8db0691982",
|
||||
"c1bbffc5-81f9-4251-9398-33a959b3ce37",
|
||||
"64408f94-26e0-4277-9593-2d703f4d5a91"
|
||||
],
|
||||
"disk": "8aa2b1bc-0e5a-49e0-8221-05228336b040",
|
||||
"version": "1"
|
||||
},
|
||||
"format": "xl",
|
||||
"version": "1"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"format": "xl",
|
||||
"uploadIds": [
|
||||
{
|
||||
"uploadID": "id",
|
||||
"startTime": "time",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"parts": [
|
||||
{
|
||||
"number": 1,
|
||||
"size": 5242880,
|
||||
"etag": "3565c6e741e69a007a5ac7db893a62b5",
|
||||
"name": "part.1"
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"size": 5242880,
|
||||
"etag": "d416712335c280ab1e39498552937764",
|
||||
"name": "part.2"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"md5Sum": "97586a5290d4f5a41328062d6a7da593-3",
|
||||
"content-type": "application\/octet-stream",
|
||||
"content-encoding": "gzip"
|
||||
},
|
||||
"minio": {
|
||||
"release": "DEVELOPMENT.GOGET"
|
||||
},
|
||||
"erasure": {
|
||||
"algorithm": "klauspost/reedsolomon/vandermonde",
|
||||
"index": 2,
|
||||
"distribution": [ 1, 3, 4, 2, 5, 8, 7, 6, 9 ],
|
||||
"blockSize": 4194304,
|
||||
"parity": 5,
|
||||
"data": 5,
|
||||
"checksum": [
|
||||
{
|
||||
"name": "part.1",
|
||||
"algorithm": "blake2b",
|
||||
"hash": "173c2cda7fb9e2798f91ba10135b65475fd2b97684355b328941d1c470a37549502672b2b4630c8268d5e5f79c8eb2fe433a25c6368f31d75b2c0504e3104c0e",
|
||||
},
|
||||
{
|
||||
"name": "part.2",
|
||||
"algorithm": "blake2b",
|
||||
"hash": "14f9ba0006b2db7cd171507e79032ad3a34eab7df02b6564c487614b6d1e6613343a4f56074aa3473ac9c3b26e00cbbeae937d478cca4cfb138e72838ebb0826",
|
||||
},
|
||||
],
|
||||
},
|
||||
"stat": {
|
||||
"modTime": "2016-05-24T00:09:40.122390255Z",
|
||||
"size": 14824084
|
||||
},
|
||||
"format": "xl",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -19,21 +19,22 @@ minio server <testdir>
|
||||
|
||||
JSON RPC namespace is `Web`.
|
||||
|
||||
#### Auth Operations
|
||||
#### Server operations.
|
||||
|
||||
* ServerInfo - fetches current server information, includes memory statistics, minio binary
|
||||
version, golang runtime version and more.
|
||||
|
||||
#### Auth operations
|
||||
|
||||
* Login - waits for 'username, password' and on success replies a new Json Web Token (JWT).
|
||||
* ResetToken - resets token, requires password and token.
|
||||
* Logout - currently a dummy operation.
|
||||
* SetAuth - change access credentials with new 'username, password'.
|
||||
* GetAuth - fetch the current auth from the server.
|
||||
|
||||
#### Bucket/Object Operations.
|
||||
#### Bucket/Object operations.
|
||||
|
||||
* ListBuckets - lists buckets, requires a valid token.
|
||||
* ListObjects - lists objects, requires a valid token.
|
||||
* MakeBucket - make a new bucket, requires a valid token.
|
||||
* GetObjectURL - generates a URL for download access, requires a valid token.
|
||||
(generated URL is valid for 1hr)
|
||||
* PutObjectURL - generates a URL for upload access, requies a valid token.
|
||||
(generated URL is valid for 1hr)
|
||||
|
||||
#### Server Operations.
|
||||
* DiskInfo - get backend disk statistics.
|
||||
* RemoveObject - removes an object from a bucket, requires a valid token.
|
||||
* Upload - uploads a new object from the browser, requires a valid token.
|
||||
* Download - downloads an object from a bucket, requires a valid token.
|
||||
@@ -0,0 +1,39 @@
|
||||
## Object caching
|
||||
|
||||
Object caching by turned on by default with following settings
|
||||
|
||||
- Default cache size 8GB, can be changed from environment variable
|
||||
``MINIO_CACHE_SIZE`` supports both SI and ISO IEC standard forms
|
||||
for input size parameters.
|
||||
|
||||
- Default expiration of entries is 72 hours, can be changed from
|
||||
environment variable ``MINIO_CACHE_EXPIRY`` supportings Go
|
||||
``time.Duration`` with valid units "ns", "us" (or "µs"),
|
||||
"ms", "s", "m", "h".
|
||||
|
||||
- Default expiry interval is 1/4th of the expiration hours, so
|
||||
expiration sweep happens across the cache every 1/4th the time
|
||||
duration of the set entry expiration duration.
|
||||
|
||||
### Tricks
|
||||
|
||||
Setting MINIO_CACHE_SIZE=0 will turn off caching entirely.
|
||||
Setting MINIO_CACHE_EXPIRY=0s will turn off cache garbage collections,
|
||||
all cached objects will never expire.
|
||||
|
||||
### Behavior
|
||||
|
||||
Caching happens for both GET and PUT.
|
||||
|
||||
- GET caches new objects for entries not found in cache,
|
||||
otherwise serves from the cache.
|
||||
|
||||
- PUT/POST caches all successfully uploaded objects.
|
||||
|
||||
NOTE: Cache is not populated if there are any errors
|
||||
while reading from the disk.
|
||||
|
||||
Expiration happens automatically based on the configured
|
||||
interval as explained above, frequently accessed objects
|
||||
stay alive for significantly longer time due to the fact
|
||||
that expiration time is reset for every cache hit.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Minio Erasure Code Quickstart Guide [](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
Minio protects data against hardware failures and silent data corruption using erasure code and checksums. You may lose half the number (N/2) of drives and still be able to recover the data.
|
||||
|
||||
## 1. Prerequisites:
|
||||
|
||||
Install Minio - [Minio Quickstart Guide](https://docs.minio.io/docs/minio)
|
||||
|
||||
## What is Erasure Code?
|
||||
|
||||
Erasure code is a mathematical algorithm to reconstruct missing or corrupted data. Minio uses Reed-Solomon code to shard objects into N/2 data and N/2 parity blocks. This means that in a 12 drive setup, an object is sharded across as 6 data and 6 parity blocks. You can lose as many as 6 drives (be it parity or data) and still reconstruct the data reliably from the remaining drives.
|
||||
|
||||
## Why is Erasure Code useful?
|
||||
|
||||
Erasure code protects data from multiple drives failure unlike RAID or replication. For eg RAID6 can protect against 2 drive failure whereas in Minio erasure code you can lose as many as half number of drives and still the data remains safe. Further Minio's erasure code is at object level and can heal one object at a time. For RAID, healing can only be performed at volume level which translates into huge down time. As Minio encodes each object individually with a high parity count. Storage servers once deployed should not require drive replacement or healing for the lifetime of the server. Minio's erasure coded backend is designed for operational efficiency and takes full advantage of hardware acceleration whenever available.
|
||||
|
||||

|
||||
|
||||
## What is Bit Rot protection?
|
||||
|
||||
Bit Rot also known as Data Rot or Silent Data Corruption is a serious data loss issue faced by disk drives today. Data on the drive may silently get corrupted without signalling an error has occurred. This makes Bit Rot more dangerous than permanent hard drive failure.
|
||||
|
||||
Minio's erasure coded backend uses high speed [BLAKE2](https://blog.minio.io/accelerating-blake2b-by-4x-using-simd-in-go-assembly-33ef16c8a56b#.jrp1fdwer) hash based checksums to protect against Bit Rot.
|
||||
|
||||
## Deployment Scenarios
|
||||
|
||||
Minio server runs on a variety of hardware, operating systems and virtual/container environments.
|
||||
|
||||
Minio erasure code backend is limited by design to a minimum of 6 drives and a maximum of 16 drives. The hard limit of 16 drives comes from operational experience. Failure domain becomes too large beyond 16 drives. If you need to scale beyond 16 drives, you may run multiple instances of Minio server on different ports.
|
||||
|
||||
#### Reference Physical Hardware:
|
||||
|
||||
* [SMC 5018A-AR12L (Intel Atom)](http://www.supermicro.com/products/system/1U/5018/SSG-5018A-AR12L.cfm?parts=SHOW) - SMC 1U SoC Atom C2750 platform with 12x 3.5” drive bays
|
||||
* [Quanta Grid D51B-2U (OCP Compliant) ](http://www.qct.io/Product/Servers/Rackmount-Servers/2U/QuantaGrid-D51B-2U-p256c77c70c83c118)- Quanta 2U DP E5-2600v3 platform with 12x 3.5” drive bays
|
||||
* [Cisco UCS C240 M4 Rack Server](http://www.cisco.com/c/en/us/products/servers-unified-computing/ucs-c240-m4-rack-server/index.html) - Cisco 2U DP E5-2600v3 platform with 12x 3.5” drive bays
|
||||
* [Intel® Server System R2312WTTYSR](http://ark.intel.com/products/88286) - Intel 2U DP E5-2600v3 platform with 12x 3.5” drive bays
|
||||
|
||||
#### Reference Cloud Hosting Providers:
|
||||
|
||||
* [Packet](https://www.packet.net): Packet is the baremetal cloud provider.
|
||||
* [Digital Ocean](https://www.digitalocean.com): Deploy an SSD cloud server in 55 seconds.
|
||||
* [OVH](https://www.ovh.com/us): Build your own infrastructure with OVH public cloud.
|
||||
* [Onlinetech](http://www.onlinetech.com): Secure, compliant enterprise cloud.
|
||||
* [SSD Nodes](https://www.ssdnodes.com): Simple, high performance cloud provider with truly personalized support.
|
||||
|
||||
## 2. Run Minio Server with Erasure Code.
|
||||
|
||||
Example: Start Minio server in a 12 drives setup.
|
||||
|
||||
```sh
|
||||
|
||||
$ minio server /mnt/export1/backend /mnt/export2/backend /mnt/export3/backend /mnt/export4/backend /mnt/export5/backend /mnt/export6/backend /mnt/export7/backend /mnt/export8/backend /mnt/export9/backend /mnt/export10/backend /mnt/export11/backend /mnt/export12/backend
|
||||
|
||||
```
|
||||
|
||||
## 3. Test your setup
|
||||
|
||||
You may unplug drives randomly and continue to perform I/O on the system.
|
||||
@@ -0,0 +1,80 @@
|
||||
# How to run multiple Minio server instances on single machine. [](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||

|
||||
|
||||
|
||||
In this document we will illustrate how to set up multiple Minio server instances on single machine. These Minio servers are running on their own port, data directory & configuration directory.
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
* Download Minio server from [here](https://docs.minio.io/docs/minio)
|
||||
* Download & Install mc from [here](https://docs.minio.io/docs/minio-client-quickstart-guide)
|
||||
* Ports should be available for Minio server's use
|
||||
|
||||
## 2. Install and Configure Minio Server
|
||||
|
||||
Minio server is running on port 9002, 9003, 9004 with associated data directory and configuration file directory.
|
||||
|
||||
**Minio server on port ``9002``**
|
||||
|
||||
```sh
|
||||
$ ./minio -C /home/minio/minio1/.minio server --address 192.168.1.11:9002 minio1/data1/
|
||||
|
||||
Endpoint: http://192.168.1.11:9002
|
||||
AccessKey: XTW9SWKRWYKWE9M9K9RG
|
||||
SecretKey: pZehbS5UNrA9BAhYHnWC/QVvQ7vGVge48WGHzG9t
|
||||
Region: us-east-1
|
||||
```
|
||||
|
||||
**Minio server on port ``9003``**
|
||||
|
||||
```sh
|
||||
$ ./minio -C /home/minio/minio2/.minio server --address 192.168.1.11:9003 minio1/data2/
|
||||
|
||||
Endpoint: http://192.168.1.11:9003
|
||||
AccessKey: UTD2WWPJOK754KMZKHWF
|
||||
SecretKey: DbikDIY4+wItcexJa4nyrwQC0V2r7kLsK5SsRgHb
|
||||
Region: us-east-1
|
||||
```
|
||||
|
||||
**Minio server on port ``9004``**
|
||||
|
||||
```sh
|
||||
$ ./minio -C /home/minio/minio3/.minio server --address 192.168.1.11:9004 minio1/data3/
|
||||
|
||||
Endpoint: http://192.168.1.11:9004
|
||||
AccessKey: KXLOJ908VEJ2K9RGUFHQ
|
||||
SecretKey: LpkeePMtEWAa6payiCovfrNKiFHhABsJhMwGynF8
|
||||
Region: us-east-1
|
||||
```
|
||||
|
||||
This is how directory structure will look like for ``minio1``, replace it with your local setup.
|
||||
|
||||
```sh
|
||||
$ tree -la minio1/
|
||||
minio1/
|
||||
├── data1
|
||||
└── .minio
|
||||
├── certs
|
||||
├── config.json
|
||||
└── config.json.old
|
||||
|
||||
3 directories, 2 files
|
||||
|
||||
```
|
||||
**Testing it all**
|
||||
|
||||
Using terminal comamnd ``netstat`` we can see ``Minio Server`` is running on different ports on same machine.
|
||||
|
||||
```sh
|
||||
$ netstat -ntlp | grep minio
|
||||
tcp 0 0 192.168.1.11:9002 0.0.0.0:* LISTEN 29573/minio
|
||||
tcp 0 0 192.168.1.11:9003 0.0.0.0:* LISTEN 29597/minio
|
||||
tcp 0 0 192.168.1.11:9004 0.0.0.0:* LISTEN 29631/minio
|
||||
```
|
||||
|
||||
|
||||
# Explore Further
|
||||
* [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide)
|
||||
* [Minio Client Complete Guide](https://docs.minio.io/docs/minio-client-complete-guide)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# Minio Server configuration files Guide [](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
In this document we will walk through the configuration files of Minio Server.
|
||||
|
||||
## Minio Server configuration directory
|
||||
Minio Server configurations are stored in file name ``.minio``. It's a hidden file which resides on user's home directory.
|
||||
|
||||
**This how the structure of the directory looks like:**
|
||||
|
||||
```sh
|
||||
$ tree ~/.minio/
|
||||
/Users/supernova/.minio/
|
||||
├── certs
|
||||
├── config.json
|
||||
└── config.json.old
|
||||
|
||||
1 directory, 2 files
|
||||
```
|
||||
### Files and directories.
|
||||
|
||||
##### ``certs`` directory
|
||||
``certs`` directory stores key & cert information, which are needed to run Minio in ``HTTPS``. You can read more on running Minio with ``HTTPS`` with Let's Encrypt cert with Concert [here](https://docs.minio.io/docs/generate-let-s-encypt-certificate-using-concert-for-minio)
|
||||
|
||||
##### ``config.json``
|
||||
config.json is the configuration file for Minio, it gets generated after you install and start Minio.
|
||||
|
||||
```sh
|
||||
|
||||
$ cat config.json
|
||||
{
|
||||
"version": "6",
|
||||
"credential": {
|
||||
"accessKey": "YI7S1CKXB76RGOGT6R8W",
|
||||
"secretKey": "FJ9PWUVNXGPfiI72WMRFepN3LsFgW3MjsxSALroV"
|
||||
},
|
||||
"region": "us-east-1",
|
||||
"logger": {
|
||||
"console": {
|
||||
"enable": true,
|
||||
"level": "fatal"
|
||||
},
|
||||
"file": {
|
||||
"enable": false,
|
||||
"fileName": "",
|
||||
"level": "error"
|
||||
},
|
||||
"syslog": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"level": "debug"
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"amqp": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"exchange": "",
|
||||
"routingKey": "",
|
||||
"exchangeType": "",
|
||||
"mandatory": false,
|
||||
"immediate": false,
|
||||
"durable": false,
|
||||
"internal": false,
|
||||
"noWait": false,
|
||||
"autoDeleted": false
|
||||
}
|
||||
},
|
||||
"elasticsearch": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"index": ""
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"password": "",
|
||||
"key": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
``version`` : Represents `version` number of current configuration file.
|
||||
|
||||
``credential`` : Represents authentication credentials for the server, value is automatically generated upon first server start.
|
||||
|
||||
``region`` : Represents deployment region for the server, value defaults to `us-east-1`.
|
||||
|
||||
``logger `` : Represents various logging types supported for server error logs, console logger is enabled by default.
|
||||
|
||||
``notify``: Represents various notification types supported. These notification types should be configured prior to using bucket
|
||||
|
||||
|
||||
##### ``config.json.old``
|
||||
This file keeps previous config file version details.
|
||||
|
||||
## Explore Further
|
||||
* [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,116 @@
|
||||
# How to use ``mc mirror`` to setup replication between two sites running Minio. [](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
In this document we will illustrate how to set up replication between two Minio servers, `minio1` and `minio2` running on ``192.168.1.11`` and ``192.168.1.12`` respectively. We will mirror the data directory on `minio1` to the bucket on `minio2`.
|
||||
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
* Download Minio server from [here](https://docs.minio.io/docs/minio)
|
||||
* Download & Install mc from [here](https://docs.minio.io/docs/minio-client-quickstart-guide)
|
||||
* Familiarity with [``mc mirror``](https://docs.minio.io/docs/minio-client-complete-guide#mirror)
|
||||
|
||||
## 2. Install and Configure Minio Server
|
||||
|
||||
### Setup minio1
|
||||
|
||||
```sh
|
||||
$ ./minio server minio1-data/
|
||||
|
||||
Endpoint: http://127.0.0.1:9000 http://192.168.1.11:9000
|
||||
AccessKey: MURIVYBYNPTYE7O8I779
|
||||
SecretKey: lVbZmz4CvGkBl7JKw5icuL7RCcSvpBJTkAJTFQwz
|
||||
Region: us-east-1
|
||||
```
|
||||
**mc alias**
|
||||
|
||||
Alias is a short name to your cloud storage service for ``Minio client``. End-point, access and secret keys are supplied by your cloud storage provider. API signature is an optional argument. By default, it is set to ``S3v4``.
|
||||
|
||||
```sh
|
||||
$ ./mc config host add minio1 http://192.168.1.11:9000 MURIVYBYNPTYE7O8I779 lVbZmz4CvGkBl7JKw5icuL7RCcSvpBJTkAJTFQwz
|
||||
```
|
||||
|
||||
**Create buckets and add objects**
|
||||
|
||||
We have created few buckets using [``mc mb``](https://docs.minio.io/docs/minio-client-complete-guide#mb) and added objects to it using [``mc cp``](https://docs.minio.io/docs/minio-client-complete-guide#cp) Minio client commands.
|
||||
|
||||
```sh
|
||||
$ ./mc mb minio1/mybucket
|
||||
$ ./mc cp myfile.txt minio1/bucket1
|
||||
$ ./mc ls minio1
|
||||
[2016-07-31 10:26:55 PDT] 0B bucket1/
|
||||
[2016-07-31 09:36:17 PDT] 0B bucket2/
|
||||
[2016-07-31 09:38:08 PDT] 0B bucket3/
|
||||
```
|
||||
|
||||
### Setup minio2
|
||||
|
||||
```sh
|
||||
$ ./minio server minio2-data/
|
||||
|
||||
Endpoint: http://127.0.0.1:9000 http://192.168.1.12:9000
|
||||
AccessKey: YRDRWWQLEWS9OBJ31GZ2
|
||||
SecretKey: y2sSWzx5ytwvkELcxOuSaQ8n3doNqoIilRpb5Kjj
|
||||
Region: us-east-1
|
||||
```
|
||||
|
||||
```sh
|
||||
$ ./mc config host add minio2 http://192.168.1.12:9000 YRDRWWQLEWS9OBJ31GZ2 y2sSWzx5ytwvkELcxOuSaQ8n3doNqoIilRpb5Kjj
|
||||
```
|
||||
|
||||
**Create bucket**
|
||||
|
||||
We are creating destination bucket ``mbucket`` on ``minio2`` and adding ``minio2`` alias. The bucket ``mbucket`` will be used to mirror data directory of ``minio1``.
|
||||
|
||||
```sh
|
||||
$ ./mc mb minio1/mbucket
|
||||
```
|
||||
|
||||
```sh
|
||||
$ ./mc config host add minio2 http://192.168.1.12:9000 YRDRWWQLEWS9OBJ31GZ2 y2sSWzx5ytwvkELcxOuSaQ8n3doNqoIilRpb5Kjj
|
||||
|
||||
```
|
||||
|
||||
## 4. Setup crontab
|
||||
Cron is a Unix/Linux system utility by which you can schedule a task process for particular duration, we have tested this setup on Ubuntu Linux.
|
||||
|
||||
|
||||
### Script
|
||||
|
||||
Add crontab configuration on `minio1` providing path of data directory, ``minio1-data``.
|
||||
|
||||
``--force`` option with ``mc mirror`` overwrites the destination contents, this would keep your contents in sync.
|
||||
|
||||
```sh
|
||||
|
||||
#!/bin/bash
|
||||
datadir="/home/minio/minio1-data/"
|
||||
minio2="minio2/mbucket"
|
||||
MC_PATH="/home/minio/mc"
|
||||
$MC_PATH --quiet mirror --force $minio1 $minio2
|
||||
|
||||
```
|
||||
|
||||
Set executable permissions on the script before adding a cron entry.
|
||||
|
||||
```sh
|
||||
|
||||
$ chmod 755 /home/minio/minio.sh
|
||||
```
|
||||
|
||||
Set a new cron entry to run ``minio.sh`` script once every 30mins.
|
||||
|
||||
```sh
|
||||
|
||||
$ crontab -e
|
||||
*/30 * * * * /home/minio/minio.sh
|
||||
```
|
||||
|
||||
Note: We are going to introduce continuous replication feature in `mc mirror` which will enable the sites to be in sync without having the need to setup cron job.
|
||||
|
||||
# Explore Further
|
||||
* [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide)
|
||||
* [Minio Client Complete Guide](https://docs.minio.io/docs/minio-client-complete-guide)
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"hash"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
)
|
||||
|
||||
// erasureCreateFile - writes an entire stream by erasure coding to
|
||||
// all the disks, writes also calculate individual block's checksum
|
||||
// for future bit-rot protection.
|
||||
func erasureCreateFile(disks []StorageAPI, volume, path string, reader io.Reader, blockSize int64, dataBlocks int, parityBlocks int, algo string, writeQuorum int) (bytesWritten int64, checkSums []string, err error) {
|
||||
// Allocated blockSized buffer for reading.
|
||||
buf := make([]byte, blockSize)
|
||||
|
||||
hashWriters := newHashWriters(len(disks), algo)
|
||||
|
||||
// Read until io.EOF, erasure codes data and writes to all disks.
|
||||
for {
|
||||
var blocks [][]byte
|
||||
n, rErr := io.ReadFull(reader, buf)
|
||||
// FIXME: this is a bug in Golang, n == 0 and err ==
|
||||
// io.ErrUnexpectedEOF for io.ReadFull function.
|
||||
if n == 0 && rErr == io.ErrUnexpectedEOF {
|
||||
return 0, nil, rErr
|
||||
}
|
||||
if rErr == io.EOF {
|
||||
// We have reached EOF on the first byte read, io.Reader
|
||||
// must be 0bytes, we don't need to erasure code
|
||||
// data. Will create a 0byte file instead.
|
||||
if bytesWritten == 0 {
|
||||
blocks = make([][]byte, len(disks))
|
||||
rErr = appendFile(disks, volume, path, blocks, hashWriters, writeQuorum)
|
||||
if rErr != nil {
|
||||
return 0, nil, rErr
|
||||
}
|
||||
} // else we have reached EOF after few reads, no need to
|
||||
// add an additional 0bytes at the end.
|
||||
break
|
||||
}
|
||||
if rErr != nil && rErr != io.ErrUnexpectedEOF {
|
||||
return 0, nil, rErr
|
||||
}
|
||||
if n > 0 {
|
||||
// Returns encoded blocks.
|
||||
var enErr error
|
||||
blocks, enErr = encodeData(buf[0:n], dataBlocks, parityBlocks)
|
||||
if enErr != nil {
|
||||
return 0, nil, enErr
|
||||
}
|
||||
|
||||
// Write to all disks.
|
||||
if err = appendFile(disks, volume, path, blocks, hashWriters, writeQuorum); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
bytesWritten += int64(n)
|
||||
}
|
||||
}
|
||||
|
||||
checkSums = make([]string, len(disks))
|
||||
for i := range checkSums {
|
||||
checkSums[i] = hex.EncodeToString(hashWriters[i].Sum(nil))
|
||||
}
|
||||
return bytesWritten, checkSums, nil
|
||||
}
|
||||
|
||||
// encodeData - encodes incoming data buffer into
|
||||
// dataBlocks+parityBlocks returns a 2 dimensional byte array.
|
||||
func encodeData(dataBuffer []byte, dataBlocks, parityBlocks int) ([][]byte, error) {
|
||||
rs, err := reedsolomon.New(dataBlocks, parityBlocks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Split the input buffer into data and parity blocks.
|
||||
var blocks [][]byte
|
||||
blocks, err = rs.Split(dataBuffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Encode parity blocks using data blocks.
|
||||
err = rs.Encode(blocks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return encoded blocks.
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
// appendFile - append data buffer at path.
|
||||
func appendFile(disks []StorageAPI, volume, path string, enBlocks [][]byte, hashWriters []hash.Hash, writeQuorum int) (err error) {
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wErrs = make([]error, len(disks))
|
||||
// Write encoded data to quorum disks in parallel.
|
||||
for index, disk := range disks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
// Write encoded data in routine.
|
||||
go func(index int, disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
wErr := disk.AppendFile(volume, path, enBlocks[index])
|
||||
if wErr != nil {
|
||||
wErrs[index] = wErr
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate hash for each blocks.
|
||||
hashWriters[index].Write(enBlocks[index])
|
||||
|
||||
// Successfully wrote.
|
||||
wErrs[index] = nil
|
||||
}(index, disk)
|
||||
}
|
||||
|
||||
// Wait for all the appends to finish.
|
||||
wg.Wait()
|
||||
|
||||
// Do we have write quorum?.
|
||||
if !isDiskQuorum(wErrs, writeQuorum) {
|
||||
return errXLWriteQuorum
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
)
|
||||
|
||||
// Simulates a faulty disk for AppendFile()
|
||||
type AppendDiskDown struct {
|
||||
*posix
|
||||
}
|
||||
|
||||
func (a AppendDiskDown) AppendFile(volume string, path string, buf []byte) error {
|
||||
return errFaultyDisk
|
||||
}
|
||||
|
||||
// Test erasureCreateFile()
|
||||
func TestErasureCreateFile(t *testing.T) {
|
||||
// Initialize environment needed for the test.
|
||||
dataBlocks := 7
|
||||
parityBlocks := 7
|
||||
blockSize := int64(blockSizeV1)
|
||||
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer setup.Remove()
|
||||
|
||||
disks := setup.disks
|
||||
|
||||
// Prepare a slice of 1MB with random data.
|
||||
data := make([]byte, 1*1024*1024)
|
||||
_, err = rand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Test when all disks are up.
|
||||
size, _, err := erasureCreateFile(disks, "testbucket", "testobject1", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != int64(len(data)) {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, len(data))
|
||||
}
|
||||
|
||||
// 2 disks down.
|
||||
disks[4] = AppendDiskDown{disks[4].(*posix)}
|
||||
disks[5] = AppendDiskDown{disks[5].(*posix)}
|
||||
|
||||
// Test when two disks are down.
|
||||
size, _, err = erasureCreateFile(disks, "testbucket", "testobject2", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != int64(len(data)) {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, len(data))
|
||||
}
|
||||
|
||||
// 4 more disks down. 6 disks down in total.
|
||||
disks[6] = AppendDiskDown{disks[6].(*posix)}
|
||||
disks[7] = AppendDiskDown{disks[7].(*posix)}
|
||||
disks[8] = AppendDiskDown{disks[8].(*posix)}
|
||||
disks[9] = AppendDiskDown{disks[9].(*posix)}
|
||||
|
||||
size, _, err = erasureCreateFile(disks, "testbucket", "testobject3", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != int64(len(data)) {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, len(data))
|
||||
}
|
||||
|
||||
// 1 more disk down. 7 disk down in total. Should return quorum error.
|
||||
disks[10] = AppendDiskDown{disks[10].(*posix)}
|
||||
_, _, err = erasureCreateFile(disks, "testbucket", "testobject4", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if err != errXLWriteQuorum {
|
||||
t.Errorf("erasureCreateFile returned expected errXLWriteQuorum error, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestErasureEncode checks for encoding for different data sets.
|
||||
func TestErasureEncode(t *testing.T) {
|
||||
// Collection of cases for encode cases.
|
||||
testEncodeCases := []struct {
|
||||
inputData []byte
|
||||
inputDataBlocks int
|
||||
inputParityBlocks int
|
||||
|
||||
shouldPass bool
|
||||
expectedErr error
|
||||
}{
|
||||
// TestCase - 1.
|
||||
// Regular data encoded.
|
||||
{
|
||||
[]byte("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."),
|
||||
8,
|
||||
8,
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
// TestCase - 2.
|
||||
// Empty data errors out.
|
||||
{
|
||||
[]byte(""),
|
||||
8,
|
||||
8,
|
||||
false,
|
||||
reedsolomon.ErrShortData,
|
||||
},
|
||||
// TestCase - 3.
|
||||
// Single byte encoded.
|
||||
{
|
||||
[]byte("1"),
|
||||
4,
|
||||
4,
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
// TestCase - 4.
|
||||
// test case with negative data block.
|
||||
{
|
||||
[]byte("1"),
|
||||
-1,
|
||||
8,
|
||||
false,
|
||||
reedsolomon.ErrInvShardNum,
|
||||
},
|
||||
// TestCase - 5.
|
||||
// test case with negative parity block.
|
||||
{
|
||||
[]byte("1"),
|
||||
8,
|
||||
-1,
|
||||
false,
|
||||
reedsolomon.ErrInvShardNum,
|
||||
},
|
||||
// TestCase - 6.
|
||||
// test case with zero data block.
|
||||
{
|
||||
[]byte("1"),
|
||||
0,
|
||||
8,
|
||||
false,
|
||||
reedsolomon.ErrInvShardNum,
|
||||
},
|
||||
// TestCase - 7.
|
||||
// test case with zero parity block.
|
||||
{
|
||||
[]byte("1"),
|
||||
8,
|
||||
0,
|
||||
false,
|
||||
reedsolomon.ErrInvShardNum,
|
||||
},
|
||||
// TestCase - 8.
|
||||
// test case with data + parity blocks > 255.
|
||||
// expected to fail with Error Max Shard number.
|
||||
{
|
||||
[]byte("1"),
|
||||
128,
|
||||
128,
|
||||
false,
|
||||
reedsolomon.ErrMaxShardNum,
|
||||
},
|
||||
}
|
||||
|
||||
// Test encode cases.
|
||||
for i, testCase := range testEncodeCases {
|
||||
_, actualErr := encodeData(testCase.inputData, testCase.inputDataBlocks, testCase.inputParityBlocks)
|
||||
if actualErr != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass but failed instead with \"%s\"", i+1, actualErr)
|
||||
}
|
||||
if actualErr == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to fail with error <Error> \"%v\", but instead passed", i+1, testCase.expectedErr)
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if actualErr != nil && !testCase.shouldPass {
|
||||
if testCase.expectedErr != actualErr {
|
||||
t.Errorf("Test %d: Expected Error to be \"%v\", but instead found \"%v\" ", i+1, testCase.expectedErr, actualErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
"github.com/minio/minio/pkg/bpool"
|
||||
)
|
||||
|
||||
// isSuccessDecodeBlocks - do we have all the blocks to be
|
||||
// successfully decoded?. Input encoded blocks ordered matrix.
|
||||
func isSuccessDecodeBlocks(enBlocks [][]byte, dataBlocks int) bool {
|
||||
// Count number of data and parity blocks that were read.
|
||||
var successDataBlocksCount = 0
|
||||
var successParityBlocksCount = 0
|
||||
for index := range enBlocks {
|
||||
if enBlocks[index] == nil {
|
||||
continue
|
||||
}
|
||||
// block index lesser than data blocks, update data block count.
|
||||
if index < dataBlocks {
|
||||
successDataBlocksCount++
|
||||
continue
|
||||
} // else { // update parity block count.
|
||||
successParityBlocksCount++
|
||||
}
|
||||
// Returns true if we have atleast dataBlocks parity.
|
||||
return successDataBlocksCount == dataBlocks || successDataBlocksCount+successParityBlocksCount >= dataBlocks
|
||||
}
|
||||
|
||||
// isSuccessDataBlocks - do we have all the data blocks?
|
||||
// Input encoded blocks ordered matrix.
|
||||
func isSuccessDataBlocks(enBlocks [][]byte, dataBlocks int) bool {
|
||||
// Count number of data blocks that were read.
|
||||
var successDataBlocksCount = 0
|
||||
for index := range enBlocks[:dataBlocks] {
|
||||
if enBlocks[index] == nil {
|
||||
continue
|
||||
}
|
||||
// block index lesser than data blocks, update data block count.
|
||||
if index < dataBlocks {
|
||||
successDataBlocksCount++
|
||||
}
|
||||
}
|
||||
// Returns true if we have atleast the dataBlocks.
|
||||
return successDataBlocksCount >= dataBlocks
|
||||
}
|
||||
|
||||
// Return readable disks slice from which we can read parallelly.
|
||||
func getReadDisks(orderedDisks []StorageAPI, index int, dataBlocks int) (readDisks []StorageAPI, nextIndex int, err error) {
|
||||
readDisks = make([]StorageAPI, len(orderedDisks))
|
||||
dataDisks := 0
|
||||
parityDisks := 0
|
||||
// Count already read data and parity chunks.
|
||||
for i := 0; i < index; i++ {
|
||||
if orderedDisks[i] == nil {
|
||||
continue
|
||||
}
|
||||
if i < dataBlocks {
|
||||
dataDisks++
|
||||
} else {
|
||||
parityDisks++
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity checks - we should never have this situation.
|
||||
if dataDisks == dataBlocks {
|
||||
return nil, 0, errUnexpected
|
||||
}
|
||||
if dataDisks+parityDisks >= dataBlocks {
|
||||
return nil, 0, errUnexpected
|
||||
}
|
||||
|
||||
// Find the disks from which next set of parallel reads should happen.
|
||||
for i := index; i < len(orderedDisks); i++ {
|
||||
if orderedDisks[i] == nil {
|
||||
continue
|
||||
}
|
||||
if i < dataBlocks {
|
||||
dataDisks++
|
||||
} else {
|
||||
parityDisks++
|
||||
}
|
||||
readDisks[i] = orderedDisks[i]
|
||||
if dataDisks == dataBlocks {
|
||||
return readDisks, i + 1, nil
|
||||
} else if dataDisks+parityDisks == dataBlocks {
|
||||
return readDisks, i + 1, nil
|
||||
}
|
||||
}
|
||||
return nil, 0, errXLReadQuorum
|
||||
}
|
||||
|
||||
// parallelRead - reads chunks in parallel from the disks specified in []readDisks.
|
||||
func parallelRead(volume, path string, readDisks []StorageAPI, orderedDisks []StorageAPI, enBlocks [][]byte, blockOffset int64, curChunkSize int64, bitRotVerify func(diskIndex int) bool, pool *bpool.BytePool) {
|
||||
// WaitGroup to synchronise the read go-routines.
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
// Read disks in parallel.
|
||||
for index := range readDisks {
|
||||
if readDisks[index] == nil {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
// Reads chunk from readDisk[index] in routine.
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
|
||||
// Verify bit rot for the file on this disk.
|
||||
if !bitRotVerify(index) {
|
||||
// So that we don't read from this disk for the next block.
|
||||
orderedDisks[index] = nil
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := pool.Get()
|
||||
if err != nil {
|
||||
errorIf(err, "unable to get buffer from byte pool")
|
||||
orderedDisks[index] = nil
|
||||
return
|
||||
}
|
||||
buf = buf[:curChunkSize]
|
||||
|
||||
_, err = readDisks[index].ReadFile(volume, path, blockOffset, buf)
|
||||
if err != nil {
|
||||
orderedDisks[index] = nil
|
||||
return
|
||||
}
|
||||
enBlocks[index] = buf
|
||||
}(index)
|
||||
}
|
||||
|
||||
// Waiting for first routines to finish.
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// erasureReadFile - read bytes from erasure coded files and writes to given writer.
|
||||
// Erasure coded files are read block by block as per given erasureInfo and data chunks
|
||||
// are decoded into a data block. Data block is trimmed for given offset and length,
|
||||
// then written to given writer. This function also supports bit-rot detection by
|
||||
// verifying checksum of individual block's checksum.
|
||||
func erasureReadFile(writer io.Writer, disks []StorageAPI, volume string, path string, offset int64, length int64, totalLength int64, blockSize int64, dataBlocks int, parityBlocks int, checkSums []string, algo string, pool *bpool.BytePool) (int64, error) {
|
||||
// Offset and length cannot be negative.
|
||||
if offset < 0 || length < 0 {
|
||||
return 0, errUnexpected
|
||||
}
|
||||
|
||||
// Can't request more data than what is available.
|
||||
if offset+length > totalLength {
|
||||
return 0, errUnexpected
|
||||
}
|
||||
|
||||
// chunkSize is the amount of data that needs to be read from each disk at a time.
|
||||
chunkSize := getChunkSize(blockSize, dataBlocks)
|
||||
|
||||
// bitRotVerify verifies if the file on a particular disk doesn't have bitrot
|
||||
// by verifying the hash of the contents of the file.
|
||||
bitRotVerify := func() func(diskIndex int) bool {
|
||||
verified := make([]bool, len(disks))
|
||||
// Return closure so that we have reference to []verified and
|
||||
// not recalculate the hash on it every time the function is
|
||||
// called for the same disk.
|
||||
return func(diskIndex int) bool {
|
||||
if verified[diskIndex] {
|
||||
// Already validated.
|
||||
return true
|
||||
}
|
||||
// Is this a valid block?
|
||||
isValid := isValidBlock(disks[diskIndex], volume, path, checkSums[diskIndex], algo)
|
||||
verified[diskIndex] = isValid
|
||||
return isValid
|
||||
}
|
||||
}()
|
||||
|
||||
// Total bytes written to writer
|
||||
bytesWritten := int64(0)
|
||||
|
||||
startBlock := offset / blockSize
|
||||
endBlock := (offset + length) / blockSize
|
||||
|
||||
// curChunkSize = chunk size for the current block in the for loop below.
|
||||
// curBlockSize = block size for the current block in the for loop below.
|
||||
// curChunkSize and curBlockSize can change for the last block if totalLength%blockSize != 0
|
||||
curChunkSize := chunkSize
|
||||
curBlockSize := blockSize
|
||||
|
||||
// For each block, read chunk from each disk. If we are able to read all the data disks then we don't
|
||||
// need to read parity disks. If one of the data disk is missing we need to read DataBlocks+1 number
|
||||
// of disks. Once read, we Reconstruct() missing data if needed and write it to the given writer.
|
||||
for block := startBlock; block <= endBlock; block++ {
|
||||
// Mark all buffers as unused at the start of the loop so that the buffers
|
||||
// can be reused.
|
||||
pool.Reset()
|
||||
|
||||
// Each element of enBlocks holds curChunkSize'd amount of data read from its corresponding disk.
|
||||
enBlocks := make([][]byte, len(disks))
|
||||
|
||||
if ((offset + bytesWritten) / blockSize) == (totalLength / blockSize) {
|
||||
// This is the last block for which curBlockSize and curChunkSize can change.
|
||||
// For ex. if totalLength is 15M and blockSize is 10MB, curBlockSize for
|
||||
// the last block should be 5MB.
|
||||
curBlockSize = totalLength % blockSize
|
||||
curChunkSize = getChunkSize(curBlockSize, dataBlocks)
|
||||
}
|
||||
|
||||
// NOTE: That for the offset calculation we have to use chunkSize and
|
||||
// not curChunkSize. If we use curChunkSize for offset calculation
|
||||
// then it can result in wrong offset for the last block.
|
||||
blockOffset := block * chunkSize
|
||||
|
||||
// nextIndex - index from which next set of parallel reads
|
||||
// should happen.
|
||||
nextIndex := 0
|
||||
|
||||
for {
|
||||
// readDisks - disks from which we need to read in parallel.
|
||||
var readDisks []StorageAPI
|
||||
var err error
|
||||
// get readable disks slice from which we can read parallelly.
|
||||
readDisks, nextIndex, err = getReadDisks(disks, nextIndex, dataBlocks)
|
||||
if err != nil {
|
||||
return bytesWritten, err
|
||||
}
|
||||
// Issue a parallel read across the disks specified in readDisks.
|
||||
parallelRead(volume, path, readDisks, disks, enBlocks, blockOffset, curChunkSize, bitRotVerify, pool)
|
||||
if isSuccessDecodeBlocks(enBlocks, dataBlocks) {
|
||||
// If enough blocks are available to do rs.Reconstruct()
|
||||
break
|
||||
}
|
||||
if nextIndex == len(disks) {
|
||||
// No more disks to read from.
|
||||
return bytesWritten, errXLReadQuorum
|
||||
}
|
||||
// We do not have enough enough data blocks to reconstruct the data
|
||||
// hence continue the for-loop till we have enough data blocks.
|
||||
}
|
||||
|
||||
// If we have all the data blocks no need to decode, continue to write.
|
||||
if !isSuccessDataBlocks(enBlocks, dataBlocks) {
|
||||
// Reconstruct the missing data blocks.
|
||||
if err := decodeData(enBlocks, dataBlocks, parityBlocks); err != nil {
|
||||
return bytesWritten, err
|
||||
}
|
||||
}
|
||||
|
||||
// Offset in enBlocks from where data should be read from.
|
||||
enBlocksOffset := int64(0)
|
||||
|
||||
// Total data to be read from enBlocks.
|
||||
enBlocksLength := curBlockSize
|
||||
|
||||
// If this is the start block then enBlocksOffset might not be 0.
|
||||
if block == startBlock {
|
||||
enBlocksOffset = offset % blockSize
|
||||
enBlocksLength -= enBlocksOffset
|
||||
}
|
||||
|
||||
remaining := length - bytesWritten
|
||||
if remaining < enBlocksLength {
|
||||
// We should not send more data than what was requested.
|
||||
enBlocksLength = remaining
|
||||
}
|
||||
|
||||
// Write data blocks.
|
||||
n, err := writeDataBlocks(writer, enBlocks, dataBlocks, enBlocksOffset, enBlocksLength)
|
||||
if err != nil {
|
||||
return bytesWritten, err
|
||||
}
|
||||
|
||||
// Update total bytes written.
|
||||
bytesWritten += n
|
||||
|
||||
if bytesWritten == length {
|
||||
// Done writing all the requested data.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Success.
|
||||
return bytesWritten, nil
|
||||
}
|
||||
|
||||
// isValidBlock - calculates the checksum hash for the block and
|
||||
// validates if its correct returns true for valid cases, false otherwise.
|
||||
func isValidBlock(disk StorageAPI, volume, path, checkSum, checkSumAlgo string) (ok bool) {
|
||||
// Disk is not available, not a valid block.
|
||||
if disk == nil {
|
||||
return false
|
||||
}
|
||||
// Checksum not available, not a valid block.
|
||||
if checkSum == "" {
|
||||
return false
|
||||
}
|
||||
// Read everything for a given block and calculate hash.
|
||||
hashWriter := newHash(checkSumAlgo)
|
||||
hashBytes, err := hashSum(disk, volume, path, hashWriter)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to calculate checksum %s/%s", volume, path)
|
||||
return false
|
||||
}
|
||||
return hex.EncodeToString(hashBytes) == checkSum
|
||||
}
|
||||
|
||||
// decodeData - decode encoded blocks.
|
||||
func decodeData(enBlocks [][]byte, dataBlocks, parityBlocks int) error {
|
||||
// Initialized reedsolomon.
|
||||
rs, err := reedsolomon.New(dataBlocks, parityBlocks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reconstruct encoded blocks.
|
||||
err = rs.Reconstruct(enBlocks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify reconstructed blocks (parity).
|
||||
ok, err := rs.Verify(enBlocks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
// Blocks cannot be reconstructed, corrupted data.
|
||||
err = errors.New("Verification failed after reconstruction, data likely corrupted.")
|
||||
return err
|
||||
}
|
||||
|
||||
// Success.
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/bpool"
|
||||
)
|
||||
import "reflect"
|
||||
|
||||
// Tests getReadDisks which returns readable disks slice from which we can
|
||||
// read parallelly.
|
||||
func testGetReadDisks(t *testing.T, xl xlObjects) {
|
||||
d := xl.storageDisks
|
||||
testCases := []struct {
|
||||
index int // index argument for getReadDisks
|
||||
argDisks []StorageAPI // disks argument for getReadDisks
|
||||
retDisks []StorageAPI // disks return value from getReadDisks
|
||||
nextIndex int // return value from getReadDisks
|
||||
err error // error return value from getReadDisks
|
||||
}{
|
||||
// Test case - 1.
|
||||
// When all disks are available, should return data disks.
|
||||
{
|
||||
0,
|
||||
[]StorageAPI{d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15]},
|
||||
[]StorageAPI{d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], nil, nil, nil, nil, nil, nil, nil, nil},
|
||||
8,
|
||||
nil,
|
||||
},
|
||||
// Test case - 2.
|
||||
// If a parity disk is down, should return all data disks.
|
||||
{
|
||||
0,
|
||||
[]StorageAPI{d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], nil, d[10], d[11], d[12], d[13], d[14], d[15]},
|
||||
[]StorageAPI{d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], nil, nil, nil, nil, nil, nil, nil, nil},
|
||||
8,
|
||||
nil,
|
||||
},
|
||||
// Test case - 3.
|
||||
// If a data disk is down, should return 7 data and 1 parity.
|
||||
{
|
||||
0,
|
||||
[]StorageAPI{nil, d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15]},
|
||||
[]StorageAPI{nil, d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], nil, nil, nil, nil, nil, nil, nil},
|
||||
9,
|
||||
nil,
|
||||
},
|
||||
// Test case - 4.
|
||||
// If 7 data disks are down, should return 1 data and 7 parity.
|
||||
{
|
||||
0,
|
||||
[]StorageAPI{nil, nil, nil, nil, nil, nil, nil, d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15]},
|
||||
[]StorageAPI{nil, nil, nil, nil, nil, nil, nil, d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], nil},
|
||||
15,
|
||||
nil,
|
||||
},
|
||||
// Test case - 5.
|
||||
// When 2 disks fail during parallelRead, next call to getReadDisks should return 3 disks
|
||||
{
|
||||
8,
|
||||
[]StorageAPI{nil, nil, d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15]},
|
||||
[]StorageAPI{nil, nil, nil, nil, nil, nil, nil, nil, d[8], d[9], nil, nil, nil, nil, nil, nil},
|
||||
10,
|
||||
nil,
|
||||
},
|
||||
// Test case - 6.
|
||||
// If 2 disks again fail from the 3 disks returned previously, return next 2 disks
|
||||
{
|
||||
11,
|
||||
[]StorageAPI{nil, nil, d[2], d[3], d[4], d[5], d[6], d[7], nil, nil, d[10], d[11], d[12], d[13], d[14], d[15]},
|
||||
[]StorageAPI{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, d[11], nil, nil, nil, nil},
|
||||
12,
|
||||
nil,
|
||||
},
|
||||
// Test case - 7.
|
||||
// No more disks are available for read, return error
|
||||
{
|
||||
13,
|
||||
[]StorageAPI{nil, nil, d[2], d[3], d[4], d[5], d[6], d[7], nil, nil, d[10], nil, nil, nil, nil, nil},
|
||||
nil,
|
||||
0,
|
||||
errXLReadQuorum,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
disks, nextIndex, err := getReadDisks(test.argDisks, test.index, xl.dataBlocks)
|
||||
if err != test.err {
|
||||
t.Errorf("test-case %d - expected error : %s, got : %s", i+1, test.err, err)
|
||||
continue
|
||||
}
|
||||
if test.nextIndex != nextIndex {
|
||||
t.Errorf("test-case %d - expected nextIndex: %d, got : %d", i+1, test.nextIndex, nextIndex)
|
||||
continue
|
||||
}
|
||||
if reflect.DeepEqual(test.retDisks, disks) == false {
|
||||
t.Errorf("test-case %d : incorrect disks returned. expected %+v, got %+v", i+1, test.retDisks, disks)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test getOrderedDisks which returns ordered slice of disks from their
|
||||
// actual distribution.
|
||||
func testGetOrderedDisks(t *testing.T, xl xlObjects) {
|
||||
disks := xl.storageDisks
|
||||
distribution := []int{16, 14, 12, 10, 8, 6, 4, 2, 1, 3, 5, 7, 9, 11, 13, 15}
|
||||
orderedDisks := getOrderedDisks(distribution, disks)
|
||||
// From the "distribution" above you can notice that:
|
||||
// 1st data block is in the 9th disk (i.e distribution index 8)
|
||||
// 2nd data block is in the 8th disk (i.e distribution index 7) and so on.
|
||||
if orderedDisks[0] != disks[8] ||
|
||||
orderedDisks[1] != disks[7] ||
|
||||
orderedDisks[2] != disks[9] ||
|
||||
orderedDisks[3] != disks[6] ||
|
||||
orderedDisks[4] != disks[10] ||
|
||||
orderedDisks[5] != disks[5] ||
|
||||
orderedDisks[6] != disks[11] ||
|
||||
orderedDisks[7] != disks[4] ||
|
||||
orderedDisks[8] != disks[12] ||
|
||||
orderedDisks[9] != disks[3] ||
|
||||
orderedDisks[10] != disks[13] ||
|
||||
orderedDisks[11] != disks[2] ||
|
||||
orderedDisks[12] != disks[14] ||
|
||||
orderedDisks[13] != disks[1] ||
|
||||
orderedDisks[14] != disks[15] ||
|
||||
orderedDisks[15] != disks[0] {
|
||||
t.Errorf("getOrderedDisks returned incorrect order.")
|
||||
}
|
||||
}
|
||||
|
||||
// Test for isSuccessDataBlocks and isSuccessDecodeBlocks.
|
||||
func TestIsSuccessBlocks(t *testing.T) {
|
||||
dataBlocks := 8
|
||||
testCases := []struct {
|
||||
enBlocks [][]byte // data and parity blocks.
|
||||
successData bool // expected return value of isSuccessDataBlocks()
|
||||
successDecode bool // expected return value of isSuccessDecodeBlocks()
|
||||
}{
|
||||
{
|
||||
// When all data and partity blocks are available.
|
||||
[][]byte{
|
||||
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
},
|
||||
true,
|
||||
true,
|
||||
},
|
||||
{
|
||||
// When one data block is not available.
|
||||
[][]byte{
|
||||
nil, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
// When one data and all parity are available, enough for reedsolomon.Reconstruct()
|
||||
[][]byte{
|
||||
nil, nil, nil, nil, nil, nil, nil, {'a'},
|
||||
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
// When all data disks are not available, enough for reedsolomon.Reconstruct()
|
||||
[][]byte{
|
||||
nil, nil, nil, nil, nil, nil, nil, nil,
|
||||
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
// Not enough disks for reedsolomon.Reconstruct()
|
||||
[][]byte{
|
||||
nil, nil, nil, nil, nil, nil, nil, nil,
|
||||
nil, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
got := isSuccessDataBlocks(test.enBlocks, dataBlocks)
|
||||
if test.successData != got {
|
||||
t.Errorf("test-case %d : expected %v got %v", i+1, test.successData, got)
|
||||
}
|
||||
got = isSuccessDecodeBlocks(test.enBlocks, dataBlocks)
|
||||
if test.successDecode != got {
|
||||
t.Errorf("test-case %d : expected %v got %v", i+1, test.successDecode, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper function for testGetReadDisks, testGetOrderedDisks.
|
||||
func TestErasureReadUtils(t *testing.T) {
|
||||
objLayer, dirs, err := getXLObjectLayer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer removeRoots(dirs)
|
||||
xl := objLayer.(xlObjects)
|
||||
testGetReadDisks(t, xl)
|
||||
testGetOrderedDisks(t, xl)
|
||||
}
|
||||
|
||||
// Simulates a faulty disk for ReadFile()
|
||||
type ReadDiskDown struct {
|
||||
*posix
|
||||
}
|
||||
|
||||
func (r ReadDiskDown) ReadFile(volume string, path string, offset int64, buf []byte) (n int64, err error) {
|
||||
return 0, errFaultyDisk
|
||||
}
|
||||
|
||||
func TestErasureReadFileDiskFail(t *testing.T) {
|
||||
// Initialize environment needed for the test.
|
||||
dataBlocks := 7
|
||||
parityBlocks := 7
|
||||
blockSize := int64(blockSizeV1)
|
||||
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer setup.Remove()
|
||||
|
||||
disks := setup.disks
|
||||
|
||||
// Prepare a slice of 1MB with random data.
|
||||
data := make([]byte, 1*1024*1024)
|
||||
length := int64(len(data))
|
||||
_, err = rand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a test file to read from.
|
||||
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != length {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, length)
|
||||
}
|
||||
|
||||
// create byte pool which will be used by erasureReadFile for
|
||||
// reading from disks and erasure decoding.
|
||||
chunkSize := getChunkSize(blockSize, dataBlocks)
|
||||
pool := bpool.NewBytePool(chunkSize, len(disks))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", 0, length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !bytes.Equal(buf.Bytes(), data) {
|
||||
t.Error("Contents of the erasure coded file differs")
|
||||
}
|
||||
|
||||
// 2 disks down. Read should succeed.
|
||||
disks[4] = ReadDiskDown{disks[4].(*posix)}
|
||||
disks[5] = ReadDiskDown{disks[5].(*posix)}
|
||||
|
||||
buf.Reset()
|
||||
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", 0, length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !bytes.Equal(buf.Bytes(), data) {
|
||||
t.Error("Contents of the erasure coded file differs")
|
||||
}
|
||||
|
||||
// 4 more disks down. 6 disks down in total. Read should succeed.
|
||||
disks[6] = ReadDiskDown{disks[6].(*posix)}
|
||||
disks[8] = ReadDiskDown{disks[8].(*posix)}
|
||||
disks[9] = ReadDiskDown{disks[9].(*posix)}
|
||||
disks[11] = ReadDiskDown{disks[11].(*posix)}
|
||||
|
||||
buf.Reset()
|
||||
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", 0, length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !bytes.Equal(buf.Bytes(), data) {
|
||||
t.Error("Contents of the erasure coded file differs")
|
||||
}
|
||||
|
||||
// 2 more disk down. 8 disks down in total. Read should fail.
|
||||
disks[12] = ReadDiskDown{disks[12].(*posix)}
|
||||
disks[13] = ReadDiskDown{disks[13].(*posix)}
|
||||
buf.Reset()
|
||||
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", 0, length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
|
||||
if err != errXLReadQuorum {
|
||||
t.Fatal("expected errXLReadQuorum error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErasureReadFileOffsetLength(t *testing.T) {
|
||||
// Initialize environment needed for the test.
|
||||
dataBlocks := 7
|
||||
parityBlocks := 7
|
||||
blockSize := int64(1 * 1024 * 1024)
|
||||
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer setup.Remove()
|
||||
|
||||
disks := setup.disks
|
||||
|
||||
// Prepare a slice of 5MB with random data.
|
||||
data := make([]byte, 5*1024*1024)
|
||||
length := int64(len(data))
|
||||
_, err = rand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a test file to read from.
|
||||
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != length {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, length)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
offset, length int64
|
||||
}{
|
||||
// Full file.
|
||||
{0, length},
|
||||
// Read nothing.
|
||||
{length, 0},
|
||||
// 2nd block.
|
||||
{blockSize, blockSize},
|
||||
// Test cases for random offsets and lengths.
|
||||
{blockSize - 1, 2},
|
||||
{blockSize - 1, blockSize + 1},
|
||||
{blockSize + 1, blockSize - 1},
|
||||
{blockSize + 1, blockSize},
|
||||
{blockSize + 1, blockSize + 1},
|
||||
{blockSize*2 - 1, blockSize + 1},
|
||||
{length - 1, 1},
|
||||
{length - blockSize, blockSize},
|
||||
{length - blockSize - 1, blockSize},
|
||||
{length - blockSize - 1, blockSize + 1},
|
||||
}
|
||||
chunkSize := getChunkSize(blockSize, dataBlocks)
|
||||
pool := bpool.NewBytePool(chunkSize, len(disks))
|
||||
|
||||
// Compare the data read from file with "data" byte array.
|
||||
for i, testCase := range testCases {
|
||||
expected := data[testCase.offset:(testCase.offset + testCase.length)]
|
||||
buf := &bytes.Buffer{}
|
||||
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", testCase.offset, testCase.length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
got := buf.Bytes()
|
||||
if !bytes.Equal(expected, got) {
|
||||
t.Errorf("Test %d : read data is different from what was expected", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test erasureReadFile with random offset and lengths.
|
||||
// This test is t.Skip()ed as it a long time to run, hence should be run
|
||||
// explicitly after commenting out t.Skip()
|
||||
func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
||||
// Comment the following line to run this test.
|
||||
t.SkipNow()
|
||||
// Initialize environment needed for the test.
|
||||
dataBlocks := 7
|
||||
parityBlocks := 7
|
||||
blockSize := int64(1 * 1024 * 1024)
|
||||
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer setup.Remove()
|
||||
|
||||
disks := setup.disks
|
||||
|
||||
// Prepare a slice of 5MB with random data.
|
||||
data := make([]byte, 5*1024*1024)
|
||||
length := int64(len(data))
|
||||
_, err = rand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 10000 iterations with random offsets and lengths.
|
||||
iterations := 10000
|
||||
|
||||
// Create a test file to read from.
|
||||
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != length {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, length)
|
||||
}
|
||||
|
||||
// To generate random offset/length.
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
// create pool buffer which will be used by erasureReadFile for
|
||||
// reading from disks and erasure decoding.
|
||||
chunkSize := getChunkSize(blockSize, dataBlocks)
|
||||
pool := bpool.NewBytePool(chunkSize, len(disks))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
// Verify erasureReadFile() for random offsets and lengths.
|
||||
for i := 0; i < iterations; i++ {
|
||||
offset := r.Int63n(length)
|
||||
readLen := r.Int63n(length - offset)
|
||||
|
||||
expected := data[offset : offset+readLen]
|
||||
|
||||
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", offset, readLen, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
|
||||
if err != nil {
|
||||
t.Fatal(err, offset, readLen)
|
||||
}
|
||||
got := buf.Bytes()
|
||||
if !bytes.Equal(expected, got) {
|
||||
t.Fatalf("read data is different from what was expected, offset=%d length=%d", offset, readLen)
|
||||
}
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
"github.com/minio/blake2b-simd"
|
||||
)
|
||||
|
||||
// newHashWriters - inititialize a slice of hashes for the disk count.
|
||||
func newHashWriters(diskCount int, algo string) []hash.Hash {
|
||||
hashWriters := make([]hash.Hash, diskCount)
|
||||
for index := range hashWriters {
|
||||
hashWriters[index] = newHash(algo)
|
||||
}
|
||||
return hashWriters
|
||||
}
|
||||
|
||||
// newHash - gives you a newly allocated hash depending on the input algorithm.
|
||||
func newHash(algo string) hash.Hash {
|
||||
switch algo {
|
||||
case "blake2b":
|
||||
return blake2b.New512()
|
||||
// Add new hashes here.
|
||||
default:
|
||||
// Default to blake2b.
|
||||
return blake2b.New512()
|
||||
}
|
||||
}
|
||||
|
||||
// hashSum calculates the hash of the entire path and returns.
|
||||
func hashSum(disk StorageAPI, volume, path string, writer hash.Hash) ([]byte, error) {
|
||||
// Allocate staging buffer of 128KiB for copyBuffer.
|
||||
buf := make([]byte, readSizeV1)
|
||||
|
||||
// Copy entire buffer to writer.
|
||||
if err := copyBuffer(writer, disk, volume, path, buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return the final hash sum.
|
||||
return writer.Sum(nil), nil
|
||||
}
|
||||
|
||||
// getDataBlockLen - get length of data blocks from encoded blocks.
|
||||
func getDataBlockLen(enBlocks [][]byte, dataBlocks int) int {
|
||||
size := 0
|
||||
// Figure out the data block length.
|
||||
for _, block := range enBlocks[:dataBlocks] {
|
||||
size += len(block)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// Writes all the data blocks from encoded blocks until requested
|
||||
// outSize length. Provides a way to skip bytes until the offset.
|
||||
func writeDataBlocks(dst io.Writer, enBlocks [][]byte, dataBlocks int, offset int64, length int64) (int64, error) {
|
||||
// Offset and out size cannot be negative.
|
||||
if offset < 0 || length < 0 {
|
||||
return 0, errUnexpected
|
||||
}
|
||||
|
||||
// Do we have enough blocks?
|
||||
if len(enBlocks) < dataBlocks {
|
||||
return 0, reedsolomon.ErrTooFewShards
|
||||
}
|
||||
|
||||
// Do we have enough data?
|
||||
if int64(getDataBlockLen(enBlocks, dataBlocks)) < length {
|
||||
return 0, reedsolomon.ErrShortData
|
||||
}
|
||||
|
||||
// Counter to decrement total left to write.
|
||||
write := length
|
||||
|
||||
// Counter to increment total written.
|
||||
totalWritten := int64(0)
|
||||
|
||||
// Write all data blocks to dst.
|
||||
for _, block := range enBlocks[:dataBlocks] {
|
||||
// Skip blocks until we have reached our offset.
|
||||
if offset >= int64(len(block)) {
|
||||
// Decrement offset.
|
||||
offset -= int64(len(block))
|
||||
continue
|
||||
} else {
|
||||
// Skip until offset.
|
||||
block = block[offset:]
|
||||
|
||||
// Reset the offset for next iteration to read everything
|
||||
// from subsequent blocks.
|
||||
offset = 0
|
||||
}
|
||||
// We have written all the blocks, write the last remaining block.
|
||||
if write < int64(len(block)) {
|
||||
n, err := io.Copy(dst, bytes.NewReader(block[:write]))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
totalWritten += n
|
||||
break
|
||||
}
|
||||
// Copy the block.
|
||||
n, err := io.Copy(dst, bytes.NewReader(block))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Decrement output size.
|
||||
write -= n
|
||||
|
||||
// Increment written.
|
||||
totalWritten += n
|
||||
}
|
||||
|
||||
// Success.
|
||||
return totalWritten, nil
|
||||
}
|
||||
|
||||
// chunkSize is roughly BlockSize/DataBlocks.
|
||||
// chunkSize is calculated such that chunkSize*DataBlocks accommodates BlockSize bytes.
|
||||
// So chunkSize*DataBlocks can be slightly larger than BlockSize if BlockSize is not divisible by
|
||||
// DataBlocks. The extra space will have 0-padding.
|
||||
func getChunkSize(blockSize int64, dataBlocks int) int64 {
|
||||
return (blockSize + int64(dataBlocks) - 1) / int64(dataBlocks)
|
||||
}
|
||||
|
||||
// copyBuffer - copies from disk, volume, path to input writer until either EOF
|
||||
// is reached at volume, path or an error occurs. A success copyBuffer returns
|
||||
// err == nil, not err == EOF. Because copyBuffer is defined to read from path
|
||||
// until EOF. It does not treat an EOF from ReadFile an error to be reported.
|
||||
// Additionally copyBuffer stages through the provided buffer; otherwise if it
|
||||
// has zero length, returns error.
|
||||
func copyBuffer(writer io.Writer, disk StorageAPI, volume string, path string, buf []byte) error {
|
||||
// Error condition of zero length buffer.
|
||||
if buf != nil && len(buf) == 0 {
|
||||
return errors.New("empty buffer in readBuffer")
|
||||
}
|
||||
|
||||
// Starting offset for Reading the file.
|
||||
startOffset := int64(0)
|
||||
|
||||
// Read until io.EOF.
|
||||
for {
|
||||
n, err := disk.ReadFile(volume, path, startOffset, buf)
|
||||
if n > 0 {
|
||||
m, wErr := writer.Write(buf[:n])
|
||||
if wErr != nil {
|
||||
return wErr
|
||||
}
|
||||
if int64(m) != n {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
// Progress the offset.
|
||||
startOffset += n
|
||||
}
|
||||
|
||||
// Success.
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test validates the number hash writers returned.
|
||||
func TestNewHashWriters(t *testing.T) {
|
||||
diskNum := 8
|
||||
hashWriters := newHashWriters(diskNum, bitRotAlgo)
|
||||
if len(hashWriters) != diskNum {
|
||||
t.Errorf("Expected %d hashWriters, but instead got %d", diskNum, len(hashWriters))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Tests validate the output of getChunkSize.
|
||||
func TestGetChunkSize(t *testing.T) {
|
||||
// Refer to comments on getChunkSize() for details.
|
||||
testCases := []struct {
|
||||
blockSize int64
|
||||
dataBlocks int
|
||||
// expected result.
|
||||
expectedChunkSize int64
|
||||
}{
|
||||
{
|
||||
10,
|
||||
10,
|
||||
1,
|
||||
},
|
||||
{
|
||||
10,
|
||||
11,
|
||||
1,
|
||||
},
|
||||
{
|
||||
10,
|
||||
9,
|
||||
2,
|
||||
},
|
||||
}
|
||||
// Verify getChunkSize() for the test cases.
|
||||
for i, testCase := range testCases {
|
||||
got := getChunkSize(testCase.blockSize, testCase.dataBlocks)
|
||||
if testCase.expectedChunkSize != got {
|
||||
t.Errorf("Test %d : expected=%d got=%d", i+1, testCase.expectedChunkSize, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCopyBuffer - Tests validate the result and errors produced when `copyBuffer` is called with sample inputs.
|
||||
func TestCopyBuffer(t *testing.T) {
|
||||
// create posix test setup
|
||||
disk, diskPath, err := newPosixTestSetup()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create posix test setup, %s", err)
|
||||
}
|
||||
defer removeAll(diskPath)
|
||||
|
||||
volume := "success-vol"
|
||||
// Setup test environment.
|
||||
if err = disk.MakeVol(volume); err != nil {
|
||||
t.Fatalf("Unable to create volume, %s", err)
|
||||
}
|
||||
|
||||
// creating io.Writer for the input to copyBuffer.
|
||||
buffers := []*bytes.Buffer{
|
||||
new(bytes.Buffer),
|
||||
new(bytes.Buffer),
|
||||
new(bytes.Buffer),
|
||||
}
|
||||
|
||||
testFile := "testFile"
|
||||
testContent := []byte("hello, world")
|
||||
err = disk.AppendFile(volume, testFile, testContent)
|
||||
if err != nil {
|
||||
t.Fatalf("AppendFile failed: <ERROR> %s", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
writer io.Writer
|
||||
disk StorageAPI
|
||||
volume string
|
||||
path string
|
||||
buf []byte
|
||||
|
||||
expectedResult []byte
|
||||
// flag to indicate whether test case should pass.
|
||||
shouldPass bool
|
||||
expectedErr error
|
||||
}{
|
||||
// Test case - 1.
|
||||
// case with empty buffer.
|
||||
{nil, nil, "", "", []byte{}, nil, false, errors.New("empty buffer in readBuffer")},
|
||||
// Test case - 2.
|
||||
// Test case with empty volume.
|
||||
{buffers[0], disk, "", "", make([]byte, 5), nil, false, errInvalidArgument},
|
||||
// Test case - 3.
|
||||
// Test case with non existent volume.
|
||||
{buffers[0], disk, "abc", "", make([]byte, 5), nil, false, errVolumeNotFound},
|
||||
// Test case - 4.
|
||||
// Test case with empty filename/path.
|
||||
{buffers[0], disk, volume, "", make([]byte, 5), nil, false, errIsNotRegular},
|
||||
// Test case - 5.
|
||||
// Test case with non existent file name.
|
||||
{buffers[0], disk, volume, "abcd", make([]byte, 5), nil, false, errFileNotFound},
|
||||
// Test case - 6.
|
||||
// Test case where the writer returns EOF.
|
||||
{NewEOFWriter(buffers[0], 3), disk, volume, testFile, make([]byte, 5), nil, false, io.EOF},
|
||||
// Test case - 7.
|
||||
// Test case to produce io.ErrShortWrite, the TruncateWriter returns after writing 3 bytes.
|
||||
{TruncateWriter(buffers[1], 3), disk, volume, testFile, make([]byte, 5), nil, false, io.ErrShortWrite},
|
||||
// Teset case - 8.
|
||||
// Valid case, expected to read till EOF and write the contents into the writer.
|
||||
{buffers[2], disk, volume, testFile, make([]byte, 5), testContent, true, nil},
|
||||
}
|
||||
// iterate over the test cases and call copy Buffer with data.
|
||||
for i, testCase := range testCases {
|
||||
actualErr := copyBuffer(testCase.writer, testCase.disk, testCase.volume, testCase.path, testCase.buf)
|
||||
|
||||
if actualErr != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass but failed instead with \"%s\"", i+1, actualErr)
|
||||
}
|
||||
|
||||
if actualErr == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to fail with error <Error> \"%v\", but instead passed", i+1, testCase.expectedErr)
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if actualErr != nil && !testCase.shouldPass {
|
||||
if testCase.expectedErr.Error() != actualErr.Error() {
|
||||
t.Errorf("Test %d: Expected Error to be \"%v\", but instead found \"%v\" ", i+1, testCase.expectedErr, actualErr)
|
||||
}
|
||||
}
|
||||
// test passed as expected, asserting the result.
|
||||
if actualErr == nil && testCase.shouldPass {
|
||||
if !bytes.Equal(testCase.expectedResult, buffers[2].Bytes()) {
|
||||
t.Errorf("Test %d: copied buffer differs from the expected one.", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mustEncodeData - encodes data slice and provides encoded 2 dimensional slice.
|
||||
func mustEncodeData(data []byte, dataBlocks, parityBlocks int) [][]byte {
|
||||
encodedData, err := encodeData(data, dataBlocks, parityBlocks)
|
||||
if err != nil {
|
||||
// Upon failure panic this function.
|
||||
panic(err)
|
||||
}
|
||||
return encodedData
|
||||
}
|
||||
|
||||
// Generates good encoded data with one parity block and data block missing.
|
||||
func getGoodEncodedData(data []byte, dataBlocks, parityBlocks int) [][]byte {
|
||||
encodedData := mustEncodeData(data, dataBlocks, parityBlocks)
|
||||
encodedData[3] = nil
|
||||
encodedData[1] = nil
|
||||
return encodedData
|
||||
}
|
||||
|
||||
// Generates bad encoded data with one parity block and data block with garbage data.
|
||||
func getBadEncodedData(data []byte, dataBlocks, parityBlocks int) [][]byte {
|
||||
encodedData := mustEncodeData(data, dataBlocks, parityBlocks)
|
||||
encodedData[3] = []byte("garbage")
|
||||
encodedData[1] = []byte("garbage")
|
||||
return encodedData
|
||||
}
|
||||
|
||||
// Generates encoded data with all data blocks missing.
|
||||
func getMissingData(data []byte, dataBlocks, parityBlocks int) [][]byte {
|
||||
encodedData := mustEncodeData(data, dataBlocks, parityBlocks)
|
||||
for i := 0; i < dataBlocks+1; i++ {
|
||||
encodedData[i] = nil
|
||||
}
|
||||
return encodedData
|
||||
}
|
||||
|
||||
// Generates encoded data with less number of blocks than expected data blocks.
|
||||
func getInsufficientData(data []byte, dataBlocks, parityBlocks int) [][]byte {
|
||||
encodedData := mustEncodeData(data, dataBlocks, parityBlocks)
|
||||
// Return half of the data.
|
||||
return encodedData[:dataBlocks/2]
|
||||
}
|
||||
|
||||
// Represents erasure encoding matrix dataBlocks and paritBlocks.
|
||||
type encodingMatrix struct {
|
||||
dataBlocks int
|
||||
parityBlocks int
|
||||
}
|
||||
|
||||
// List of encoding matrices the tests will run on.
|
||||
var encodingMatrices = []encodingMatrix{
|
||||
{3, 3}, // 3 data, 3 parity blocks.
|
||||
{4, 4}, // 4 data, 4 parity blocks.
|
||||
{5, 5}, // 5 data, 5 parity blocks.
|
||||
{6, 6}, // 6 data, 6 parity blocks.
|
||||
{7, 7}, // 7 data, 7 parity blocks.
|
||||
{8, 8}, // 8 data, 8 parity blocks.
|
||||
}
|
||||
|
||||
// Tests erasure decoding functionality for various types of inputs.
|
||||
func TestErasureDecode(t *testing.T) {
|
||||
data := []byte("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.")
|
||||
|
||||
// List of decoding cases
|
||||
// - validates bad encoded data.
|
||||
// - validates good encoded data.
|
||||
// - validates insufficient data.
|
||||
testDecodeCases := []struct {
|
||||
enFn func([]byte, int, int) [][]byte
|
||||
shouldPass bool
|
||||
}{
|
||||
// Generates bad encoded data.
|
||||
{
|
||||
enFn: getBadEncodedData,
|
||||
shouldPass: false,
|
||||
},
|
||||
// Generates good encoded data.
|
||||
{
|
||||
enFn: getGoodEncodedData,
|
||||
shouldPass: true,
|
||||
},
|
||||
// Generates missing data.
|
||||
{
|
||||
enFn: getMissingData,
|
||||
shouldPass: false,
|
||||
},
|
||||
// Generates short data.
|
||||
{
|
||||
enFn: getInsufficientData,
|
||||
shouldPass: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Validates all decode tests.
|
||||
for i, testCase := range testDecodeCases {
|
||||
for _, encodingMatrix := range encodingMatrices {
|
||||
|
||||
// Encoding matrix.
|
||||
dataBlocks := encodingMatrix.dataBlocks
|
||||
parityBlocks := encodingMatrix.parityBlocks
|
||||
|
||||
// Data block size.
|
||||
blockSize := len(data)
|
||||
|
||||
// Generates encoded data based on type of testCase function.
|
||||
encodedData := testCase.enFn(data, dataBlocks, parityBlocks)
|
||||
|
||||
// Decodes the data.
|
||||
err := decodeData(encodedData, dataBlocks, parityBlocks)
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass by failed instead with %s", i+1, err)
|
||||
}
|
||||
|
||||
// Proceed to extract the data blocks.
|
||||
decodedDataWriter := new(bytes.Buffer)
|
||||
_, err = writeDataBlocks(decodedDataWriter, encodedData, dataBlocks, 0, int64(blockSize))
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass by failed instead with %s", i+1, err)
|
||||
}
|
||||
|
||||
// Validate if decoded data is what we expected.
|
||||
if bytes.Equal(decodedDataWriter.Bytes(), data) != testCase.shouldPass {
|
||||
err := errUnexpected
|
||||
t.Errorf("Test %d: Expected to pass by failed instead %s", i+1, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup for erasureCreateFile and erasureReadFile tests.
|
||||
type erasureTestSetup struct {
|
||||
dataBlocks int
|
||||
parityBlocks int
|
||||
blockSize int64
|
||||
diskPaths []string
|
||||
disks []StorageAPI
|
||||
}
|
||||
|
||||
// Removes the temporary disk directories.
|
||||
func (e erasureTestSetup) Remove() {
|
||||
for _, path := range e.diskPaths {
|
||||
removeAll(path)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an initialized setup for erasure tests.
|
||||
func newErasureTestSetup(dataBlocks int, parityBlocks int, blockSize int64) (*erasureTestSetup, error) {
|
||||
diskPaths := make([]string, dataBlocks+parityBlocks)
|
||||
disks := make([]StorageAPI, len(diskPaths))
|
||||
var err error
|
||||
for i := range diskPaths {
|
||||
disks[i], diskPaths[i], err = newPosixTestSetup()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = disks[i].MakeVol("testbucket")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &erasureTestSetup{dataBlocks, parityBlocks, blockSize, diskPaths, disks}, nil
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Global event notification queue. This is the queue that would be used to send all notifications.
|
||||
type eventNotifier struct {
|
||||
rwMutex *sync.RWMutex
|
||||
|
||||
// Collection of 'bucket' and notification config.
|
||||
notificationConfigs map[string]*notificationConfig
|
||||
snsTargets map[string][]chan []NotificationEvent
|
||||
queueTargets map[string]*logrus.Logger
|
||||
}
|
||||
|
||||
// Represents data to be sent with notification event.
|
||||
type eventData struct {
|
||||
Type EventName
|
||||
Bucket string
|
||||
ObjInfo ObjectInfo
|
||||
ReqParams map[string]string
|
||||
}
|
||||
|
||||
// New notification event constructs a new notification event message from
|
||||
// input request metadata which completed successfully.
|
||||
func newNotificationEvent(event eventData) NotificationEvent {
|
||||
/// Construct a new object created event.
|
||||
region := serverConfig.GetRegion()
|
||||
tnow := time.Now().UTC()
|
||||
sequencer := fmt.Sprintf("%X", tnow.UnixNano())
|
||||
// Following blocks fills in all the necessary details of s3 event message structure.
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/notification-content-structure.html
|
||||
nEvent := NotificationEvent{
|
||||
EventVersion: "2.0",
|
||||
EventSource: "aws:s3",
|
||||
AwsRegion: region,
|
||||
EventTime: tnow.Format(timeFormatAMZ),
|
||||
EventName: event.Type.String(),
|
||||
UserIdentity: defaultIdentity(),
|
||||
RequestParameters: event.ReqParams,
|
||||
ResponseElements: map[string]string{},
|
||||
S3: eventMeta{
|
||||
SchemaVersion: "1.0",
|
||||
ConfigurationID: "Config",
|
||||
Bucket: bucketMeta{
|
||||
Name: event.Bucket,
|
||||
OwnerIdentity: defaultIdentity(),
|
||||
ARN: "arn:aws:s3:::" + event.Bucket,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
escapedObj := url.QueryEscape(event.ObjInfo.Name)
|
||||
// For delete object event type, we do not need to set ETag and Size.
|
||||
if event.Type == ObjectRemovedDelete {
|
||||
nEvent.S3.Object = objectMeta{
|
||||
Key: escapedObj,
|
||||
Sequencer: sequencer,
|
||||
}
|
||||
return nEvent
|
||||
}
|
||||
// For all other events we should set ETag and Size.
|
||||
nEvent.S3.Object = objectMeta{
|
||||
Key: escapedObj,
|
||||
ETag: event.ObjInfo.MD5Sum,
|
||||
Size: event.ObjInfo.Size,
|
||||
Sequencer: sequencer,
|
||||
}
|
||||
// Success.
|
||||
return nEvent
|
||||
}
|
||||
|
||||
// Fetch the saved queue target.
|
||||
func (en eventNotifier) GetQueueTarget(queueARN string) *logrus.Logger {
|
||||
return en.queueTargets[queueARN]
|
||||
}
|
||||
|
||||
func (en eventNotifier) GetSNSTarget(snsARN string) []chan []NotificationEvent {
|
||||
en.rwMutex.RLock()
|
||||
defer en.rwMutex.RUnlock()
|
||||
return en.snsTargets[snsARN]
|
||||
}
|
||||
|
||||
// Set a new sns target for an input sns ARN.
|
||||
func (en *eventNotifier) SetSNSTarget(snsARN string, listenerCh chan []NotificationEvent) error {
|
||||
en.rwMutex.Lock()
|
||||
defer en.rwMutex.Unlock()
|
||||
if listenerCh == nil {
|
||||
return errors.New("invalid argument")
|
||||
}
|
||||
en.snsTargets[snsARN] = append(en.snsTargets[snsARN], listenerCh)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove sns target for an input sns ARN.
|
||||
func (en *eventNotifier) RemoveSNSTarget(snsARN string, listenerCh chan []NotificationEvent) {
|
||||
en.rwMutex.Lock()
|
||||
defer en.rwMutex.Unlock()
|
||||
snsTarget, ok := en.snsTargets[snsARN]
|
||||
if ok {
|
||||
for i, savedListenerCh := range snsTarget {
|
||||
if listenerCh == savedListenerCh {
|
||||
snsTarget = append(snsTarget[:i], snsTarget[i+1:]...)
|
||||
if len(snsTarget) == 0 {
|
||||
delete(en.snsTargets, snsARN)
|
||||
break
|
||||
}
|
||||
en.snsTargets[snsARN] = snsTarget
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if bucket notification is set for the bucket, false otherwise.
|
||||
func (en *eventNotifier) IsBucketNotificationSet(bucket string) bool {
|
||||
if en == nil {
|
||||
return false
|
||||
}
|
||||
en.rwMutex.RLock()
|
||||
defer en.rwMutex.RUnlock()
|
||||
_, ok := en.notificationConfigs[bucket]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Fetch bucket notification config for an input bucket.
|
||||
func (en eventNotifier) GetBucketNotificationConfig(bucket string) *notificationConfig {
|
||||
en.rwMutex.RLock()
|
||||
defer en.rwMutex.RUnlock()
|
||||
return en.notificationConfigs[bucket]
|
||||
}
|
||||
|
||||
// Set a new notification config for a bucket, this operation will overwrite any previous
|
||||
// notification configs for the bucket.
|
||||
func (en *eventNotifier) SetBucketNotificationConfig(bucket string, notificationCfg *notificationConfig) error {
|
||||
en.rwMutex.Lock()
|
||||
defer en.rwMutex.Unlock()
|
||||
if notificationCfg == nil {
|
||||
return errors.New("invalid argument")
|
||||
}
|
||||
en.notificationConfigs[bucket] = notificationCfg
|
||||
return nil
|
||||
}
|
||||
|
||||
// eventNotify notifies an event to relevant targets based on their
|
||||
// bucket notification configs.
|
||||
func eventNotify(event eventData) {
|
||||
// Notifies a new event.
|
||||
// List of events reported through this function are
|
||||
// - s3:ObjectCreated:Put
|
||||
// - s3:ObjectCreated:Post
|
||||
// - s3:ObjectCreated:Copy
|
||||
// - s3:ObjectCreated:CompleteMultipartUpload
|
||||
// - s3:ObjectRemoved:Delete
|
||||
|
||||
nConfig := eventN.GetBucketNotificationConfig(event.Bucket)
|
||||
// No bucket notifications enabled, drop the event notification.
|
||||
if len(nConfig.QueueConfigs) == 0 && len(nConfig.TopicConfigs) == 0 && len(nConfig.LambdaConfigs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Event type.
|
||||
eventType := event.Type.String()
|
||||
|
||||
// Object name.
|
||||
objectName := event.ObjInfo.Name
|
||||
|
||||
// Save the notification event to be sent.
|
||||
notificationEvent := []NotificationEvent{newNotificationEvent(event)}
|
||||
|
||||
// Validate if the event and object match the queue configs.
|
||||
for _, qConfig := range nConfig.QueueConfigs {
|
||||
eventMatch := eventMatch(eventType, qConfig.Events)
|
||||
ruleMatch := filterRuleMatch(objectName, qConfig.Filter.Key.FilterRules)
|
||||
if eventMatch && ruleMatch {
|
||||
targetLog := eventN.GetQueueTarget(qConfig.QueueARN)
|
||||
if targetLog != nil {
|
||||
targetLog.WithFields(logrus.Fields{
|
||||
"Records": notificationEvent,
|
||||
}).Info()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Validate if the event and object match the sns configs.
|
||||
for _, topicConfig := range nConfig.TopicConfigs {
|
||||
ruleMatch := filterRuleMatch(objectName, topicConfig.Filter.Key.FilterRules)
|
||||
eventMatch := eventMatch(eventType, topicConfig.Events)
|
||||
if eventMatch && ruleMatch {
|
||||
targetListeners := eventN.GetSNSTarget(topicConfig.TopicARN)
|
||||
for _, listener := range targetListeners {
|
||||
listener <- notificationEvent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loads notifcation config if any for a given bucket, returns back structured notification config.
|
||||
func loadNotificationConfig(bucket string, objAPI ObjectLayer) (*notificationConfig, error) {
|
||||
// Construct the notification config path.
|
||||
notificationConfigPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
||||
objInfo, err := objAPI.GetObjectInfo(minioMetaBucket, notificationConfigPath)
|
||||
if err != nil {
|
||||
// 'notification.xml' not found return 'errNoSuchNotifications'.
|
||||
// This is default when no bucket notifications are found on the bucket.
|
||||
switch err.(type) {
|
||||
case ObjectNotFound:
|
||||
return nil, errNoSuchNotifications
|
||||
}
|
||||
// Returns error for other errors.
|
||||
return nil, err
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
err = objAPI.GetObject(minioMetaBucket, notificationConfigPath, 0, objInfo.Size, &buffer)
|
||||
if err != nil {
|
||||
// 'notification.xml' not found return 'errNoSuchNotifications'.
|
||||
// This is default when no bucket notifications are found on the bucket.
|
||||
switch err.(type) {
|
||||
case ObjectNotFound:
|
||||
return nil, errNoSuchNotifications
|
||||
}
|
||||
// Returns error for other errors.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal notification bytes.
|
||||
notificationConfigBytes := buffer.Bytes()
|
||||
notificationCfg := ¬ificationConfig{}
|
||||
if err = xml.Unmarshal(notificationConfigBytes, ¬ificationCfg); err != nil {
|
||||
return nil, err
|
||||
} // Successfully marshalled notification configuration.
|
||||
|
||||
// Return success.
|
||||
return notificationCfg, nil
|
||||
}
|
||||
|
||||
// loads all bucket notifications if present.
|
||||
func loadAllBucketNotifications(objAPI ObjectLayer) (map[string]*notificationConfig, error) {
|
||||
// List buckets to proceed loading all notification configuration.
|
||||
buckets, err := objAPI.ListBuckets()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configs := make(map[string]*notificationConfig)
|
||||
|
||||
// Loads all bucket notifications.
|
||||
for _, bucket := range buckets {
|
||||
var nCfg *notificationConfig
|
||||
nCfg, err = loadNotificationConfig(bucket.Name, objAPI)
|
||||
if err != nil {
|
||||
if err == errNoSuchNotifications {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
configs[bucket.Name] = nCfg
|
||||
}
|
||||
|
||||
// Success.
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// Loads all queue targets, initializes each queueARNs depending on their config.
|
||||
// Each instance of queueARN registers its own logrus to communicate with the
|
||||
// queue service. QueueARN once initialized is not initialized again for the
|
||||
// same queueARN, instead previous connection is used.
|
||||
func loadAllQueueTargets() (map[string]*logrus.Logger, error) {
|
||||
queueTargets := make(map[string]*logrus.Logger)
|
||||
// Load all amqp targets, initialize their respective loggers.
|
||||
for accountID, amqpN := range serverConfig.GetAMQP() {
|
||||
if !amqpN.Enable {
|
||||
continue
|
||||
}
|
||||
// Construct the queue ARN for AMQP.
|
||||
queueARN := minioSqs + serverConfig.GetRegion() + ":" + accountID + ":" + queueTypeAMQP
|
||||
// Queue target if already initialized we move to the next ARN.
|
||||
_, ok := queueTargets[queueARN]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
// Using accountID we can now initialize a new AMQP logrus instance.
|
||||
amqpLog, err := newAMQPNotify(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queueTargets[queueARN] = amqpLog
|
||||
}
|
||||
// Load redis targets, initialize their respective loggers.
|
||||
for accountID, redisN := range serverConfig.GetRedis() {
|
||||
if !redisN.Enable {
|
||||
continue
|
||||
}
|
||||
// Construct the queue ARN for Redis.
|
||||
queueARN := minioSqs + serverConfig.GetRegion() + ":" + accountID + ":" + queueTypeRedis
|
||||
// Queue target if already initialized we move to the next ARN.
|
||||
_, ok := queueTargets[queueARN]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
// Using accountID we can now initialize a new Redis logrus instance.
|
||||
redisLog, err := newRedisNotify(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queueTargets[queueARN] = redisLog
|
||||
}
|
||||
// Load elastic targets, initialize their respective loggers.
|
||||
for accountID, elasticN := range serverConfig.GetElasticSearch() {
|
||||
if !elasticN.Enable {
|
||||
continue
|
||||
}
|
||||
// Construct the queue ARN for Elastic.
|
||||
queueARN := minioSqs + serverConfig.GetRegion() + ":" + accountID + ":" + queueTypeElastic
|
||||
_, ok := queueTargets[queueARN]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
// Using accountID we can now initialize a new ElasticSearch logrus instance.
|
||||
elasticLog, err := newElasticNotify(accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queueTargets[queueARN] = elasticLog
|
||||
}
|
||||
// Successfully initialized queue targets.
|
||||
return queueTargets, nil
|
||||
}
|
||||
|
||||
// Global instance of event notification queue.
|
||||
var eventN *eventNotifier
|
||||
|
||||
// Initialize event notifier.
|
||||
func initEventNotifier(objAPI ObjectLayer) error {
|
||||
if objAPI == nil {
|
||||
return errInvalidArgument
|
||||
}
|
||||
|
||||
// Read all saved bucket notifications.
|
||||
configs, err := loadAllBucketNotifications(objAPI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initializes all queue targets.
|
||||
queueTargets, err := loadAllQueueTargets()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Inititalize event notifier queue.
|
||||
eventN = &eventNotifier{
|
||||
rwMutex: &sync.RWMutex{},
|
||||
notificationConfigs: configs,
|
||||
queueTargets: queueTargets,
|
||||
snsTargets: make(map[string][]chan []NotificationEvent),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
// Tests various forms of inititalization of event notifier.
|
||||
func TestInitEventNotifier(t *testing.T) {
|
||||
fs, disk, err := getSingleNodeObjectLayer()
|
||||
if err != nil {
|
||||
t.Fatal("Unable to initialize FS backend.", err)
|
||||
}
|
||||
xl, disks, err := getXLObjectLayer()
|
||||
if err != nil {
|
||||
t.Fatal("Unable to initialize XL backend.", err)
|
||||
}
|
||||
|
||||
disks = append(disks, disk)
|
||||
for _, d := range disks {
|
||||
defer removeAll(d)
|
||||
}
|
||||
|
||||
// Collection of test cases for inititalizing event notifier.
|
||||
testCases := []struct {
|
||||
objAPI ObjectLayer
|
||||
configs map[string]*notificationConfig
|
||||
err error
|
||||
}{
|
||||
// Test 1 - invalid arguments.
|
||||
{
|
||||
objAPI: nil,
|
||||
err: errInvalidArgument,
|
||||
},
|
||||
// Test 2 - valid FS object layer but no bucket notifications.
|
||||
{
|
||||
objAPI: fs,
|
||||
err: nil,
|
||||
},
|
||||
// Test 3 - valid XL object layer but no bucket notifications.
|
||||
{
|
||||
objAPI: xl,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
// Validate if event notifier is properly initialized.
|
||||
for i, testCase := range testCases {
|
||||
err = initEventNotifier(testCase.objAPI)
|
||||
if err != testCase.err {
|
||||
t.Errorf("Test %d: Expected %s, but got: %s", i+1, testCase.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
// Simulates disk returning errFaultyDisk on all methods of StorageAPI
|
||||
// interface after successCount number of successes.
|
||||
type faultyDisk struct {
|
||||
disk *posix
|
||||
successCount int
|
||||
}
|
||||
|
||||
// instantiates a faulty
|
||||
func newFaultyDisk(disk *posix, n int) *faultyDisk {
|
||||
return &faultyDisk{disk: disk, successCount: n}
|
||||
}
|
||||
|
||||
func (f *faultyDisk) MakeVol(volume string) (err error) {
|
||||
if f.successCount > 0 {
|
||||
f.successCount--
|
||||
return f.disk.MakeVol(volume)
|
||||
}
|
||||
return errFaultyDisk
|
||||
}
|
||||
func (f *faultyDisk) ListVols() (vols []VolInfo, err error) {
|
||||
if f.successCount > 0 {
|
||||
f.successCount--
|
||||
return f.disk.ListVols()
|
||||
}
|
||||
return nil, errFaultyDisk
|
||||
}
|
||||
|
||||
func (f *faultyDisk) StatVol(volume string) (volInfo VolInfo, err error) {
|
||||
if f.successCount > 0 {
|
||||
f.successCount--
|
||||
return f.disk.StatVol(volume)
|
||||
}
|
||||
return VolInfo{}, errFaultyDisk
|
||||
}
|
||||
func (f *faultyDisk) DeleteVol(volume string) (err error) {
|
||||
if f.successCount > 0 {
|
||||
f.successCount--
|
||||
return f.disk.DeleteVol(volume)
|
||||
}
|
||||
return errFaultyDisk
|
||||
}
|
||||
|
||||
func (f *faultyDisk) ListDir(volume, path string) (entries []string, err error) {
|
||||
if f.successCount > 0 {
|
||||
f.successCount--
|
||||
return f.disk.ListDir(volume, path)
|
||||
}
|
||||
return []string{}, errFaultyDisk
|
||||
}
|
||||
|
||||
func (f *faultyDisk) ReadFile(volume string, path string, offset int64, buf []byte) (n int64, err error) {
|
||||
if f.successCount > 0 {
|
||||
f.successCount--
|
||||
return f.disk.ReadFile(volume, path, offset, buf)
|
||||
}
|
||||
return 0, errFaultyDisk
|
||||
}
|
||||
|
||||
func (f *faultyDisk) AppendFile(volume, path string, buf []byte) error {
|
||||
if f.successCount > 0 {
|
||||
f.successCount--
|
||||
return f.disk.AppendFile(volume, path, buf)
|
||||
}
|
||||
return errFaultyDisk
|
||||
}
|
||||
|
||||
func (f *faultyDisk) RenameFile(srcVolume, srcPath, dstVolume, dstPath string) error {
|
||||
if f.successCount > 0 {
|
||||
f.successCount--
|
||||
return f.disk.RenameFile(srcVolume, srcPath, dstVolume, dstPath)
|
||||
}
|
||||
return errFaultyDisk
|
||||
}
|
||||
|
||||
func (f *faultyDisk) StatFile(volume string, path string) (file FileInfo, err error) {
|
||||
if f.successCount > 0 {
|
||||
f.successCount--
|
||||
return f.disk.StatFile(volume, path)
|
||||
}
|
||||
return FileInfo{}, errFaultyDisk
|
||||
}
|
||||
|
||||
func (f *faultyDisk) DeleteFile(volume string, path string) (err error) {
|
||||
if f.successCount > 0 {
|
||||
f.successCount--
|
||||
return f.disk.DeleteFile(volume, path)
|
||||
}
|
||||
return errFaultyDisk
|
||||
}
|
||||
|
||||
func (f *faultyDisk) ReadAll(volume string, path string) (buf []byte, err error) {
|
||||
if f.successCount > 0 {
|
||||
f.successCount--
|
||||
return f.disk.ReadAll(volume, path)
|
||||
}
|
||||
return nil, errFaultyDisk
|
||||
}
|
||||
@@ -0,0 +1,875 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// fsFormat - structure holding 'fs' format.
|
||||
type fsFormat struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// xlFormat - structure holding 'xl' format.
|
||||
type xlFormat struct {
|
||||
Version string `json:"version"` // Version of 'xl' format.
|
||||
Disk string `json:"disk"` // Disk field carries assigned disk uuid.
|
||||
// JBOD field carries the input disk order generated the first
|
||||
// time when fresh disks were supplied.
|
||||
JBOD []string `json:"jbod"`
|
||||
}
|
||||
|
||||
// formatConfigV1 - structure holds format config version '1'.
|
||||
type formatConfigV1 struct {
|
||||
Version string `json:"version"` // Version of the format config.
|
||||
// Format indicates the backend format type, supports two values 'xl' and 'fs'.
|
||||
Format string `json:"format"`
|
||||
FS *fsFormat `json:"fs,omitempty"` // FS field holds fs format.
|
||||
XL *xlFormat `json:"xl,omitempty"` // XL field holds xl format.
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
All disks online
|
||||
-----------------
|
||||
- All Unformatted - format all and return success.
|
||||
- Some Unformatted - format all and return success.
|
||||
- Any JBOD inconsistent - return failure // Requires deep inspection, phase2.
|
||||
- Some are corrupt (missing format.json) - return failure // Requires deep inspection, phase2.
|
||||
- Any unrecognized disks - return failure
|
||||
|
||||
Some disks are offline and we have quorum.
|
||||
-----------------
|
||||
- Some unformatted - no heal, return success.
|
||||
- Any JBOD inconsistent - return failure // Requires deep inspection, phase2.
|
||||
- Some are corrupt (missing format.json) - return failure // Requires deep inspection, phase2.
|
||||
- Any unrecognized disks - return failure
|
||||
|
||||
No read quorum
|
||||
-----------------
|
||||
failure for all cases.
|
||||
|
||||
// Pseudo code for managing `format.json`.
|
||||
|
||||
// Generic checks.
|
||||
if (no quorum) return error
|
||||
if (any disk is corrupt) return error // phase2
|
||||
if (jbod inconsistent) return error // phase2
|
||||
if (disks not recognized) // Always error.
|
||||
|
||||
// Specific checks.
|
||||
if (all disks online)
|
||||
if (all disks return format.json)
|
||||
if (jbod consistent)
|
||||
if (all disks recognized)
|
||||
return
|
||||
else
|
||||
if (all disks return format.json not found)
|
||||
(initialize format)
|
||||
return
|
||||
else (some disks return format.json not found)
|
||||
(heal format)
|
||||
return
|
||||
fi
|
||||
fi
|
||||
else // No healing at this point forward, some disks are offline or dead.
|
||||
if (some disks return format.json not found)
|
||||
if (with force)
|
||||
// Offline disks are marked as dead.
|
||||
(heal format) // Offline disks should be marked as dead.
|
||||
return success
|
||||
else (without force)
|
||||
// --force is necessary to heal few drives, because some drives
|
||||
// are offline. Offline disks will be marked as dead.
|
||||
return error
|
||||
fi
|
||||
fi
|
||||
*/
|
||||
|
||||
// error returned when some disks are found to be unformatted.
|
||||
var errSomeDiskUnformatted = errors.New("some disks are found to be unformatted")
|
||||
|
||||
// error returned when some disks are offline.
|
||||
var errSomeDiskOffline = errors.New("some disks are offline")
|
||||
|
||||
// errDiskOrderMismatch - returned when disk UUID is not in consistent JBOD order.
|
||||
var errDiskOrderMismatch = errors.New("disk order mismatch")
|
||||
|
||||
// Returns error slice into understandable errors.
|
||||
func reduceFormatErrs(errs []error, diskCount int) (err error) {
|
||||
var errUnformattedDiskCount = 0
|
||||
var errDiskNotFoundCount = 0
|
||||
var errCorruptedFormatCount = 0
|
||||
for _, dErr := range errs {
|
||||
if dErr == errUnformattedDisk {
|
||||
errUnformattedDiskCount++
|
||||
} else if dErr == errDiskNotFound {
|
||||
errDiskNotFoundCount++
|
||||
} else if dErr == errCorruptedFormat {
|
||||
errCorruptedFormatCount++
|
||||
}
|
||||
}
|
||||
if errCorruptedFormatCount > 0 {
|
||||
return errCorruptedFormat
|
||||
}
|
||||
// Unformatted disks found, we need to figure out if any disks are offline.
|
||||
if errUnformattedDiskCount > 0 {
|
||||
// Returns errUnformattedDisk if all disks report unFormattedDisk.
|
||||
if errUnformattedDiskCount < diskCount {
|
||||
if errDiskNotFoundCount > 0 {
|
||||
// Only some disks are fresh but some disks are offline as well.
|
||||
return errSomeDiskOffline
|
||||
}
|
||||
// Some disks are fresh disks an unformatted, not disks are offline.
|
||||
return errSomeDiskUnformatted
|
||||
}
|
||||
// All disks returned unformatted, all disks must be fresh.
|
||||
return errUnformattedDisk
|
||||
}
|
||||
// No unformatted disks found no need to handle disk not found case, return success here.
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadAllFormats - load all format config from all input disks in parallel.
|
||||
func loadAllFormats(bootstrapDisks []StorageAPI) ([]*formatConfigV1, []error) {
|
||||
// Initialize sync waitgroup.
|
||||
var wg = &sync.WaitGroup{}
|
||||
|
||||
// Initialize list of errors.
|
||||
var sErrs = make([]error, len(bootstrapDisks))
|
||||
|
||||
// Initialize format configs.
|
||||
var formatConfigs = make([]*formatConfigV1, len(bootstrapDisks))
|
||||
|
||||
// Make a volume entry on all underlying storage disks.
|
||||
for index, disk := range bootstrapDisks {
|
||||
if disk == nil {
|
||||
sErrs[index] = errDiskNotFound
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
// Make a volume inside a go-routine.
|
||||
go func(index int, disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
formatConfig, lErr := loadFormat(disk)
|
||||
if lErr != nil {
|
||||
sErrs[index] = lErr
|
||||
return
|
||||
}
|
||||
formatConfigs[index] = formatConfig
|
||||
}(index, disk)
|
||||
}
|
||||
|
||||
// Wait for all make vol to finish.
|
||||
wg.Wait()
|
||||
|
||||
for _, err := range sErrs {
|
||||
if err != nil {
|
||||
// Return all formats and errors.
|
||||
return formatConfigs, sErrs
|
||||
}
|
||||
}
|
||||
// Return all formats and nil
|
||||
return formatConfigs, nil
|
||||
}
|
||||
|
||||
// genericFormatCheck - validates and returns error.
|
||||
// if (no quorum) return error
|
||||
// if (any disk is corrupt) return error // phase2
|
||||
// if (jbod inconsistent) return error // phase2
|
||||
// if (disks not recognized) // Always error.
|
||||
func genericFormatCheck(formatConfigs []*formatConfigV1, sErrs []error) (err error) {
|
||||
// Calculate the errors.
|
||||
var (
|
||||
errCorruptFormatCount = 0
|
||||
errCount = 0
|
||||
)
|
||||
|
||||
// Through all errors calculate the actual errors.
|
||||
for _, lErr := range sErrs {
|
||||
if lErr == nil {
|
||||
continue
|
||||
}
|
||||
// These errors are good conditions, means disk is online.
|
||||
if lErr == errUnformattedDisk || lErr == errVolumeNotFound {
|
||||
continue
|
||||
}
|
||||
if lErr == errCorruptedFormat {
|
||||
errCorruptFormatCount++
|
||||
} else {
|
||||
errCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate read quorum.
|
||||
readQuorum := len(formatConfigs)/2 + 1
|
||||
|
||||
// Validate the err count under tolerant limit.
|
||||
if errCount > len(formatConfigs)-readQuorum {
|
||||
return errXLReadQuorum
|
||||
}
|
||||
|
||||
// Check if number of corrupted format under quorum
|
||||
if errCorruptFormatCount > len(formatConfigs)-readQuorum {
|
||||
return errCorruptedFormat
|
||||
}
|
||||
|
||||
// Validates if format and JBOD are consistent across all disks.
|
||||
if err = checkFormatXL(formatConfigs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Success..
|
||||
return nil
|
||||
}
|
||||
|
||||
// isSavedUUIDInOrder - validates if disk uuid is present and valid in all
|
||||
// available format config JBOD. This function also validates if the disk UUID
|
||||
// is always available on all JBOD under the same order.
|
||||
func isSavedUUIDInOrder(uuid string, formatConfigs []*formatConfigV1) bool {
|
||||
var orderIndexes []int
|
||||
// Validate each for format.json for relevant uuid.
|
||||
for _, formatConfig := range formatConfigs {
|
||||
if formatConfig == nil {
|
||||
continue
|
||||
}
|
||||
// Validate if UUID is present in JBOD.
|
||||
uuidIndex := findDiskIndex(uuid, formatConfig.XL.JBOD)
|
||||
if uuidIndex == -1 {
|
||||
// UUID not found.
|
||||
errorIf(errDiskNotFound, "Disk %s not found in JBOD list", uuid)
|
||||
return false
|
||||
}
|
||||
// Save the position of UUID present in JBOD.
|
||||
orderIndexes = append(orderIndexes, uuidIndex+1)
|
||||
}
|
||||
// Once uuid is found, verify if the uuid
|
||||
// present in same order across all format configs.
|
||||
prevOrderIndex := orderIndexes[0]
|
||||
for _, orderIndex := range orderIndexes {
|
||||
if prevOrderIndex != orderIndex {
|
||||
errorIf(errDiskOrderMismatch, "Disk %s is in wrong order wanted %d, saw %d ", uuid, prevOrderIndex, orderIndex)
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Returns success, when we have verified if uuid
|
||||
// is consistent and valid across all format configs.
|
||||
return true
|
||||
}
|
||||
|
||||
// checkDisksConsistency - checks if all disks are consistent with all JBOD entries on all disks.
|
||||
func checkDisksConsistency(formatConfigs []*formatConfigV1) error {
|
||||
var disks = make([]string, len(formatConfigs))
|
||||
// Collect currently available disk uuids.
|
||||
for index, formatConfig := range formatConfigs {
|
||||
if formatConfig == nil {
|
||||
disks[index] = ""
|
||||
continue
|
||||
}
|
||||
disks[index] = formatConfig.XL.Disk
|
||||
}
|
||||
// Validate collected uuids and verify JBOD.
|
||||
for _, uuid := range disks {
|
||||
if uuid == "" {
|
||||
continue
|
||||
}
|
||||
// Is uuid present on all JBOD ?.
|
||||
if !isSavedUUIDInOrder(uuid, formatConfigs) {
|
||||
return fmt.Errorf("%s disk not found in JBOD", uuid)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkJBODConsistency - validate xl jbod order if they are consistent.
|
||||
func checkJBODConsistency(formatConfigs []*formatConfigV1) error {
|
||||
var sentinelJBOD []string
|
||||
// Extract first valid JBOD.
|
||||
for _, format := range formatConfigs {
|
||||
if format == nil {
|
||||
continue
|
||||
}
|
||||
sentinelJBOD = format.XL.JBOD
|
||||
break
|
||||
}
|
||||
for _, format := range formatConfigs {
|
||||
if format == nil {
|
||||
continue
|
||||
}
|
||||
currentJBOD := format.XL.JBOD
|
||||
if !reflect.DeepEqual(sentinelJBOD, currentJBOD) {
|
||||
return errors.New("Inconsistent JBOD found.")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findDiskIndex returns position of disk in JBOD.
|
||||
func findDiskIndex(disk string, jbod []string) int {
|
||||
for index, uuid := range jbod {
|
||||
if uuid == disk {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// reorderDisks - reorder disks in JBOD order.
|
||||
func reorderDisks(bootstrapDisks []StorageAPI, formatConfigs []*formatConfigV1) ([]StorageAPI, error) {
|
||||
var savedJBOD []string
|
||||
for _, format := range formatConfigs {
|
||||
if format == nil {
|
||||
continue
|
||||
}
|
||||
savedJBOD = format.XL.JBOD
|
||||
break
|
||||
}
|
||||
// Pick the first JBOD list to verify the order and construct new set of disk slice.
|
||||
var newDisks = make([]StorageAPI, len(bootstrapDisks))
|
||||
for fIndex, format := range formatConfigs {
|
||||
if format == nil {
|
||||
continue
|
||||
}
|
||||
jIndex := findDiskIndex(format.XL.Disk, savedJBOD)
|
||||
if jIndex == -1 {
|
||||
return nil, errors.New("Unrecognized uuid " + format.XL.Disk + " found")
|
||||
}
|
||||
newDisks[jIndex] = bootstrapDisks[fIndex]
|
||||
}
|
||||
return newDisks, nil
|
||||
}
|
||||
|
||||
// loadFormat - loads format.json from disk.
|
||||
func loadFormat(disk StorageAPI) (format *formatConfigV1, err error) {
|
||||
buf, err := disk.ReadAll(minioMetaBucket, formatConfigFile)
|
||||
if err != nil {
|
||||
// 'file not found' and 'volume not found' as
|
||||
// same. 'volume not found' usually means its a fresh disk.
|
||||
if err == errFileNotFound || err == errVolumeNotFound {
|
||||
var vols []VolInfo
|
||||
vols, err = disk.ListVols()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(vols) > 1 {
|
||||
// 'format.json' not found, but we found user data.
|
||||
return nil, errCorruptedFormat
|
||||
}
|
||||
// No other data found, its a fresh disk.
|
||||
return nil, errUnformattedDisk
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Try to decode format json into formatConfigV1 struct.
|
||||
format = &formatConfigV1{}
|
||||
if err = json.Unmarshal(buf, format); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Success.
|
||||
return format, nil
|
||||
}
|
||||
|
||||
// isFormatNotFound - returns true if all `format.json` are not
|
||||
// found on all disks.
|
||||
func isFormatNotFound(formats []*formatConfigV1) bool {
|
||||
for _, format := range formats {
|
||||
// One of the `format.json` is found.
|
||||
if format != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// All format.json missing, success.
|
||||
return true
|
||||
}
|
||||
|
||||
// isFormatFound - returns true if all input formats are found on
|
||||
// all disks.
|
||||
func isFormatFound(formats []*formatConfigV1) bool {
|
||||
for _, format := range formats {
|
||||
// One of `format.json` is not found.
|
||||
if format == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// All format.json present, success.
|
||||
return true
|
||||
}
|
||||
|
||||
// Heals any missing format.json on the drives. Returns error only for unexpected errors
|
||||
// as regular errors can be ignored since there might be enough quorum to be operational.
|
||||
// Heals only fresh disks.
|
||||
func healFormatXLFreshDisks(storageDisks []StorageAPI) error {
|
||||
formatConfigs := make([]*formatConfigV1, len(storageDisks))
|
||||
var referenceConfig *formatConfigV1
|
||||
// Loads `format.json` from all disks.
|
||||
for index, disk := range storageDisks {
|
||||
// Disk not found or ignored is a valid case.
|
||||
if disk == nil {
|
||||
// Return nil, one of the disk is offline.
|
||||
return nil
|
||||
}
|
||||
formatXL, err := loadFormat(disk)
|
||||
if err != nil {
|
||||
if err == errUnformattedDisk {
|
||||
// format.json is missing, should be healed.
|
||||
continue
|
||||
} else if err == errDiskNotFound { // Is a valid case we
|
||||
// can proceed without healing.
|
||||
return nil
|
||||
}
|
||||
// Return error for unsupported errors.
|
||||
return err
|
||||
} // Success.
|
||||
formatConfigs[index] = formatXL
|
||||
}
|
||||
|
||||
// All `format.json` has been read successfully, previously completed.
|
||||
if isFormatFound(formatConfigs) {
|
||||
// Return success.
|
||||
return nil
|
||||
}
|
||||
|
||||
// All disks are fresh, format.json will be written by initFormatXL()
|
||||
if isFormatNotFound(formatConfigs) {
|
||||
return initFormatXL(storageDisks)
|
||||
}
|
||||
|
||||
// Validate format configs for consistency in JBOD and disks.
|
||||
if err := checkFormatXL(formatConfigs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if referenceConfig == nil {
|
||||
// This config will be used to update the drives missing format.json.
|
||||
for _, formatConfig := range formatConfigs {
|
||||
if formatConfig == nil {
|
||||
continue
|
||||
}
|
||||
referenceConfig = formatConfig
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Collect new JBOD.
|
||||
newJBOD := referenceConfig.XL.JBOD
|
||||
|
||||
// Reorder the disks based on the JBOD order.
|
||||
orderedDisks, err := reorderDisks(storageDisks, formatConfigs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// From ordered disks fill the UUID position.
|
||||
for index, disk := range orderedDisks {
|
||||
if disk == nil {
|
||||
newJBOD[index] = getUUID()
|
||||
}
|
||||
}
|
||||
|
||||
// Collect new format configs.
|
||||
var newFormatConfigs = make([]*formatConfigV1, len(orderedDisks))
|
||||
|
||||
// Collect new format configs that need to be written.
|
||||
for index := range orderedDisks {
|
||||
// New configs are generated since we are going
|
||||
// to re-populate across all disks.
|
||||
config := &formatConfigV1{
|
||||
Version: referenceConfig.Version,
|
||||
Format: referenceConfig.Format,
|
||||
XL: &xlFormat{
|
||||
Version: referenceConfig.XL.Version,
|
||||
Disk: newJBOD[index],
|
||||
JBOD: newJBOD,
|
||||
},
|
||||
}
|
||||
newFormatConfigs[index] = config
|
||||
}
|
||||
|
||||
// Fill in the missing disk back from format configs.
|
||||
// We need to make sure we have kept the previous order
|
||||
// and allowed fresh disks to be arranged anywhere.
|
||||
// Following block facilitates to put fresh disks.
|
||||
for index, format := range formatConfigs {
|
||||
// Format is missing so we go through ordered disks.
|
||||
if format == nil {
|
||||
// At this point when disk is missing the fresh disk
|
||||
// in the stack get it back from storageDisks.
|
||||
for oIndex, disk := range orderedDisks {
|
||||
if disk == nil {
|
||||
orderedDisks[oIndex] = storageDisks[index]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save new `format.json` across all disks, in JBOD order.
|
||||
return saveFormatXL(orderedDisks, newFormatConfigs)
|
||||
}
|
||||
|
||||
// Disks from storageDiks are put in assignedDisks if found in orderedDisks and in unAssignedDisks otherwise
|
||||
func splitDisksByUse(storageDisks, orderedDisks []StorageAPI) (assignedDisks []StorageAPI, unAssignedDisks []StorageAPI) {
|
||||
// Populate unAssignDisks
|
||||
for i := range storageDisks {
|
||||
found := false
|
||||
for j := range orderedDisks {
|
||||
if storageDisks[i] == orderedDisks[j] {
|
||||
found = true
|
||||
assignedDisks = append(assignedDisks, storageDisks[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
unAssignedDisks = append(unAssignedDisks, storageDisks[i])
|
||||
}
|
||||
}
|
||||
return assignedDisks, unAssignedDisks
|
||||
}
|
||||
|
||||
// Inspect the content of all disks to guess the right order according to the format files.
|
||||
// The right order is represented in orderedDisks
|
||||
func reorderDisksByInspection(orderedDisks, storageDisks []StorageAPI, formatConfigs []*formatConfigV1) ([]StorageAPI, error) {
|
||||
for index, format := range formatConfigs {
|
||||
if format != nil {
|
||||
continue
|
||||
}
|
||||
vols, err := storageDisks[index].ListVols()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(vols) == 0 {
|
||||
continue
|
||||
}
|
||||
volName := ""
|
||||
// Avoid picking minioMetaBucket because ListVols() returns a non ordered list
|
||||
for i := range vols {
|
||||
if vols[i].Name != minioMetaBucket {
|
||||
volName = vols[i].Name
|
||||
break
|
||||
}
|
||||
}
|
||||
if volName == "" {
|
||||
continue
|
||||
}
|
||||
objects, err := storageDisks[index].ListDir(volName, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(objects) == 0 {
|
||||
continue
|
||||
}
|
||||
xlData, err := readXLMeta(storageDisks[index], volName, objects[0])
|
||||
if err != nil {
|
||||
if err == errFileNotFound {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
diskIndex := -1
|
||||
for i, d := range xlData.Erasure.Distribution {
|
||||
if d == xlData.Erasure.Index {
|
||||
diskIndex = i
|
||||
}
|
||||
}
|
||||
// Check for found results
|
||||
if diskIndex == -1 || orderedDisks[diskIndex] != nil {
|
||||
// Some inconsistent data are found, exit immediately.
|
||||
return nil, errCorruptedFormat
|
||||
}
|
||||
orderedDisks[diskIndex] = storageDisks[index]
|
||||
}
|
||||
return orderedDisks, nil
|
||||
}
|
||||
|
||||
// Heals corrupted format json in all disks
|
||||
func healFormatXLCorruptedDisks(storageDisks []StorageAPI) error {
|
||||
formatConfigs := make([]*formatConfigV1, len(storageDisks))
|
||||
var referenceConfig *formatConfigV1
|
||||
|
||||
// Loads `format.json` from all disks.
|
||||
for index, disk := range storageDisks {
|
||||
// Disk not found or ignored is a valid case.
|
||||
if disk == nil {
|
||||
// Return nil, one of the disk is offline.
|
||||
return nil
|
||||
}
|
||||
formatXL, err := loadFormat(disk)
|
||||
if err != nil {
|
||||
if err == errUnformattedDisk || err == errCorruptedFormat {
|
||||
// format.json is missing or corrupted, should be healed.
|
||||
continue
|
||||
} else if err == errDiskNotFound { // Is a valid case we
|
||||
// can proceed without healing.
|
||||
return nil
|
||||
}
|
||||
// Return error for unsupported errors.
|
||||
return err
|
||||
} // Success.
|
||||
formatConfigs[index] = formatXL
|
||||
}
|
||||
|
||||
// All `format.json` has been read successfully, previously completed.
|
||||
if isFormatFound(formatConfigs) {
|
||||
// Return success.
|
||||
return nil
|
||||
}
|
||||
|
||||
// All disks are fresh, format.json will be written by initFormatXL()
|
||||
if isFormatNotFound(formatConfigs) {
|
||||
return initFormatXL(storageDisks)
|
||||
}
|
||||
|
||||
// Validate format configs for consistency in JBOD and disks.
|
||||
if err := checkFormatXL(formatConfigs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if referenceConfig == nil {
|
||||
// This config will be used to update the drives missing format.json.
|
||||
for _, formatConfig := range formatConfigs {
|
||||
if formatConfig == nil {
|
||||
continue
|
||||
}
|
||||
referenceConfig = formatConfig
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Collect new JBOD.
|
||||
newJBOD := referenceConfig.XL.JBOD
|
||||
|
||||
// Reorder the disks based on the JBOD order.
|
||||
orderedDisks, err := reorderDisks(storageDisks, formatConfigs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// From ordered disks fill the UUID position.
|
||||
for index, disk := range orderedDisks {
|
||||
if disk == nil {
|
||||
newJBOD[index] = getUUID()
|
||||
}
|
||||
}
|
||||
|
||||
// For disks with corrupted formats, inspect the disks contents to guess the disks order
|
||||
orderedDisks, err = reorderDisksByInspection(orderedDisks, storageDisks, formatConfigs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// At this stage, all disks with corrupted formats but with objects inside found their way.
|
||||
// Now take care of unformatted disks, which are the `unAssignedDisks`
|
||||
_, unAssignedDisks := splitDisksByUse(storageDisks, orderedDisks)
|
||||
|
||||
// Assign unassigned disks to nil elements in orderedDisks
|
||||
for i, disk := range orderedDisks {
|
||||
if disk == nil && len(unAssignedDisks) > 0 {
|
||||
orderedDisks[i] = unAssignedDisks[0]
|
||||
unAssignedDisks = unAssignedDisks[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Collect new format configs.
|
||||
var newFormatConfigs = make([]*formatConfigV1, len(orderedDisks))
|
||||
|
||||
// Collect new format configs that need to be written.
|
||||
for index := range orderedDisks {
|
||||
// New configs are generated since we are going
|
||||
// to re-populate across all disks.
|
||||
config := &formatConfigV1{
|
||||
Version: referenceConfig.Version,
|
||||
Format: referenceConfig.Format,
|
||||
XL: &xlFormat{
|
||||
Version: referenceConfig.XL.Version,
|
||||
Disk: newJBOD[index],
|
||||
JBOD: newJBOD,
|
||||
},
|
||||
}
|
||||
newFormatConfigs[index] = config
|
||||
}
|
||||
|
||||
// Save new `format.json` across all disks, in JBOD order.
|
||||
return saveFormatXL(orderedDisks, newFormatConfigs)
|
||||
}
|
||||
|
||||
// loadFormatXL - loads XL `format.json` and returns back properly
|
||||
// ordered storage slice based on `format.json`.
|
||||
func loadFormatXL(bootstrapDisks []StorageAPI) (disks []StorageAPI, err error) {
|
||||
var unformattedDisksFoundCnt = 0
|
||||
var diskNotFoundCount = 0
|
||||
formatConfigs := make([]*formatConfigV1, len(bootstrapDisks))
|
||||
|
||||
// Try to load `format.json` bootstrap disks.
|
||||
for index, disk := range bootstrapDisks {
|
||||
if disk == nil {
|
||||
diskNotFoundCount++
|
||||
continue
|
||||
}
|
||||
var formatXL *formatConfigV1
|
||||
formatXL, err = loadFormat(disk)
|
||||
if err != nil {
|
||||
if err == errUnformattedDisk {
|
||||
unformattedDisksFoundCnt++
|
||||
continue
|
||||
} else if err == errDiskNotFound {
|
||||
diskNotFoundCount++
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// Save valid formats.
|
||||
formatConfigs[index] = formatXL
|
||||
}
|
||||
|
||||
// If all disks indicate that 'format.json' is not available return 'errUnformattedDisk'.
|
||||
if unformattedDisksFoundCnt > len(bootstrapDisks)-(len(bootstrapDisks)/2+1) {
|
||||
return nil, errUnformattedDisk
|
||||
} else if diskNotFoundCount == len(bootstrapDisks) {
|
||||
return nil, errDiskNotFound
|
||||
} else if diskNotFoundCount > len(bootstrapDisks)-(len(bootstrapDisks)/2+1) {
|
||||
return nil, errXLReadQuorum
|
||||
}
|
||||
|
||||
// Validate the format configs read are correct.
|
||||
if err = checkFormatXL(formatConfigs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Erasure code requires disks to be presented in the same order each time.
|
||||
return reorderDisks(bootstrapDisks, formatConfigs)
|
||||
}
|
||||
|
||||
// checkFormatXL - verifies if format.json format is intact.
|
||||
func checkFormatXL(formatConfigs []*formatConfigV1) error {
|
||||
for _, formatXL := range formatConfigs {
|
||||
if formatXL == nil {
|
||||
continue
|
||||
}
|
||||
// Validate format version and format type.
|
||||
if formatXL.Version != "1" {
|
||||
return fmt.Errorf("Unsupported version of backend format [%s] found.", formatXL.Version)
|
||||
}
|
||||
if formatXL.Format != "xl" {
|
||||
return fmt.Errorf("Unsupported backend format [%s] found.", formatXL.Format)
|
||||
}
|
||||
if formatXL.XL.Version != "1" {
|
||||
return fmt.Errorf("Unsupported XL backend format found [%s]", formatXL.XL.Version)
|
||||
}
|
||||
if len(formatConfigs) != len(formatXL.XL.JBOD) {
|
||||
return fmt.Errorf("Number of disks %d did not match the backend format %d", len(formatConfigs), len(formatXL.XL.JBOD))
|
||||
}
|
||||
}
|
||||
if err := checkJBODConsistency(formatConfigs); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkDisksConsistency(formatConfigs)
|
||||
}
|
||||
|
||||
// saveFormatXL - populates `format.json` on disks in its order.
|
||||
func saveFormatXL(storageDisks []StorageAPI, formats []*formatConfigV1) error {
|
||||
var errs = make([]error, len(storageDisks))
|
||||
var wg = &sync.WaitGroup{}
|
||||
// Write `format.json` to all disks.
|
||||
for index, disk := range storageDisks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(index int, disk StorageAPI, format *formatConfigV1) {
|
||||
defer wg.Done()
|
||||
|
||||
// Marshal and write to disk.
|
||||
formatBytes, err := json.Marshal(format)
|
||||
if err != nil {
|
||||
errs[index] = err
|
||||
return
|
||||
}
|
||||
|
||||
// Purge any existing temporary file, okay to ignore errors here.
|
||||
disk.DeleteFile(minioMetaBucket, formatConfigFileTmp)
|
||||
|
||||
// Append file `format.json.tmp`.
|
||||
if err = disk.AppendFile(minioMetaBucket, formatConfigFileTmp, formatBytes); err != nil {
|
||||
errs[index] = err
|
||||
return
|
||||
}
|
||||
// Rename file `format.json.tmp` --> `format.json`.
|
||||
if err = disk.RenameFile(minioMetaBucket, formatConfigFileTmp, minioMetaBucket, formatConfigFile); err != nil {
|
||||
errs[index] = err
|
||||
return
|
||||
}
|
||||
}(index, disk, formats[index])
|
||||
}
|
||||
|
||||
// Wait for the routines to finish.
|
||||
wg.Wait()
|
||||
|
||||
// Validate if we encountered any errors, return quickly.
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
// Failure.
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Success.
|
||||
return nil
|
||||
}
|
||||
|
||||
// initFormatXL - save XL format configuration on all disks.
|
||||
func initFormatXL(storageDisks []StorageAPI) (err error) {
|
||||
// Initialize jbods.
|
||||
var jbod = make([]string, len(storageDisks))
|
||||
|
||||
// Initialize formats.
|
||||
var formats = make([]*formatConfigV1, len(storageDisks))
|
||||
|
||||
// Initialize `format.json`.
|
||||
for index, disk := range storageDisks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
// Allocate format config.
|
||||
formats[index] = &formatConfigV1{
|
||||
Version: "1",
|
||||
Format: "xl",
|
||||
XL: &xlFormat{
|
||||
Version: "1",
|
||||
Disk: getUUID(),
|
||||
},
|
||||
}
|
||||
jbod[index] = formats[index].XL.Disk
|
||||
}
|
||||
|
||||
// Update the jbod entries.
|
||||
for index, disk := range storageDisks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
// Save jbod.
|
||||
formats[index].XL.JBOD = jbod
|
||||
}
|
||||
|
||||
// Save formats `format.json` across all disks.
|
||||
return saveFormatXL(storageDisks, formats)
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// generates a valid format.json for XL backend.
|
||||
func genFormatXLValid() []*formatConfigV1 {
|
||||
jbod := make([]string, 8)
|
||||
formatConfigs := make([]*formatConfigV1, 8)
|
||||
for index := range jbod {
|
||||
jbod[index] = getUUID()
|
||||
}
|
||||
for index := range jbod {
|
||||
formatConfigs[index] = &formatConfigV1{
|
||||
Version: "1",
|
||||
Format: "xl",
|
||||
XL: &xlFormat{
|
||||
Version: "1",
|
||||
Disk: jbod[index],
|
||||
JBOD: jbod,
|
||||
},
|
||||
}
|
||||
}
|
||||
return formatConfigs
|
||||
}
|
||||
|
||||
// generates a invalid format.json version for XL backend.
|
||||
func genFormatXLInvalidVersion() []*formatConfigV1 {
|
||||
jbod := make([]string, 8)
|
||||
formatConfigs := make([]*formatConfigV1, 8)
|
||||
for index := range jbod {
|
||||
jbod[index] = getUUID()
|
||||
}
|
||||
for index := range jbod {
|
||||
formatConfigs[index] = &formatConfigV1{
|
||||
Version: "1",
|
||||
Format: "xl",
|
||||
XL: &xlFormat{
|
||||
Version: "1",
|
||||
Disk: jbod[index],
|
||||
JBOD: jbod,
|
||||
},
|
||||
}
|
||||
}
|
||||
// Corrupt version numbers.
|
||||
formatConfigs[0].Version = "2"
|
||||
formatConfigs[3].Version = "-1"
|
||||
return formatConfigs
|
||||
}
|
||||
|
||||
// generates a invalid format.json JBOD for XL backend.
|
||||
func genFormatXLInvalidJBOD() []*formatConfigV1 {
|
||||
jbod := make([]string, 8)
|
||||
formatConfigs := make([]*formatConfigV1, 8)
|
||||
for index := range jbod {
|
||||
jbod[index] = getUUID()
|
||||
}
|
||||
for index := range jbod {
|
||||
formatConfigs[index] = &formatConfigV1{
|
||||
Version: "1",
|
||||
Format: "xl",
|
||||
XL: &xlFormat{
|
||||
Version: "1",
|
||||
Disk: jbod[index],
|
||||
JBOD: jbod,
|
||||
},
|
||||
}
|
||||
}
|
||||
for index := range jbod {
|
||||
jbod[index] = getUUID()
|
||||
}
|
||||
// Corrupt JBOD entries on disk 6 and disk 8.
|
||||
formatConfigs[5].XL.JBOD = jbod
|
||||
formatConfigs[7].XL.JBOD = jbod
|
||||
return formatConfigs
|
||||
}
|
||||
|
||||
// generates a invalid format.json Disk UUID for XL backend.
|
||||
func genFormatXLInvalidDisks() []*formatConfigV1 {
|
||||
jbod := make([]string, 8)
|
||||
formatConfigs := make([]*formatConfigV1, 8)
|
||||
for index := range jbod {
|
||||
jbod[index] = getUUID()
|
||||
}
|
||||
for index := range jbod {
|
||||
formatConfigs[index] = &formatConfigV1{
|
||||
Version: "1",
|
||||
Format: "xl",
|
||||
XL: &xlFormat{
|
||||
Version: "1",
|
||||
Disk: jbod[index],
|
||||
JBOD: jbod,
|
||||
},
|
||||
}
|
||||
}
|
||||
// Make disk 5 and disk 8 have inconsistent disk uuid's.
|
||||
formatConfigs[4].XL.Disk = getUUID()
|
||||
formatConfigs[7].XL.Disk = getUUID()
|
||||
return formatConfigs
|
||||
}
|
||||
|
||||
// generates a invalid format.json Disk UUID in wrong order for XL backend.
|
||||
func genFormatXLInvalidDisksOrder() []*formatConfigV1 {
|
||||
jbod := make([]string, 8)
|
||||
formatConfigs := make([]*formatConfigV1, 8)
|
||||
for index := range jbod {
|
||||
jbod[index] = getUUID()
|
||||
}
|
||||
for index := range jbod {
|
||||
formatConfigs[index] = &formatConfigV1{
|
||||
Version: "1",
|
||||
Format: "xl",
|
||||
XL: &xlFormat{
|
||||
Version: "1",
|
||||
Disk: jbod[index],
|
||||
JBOD: jbod,
|
||||
},
|
||||
}
|
||||
}
|
||||
// Re order jbod for failure case.
|
||||
var jbod1 = make([]string, 8)
|
||||
for i, j := range jbod {
|
||||
jbod1[i] = j
|
||||
}
|
||||
jbod1[1], jbod1[2] = jbod[2], jbod[1]
|
||||
formatConfigs[2].XL.JBOD = jbod1
|
||||
return formatConfigs
|
||||
}
|
||||
|
||||
func TestFormatXLHealFreshDisks(t *testing.T) {
|
||||
// Create an instance of xl backend.
|
||||
obj, fsDirs, err := getXLObjectLayer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
xl := obj.(xlObjects)
|
||||
|
||||
err = obj.MakeBucket("bucket")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bucket := "bucket"
|
||||
object := "object"
|
||||
|
||||
_, err = obj.PutObject(bucket, object, int64(len("abcd")), bytes.NewReader([]byte("abcd")), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
/* // Now, remove two format files.. Load them and reorder
|
||||
if err = xl.storageDisks[3].DeleteFile(".minio.sys", "format.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = xl.storageDisks[11].DeleteFile(".minio.sys", "format.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
} */
|
||||
|
||||
// Remove the content of export dir 10 but preserve .minio.sys because it is automatically
|
||||
// created when minio starts
|
||||
for i := 3; i <= 5; i++ {
|
||||
if err = xl.storageDisks[i].DeleteFile(".minio.sys", "format.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = xl.storageDisks[i].DeleteFile(".minio.sys", "tmp"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = xl.storageDisks[i].DeleteFile(bucket, object+"/xl.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = xl.storageDisks[i].DeleteFile(bucket, object+"/part.1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = xl.storageDisks[i].DeleteVol(bucket); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
permutedStorageDisks := []StorageAPI{xl.storageDisks[1], xl.storageDisks[4],
|
||||
xl.storageDisks[2], xl.storageDisks[8], xl.storageDisks[6], xl.storageDisks[7],
|
||||
xl.storageDisks[0], xl.storageDisks[15], xl.storageDisks[13], xl.storageDisks[14],
|
||||
xl.storageDisks[3], xl.storageDisks[10], xl.storageDisks[12], xl.storageDisks[9],
|
||||
xl.storageDisks[5], xl.storageDisks[11]}
|
||||
|
||||
// Start healing disks
|
||||
err = healFormatXLFreshDisks(permutedStorageDisks)
|
||||
if err != nil {
|
||||
t.Fatal("healing corrupted disk failed: ", err)
|
||||
}
|
||||
|
||||
// Load again XL format.json to validate it
|
||||
_, err = loadFormatXL(permutedStorageDisks)
|
||||
if err != nil {
|
||||
t.Fatal("loading healed disk failed: ", err)
|
||||
}
|
||||
|
||||
// Clean all
|
||||
removeRoots(fsDirs)
|
||||
}
|
||||
|
||||
// Simulate XL disks creation, delete some format.json and remove the content of
|
||||
// a given disk to test healing a corrupted disk
|
||||
func TestFormatXLHealCorruptedDisks(t *testing.T) {
|
||||
// Create an instance of xl backend.
|
||||
obj, fsDirs, err := getXLObjectLayer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
xl := obj.(xlObjects)
|
||||
|
||||
err = obj.MakeBucket("bucket")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bucket := "bucket"
|
||||
object := "object"
|
||||
|
||||
_, err = obj.PutObject(bucket, object, int64(len("abcd")), bytes.NewReader([]byte("abcd")), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Now, remove two format files.. Load them and reorder
|
||||
if err = xl.storageDisks[3].DeleteFile(".minio.sys", "format.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = xl.storageDisks[11].DeleteFile(".minio.sys", "format.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Remove the content of export dir 10 but preserve .minio.sys because it is automatically
|
||||
// created when minio starts
|
||||
if err = xl.storageDisks[10].DeleteFile(".minio.sys", "format.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = xl.storageDisks[10].DeleteFile(".minio.sys", "tmp"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = xl.storageDisks[10].DeleteFile(bucket, object+"/xl.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = xl.storageDisks[10].DeleteFile(bucket, object+"/part.1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = xl.storageDisks[10].DeleteVol(bucket); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
permutedStorageDisks := []StorageAPI{xl.storageDisks[1], xl.storageDisks[4],
|
||||
xl.storageDisks[2], xl.storageDisks[8], xl.storageDisks[6], xl.storageDisks[7],
|
||||
xl.storageDisks[0], xl.storageDisks[15], xl.storageDisks[13], xl.storageDisks[14],
|
||||
xl.storageDisks[3], xl.storageDisks[10], xl.storageDisks[12], xl.storageDisks[9],
|
||||
xl.storageDisks[5], xl.storageDisks[11]}
|
||||
|
||||
// Start healing disks
|
||||
err = healFormatXLCorruptedDisks(permutedStorageDisks)
|
||||
if err != nil {
|
||||
t.Fatal("healing corrupted disk failed: ", err)
|
||||
}
|
||||
|
||||
// Load again XL format.json to validate it
|
||||
_, err = loadFormatXL(permutedStorageDisks)
|
||||
if err != nil {
|
||||
t.Fatal("loading healed disk failed: ", err)
|
||||
}
|
||||
|
||||
// Clean all
|
||||
removeRoots(fsDirs)
|
||||
}
|
||||
|
||||
// Test on ReorderByInspection by simulating creating disks and removing
|
||||
// some of format.json
|
||||
func TestFormatXLReorderByInspection(t *testing.T) {
|
||||
// Create an instance of xl backend.
|
||||
obj, fsDirs, err := getXLObjectLayer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
xl := obj.(xlObjects)
|
||||
|
||||
err = obj.MakeBucket("bucket")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bucket := "bucket"
|
||||
object := "object"
|
||||
|
||||
_, err = obj.PutObject(bucket, object, int64(len("abcd")), bytes.NewReader([]byte("abcd")), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Now, remove two format files.. Load them and reorder
|
||||
if err = xl.storageDisks[3].DeleteFile(".minio.sys", "format.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = xl.storageDisks[5].DeleteFile(".minio.sys", "format.json"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
permutedStorageDisks := []StorageAPI{xl.storageDisks[1], xl.storageDisks[4],
|
||||
xl.storageDisks[2], xl.storageDisks[8], xl.storageDisks[6], xl.storageDisks[7],
|
||||
xl.storageDisks[0], xl.storageDisks[15], xl.storageDisks[13], xl.storageDisks[14],
|
||||
xl.storageDisks[3], xl.storageDisks[10], xl.storageDisks[12], xl.storageDisks[9],
|
||||
xl.storageDisks[5], xl.storageDisks[11]}
|
||||
|
||||
permutedFormatConfigs, _ := loadAllFormats(permutedStorageDisks)
|
||||
|
||||
orderedDisks, err := reorderDisks(permutedStorageDisks, permutedFormatConfigs)
|
||||
if err != nil {
|
||||
t.Fatal("error reordering disks\n")
|
||||
}
|
||||
|
||||
orderedDisks, err = reorderDisksByInspection(orderedDisks, permutedStorageDisks, permutedFormatConfigs)
|
||||
if err != nil {
|
||||
t.Fatal("failed to reorder disk by inspection")
|
||||
}
|
||||
|
||||
// Check disks reordering
|
||||
for i := 0; i <= 15; i++ {
|
||||
if orderedDisks[i] == nil && i != 3 && i != 5 {
|
||||
t.Fatal("should not be nil")
|
||||
}
|
||||
if orderedDisks[i] != nil && orderedDisks[i] != xl.storageDisks[i] {
|
||||
t.Fatal("Disks were not ordered correctly.")
|
||||
}
|
||||
}
|
||||
|
||||
removeRoots(fsDirs)
|
||||
}
|
||||
|
||||
// Wrapper for calling FormatXL tests - currently validates
|
||||
// - valid format
|
||||
// - unrecognized version number
|
||||
// - invalid JBOD
|
||||
// - invalid Disk uuid
|
||||
func TestFormatXL(t *testing.T) {
|
||||
formatInputCases := [][]*formatConfigV1{
|
||||
genFormatXLValid(),
|
||||
genFormatXLInvalidVersion(),
|
||||
genFormatXLInvalidJBOD(),
|
||||
genFormatXLInvalidDisks(),
|
||||
genFormatXLInvalidDisksOrder(),
|
||||
}
|
||||
testCases := []struct {
|
||||
formatConfigs []*formatConfigV1
|
||||
shouldPass bool
|
||||
}{
|
||||
{
|
||||
formatConfigs: formatInputCases[0],
|
||||
shouldPass: true,
|
||||
},
|
||||
{
|
||||
formatConfigs: formatInputCases[1],
|
||||
shouldPass: false,
|
||||
},
|
||||
{
|
||||
formatConfigs: formatInputCases[2],
|
||||
shouldPass: false,
|
||||
},
|
||||
{
|
||||
formatConfigs: formatInputCases[3],
|
||||
shouldPass: false,
|
||||
},
|
||||
{
|
||||
formatConfigs: formatInputCases[4],
|
||||
shouldPass: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := checkFormatXL(testCase.formatConfigs)
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass but failed with %s", i+1, err)
|
||||
}
|
||||
if err == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to fail but passed instead", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests uuid order verification function.
|
||||
func TestSavedUUIDOrder(t *testing.T) {
|
||||
uuidTestCases := make([]struct {
|
||||
uuid string
|
||||
shouldPass bool
|
||||
}, 8)
|
||||
jbod := make([]string, 8)
|
||||
formatConfigs := make([]*formatConfigV1, 8)
|
||||
for index := range jbod {
|
||||
jbod[index] = getUUID()
|
||||
uuidTestCases[index].uuid = jbod[index]
|
||||
uuidTestCases[index].shouldPass = true
|
||||
}
|
||||
for index := range jbod {
|
||||
formatConfigs[index] = &formatConfigV1{
|
||||
Version: "1",
|
||||
Format: "xl",
|
||||
XL: &xlFormat{
|
||||
Version: "1",
|
||||
Disk: jbod[index],
|
||||
JBOD: jbod,
|
||||
},
|
||||
}
|
||||
}
|
||||
// Re order jbod for failure case.
|
||||
var jbod1 = make([]string, 8)
|
||||
for i, j := range jbod {
|
||||
jbod1[i] = j
|
||||
}
|
||||
jbod1[1], jbod1[2] = jbod[2], jbod[1]
|
||||
formatConfigs[2].XL.JBOD = jbod1
|
||||
uuidTestCases[1].shouldPass = false
|
||||
uuidTestCases[2].shouldPass = false
|
||||
|
||||
for i, testCase := range uuidTestCases {
|
||||
// Is uuid present on all JBOD ?.
|
||||
if testCase.shouldPass != isSavedUUIDInOrder(testCase.uuid, formatConfigs) {
|
||||
t.Errorf("Test %d: Expected to pass but failed", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015-2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
const (
|
||||
// listObjectsLimit - maximum list objects limit.
|
||||
listObjectsLimit = 1000
|
||||
)
|
||||
|
||||
// isDirExist - returns whether given directory is exist or not.
|
||||
func isDirExist(dirname string) (bool, error) {
|
||||
fi, e := os.Lstat(dirname)
|
||||
if e != nil {
|
||||
if os.IsNotExist(e) {
|
||||
return false, nil
|
||||
}
|
||||
return false, e
|
||||
}
|
||||
return fi.IsDir(), nil
|
||||
}
|
||||
|
||||
func (fs *Filesystem) saveTreeWalk(params listObjectParams, walker *treeWalker) {
|
||||
fs.listObjectMapMutex.Lock()
|
||||
defer fs.listObjectMapMutex.Unlock()
|
||||
|
||||
walkers, _ := fs.listObjectMap[params]
|
||||
walkers = append(walkers, walker)
|
||||
|
||||
fs.listObjectMap[params] = walkers
|
||||
}
|
||||
|
||||
func (fs *Filesystem) lookupTreeWalk(params listObjectParams) *treeWalker {
|
||||
fs.listObjectMapMutex.Lock()
|
||||
defer fs.listObjectMapMutex.Unlock()
|
||||
|
||||
if walkChs, ok := fs.listObjectMap[params]; ok {
|
||||
for i, walkCh := range walkChs {
|
||||
if !walkCh.timedOut {
|
||||
newWalkChs := walkChs[i+1:]
|
||||
if len(newWalkChs) > 0 {
|
||||
fs.listObjectMap[params] = newWalkChs
|
||||
} else {
|
||||
delete(fs.listObjectMap, params)
|
||||
}
|
||||
return walkCh
|
||||
}
|
||||
}
|
||||
// As all channels are timed out, delete the map entry
|
||||
delete(fs.listObjectMap, params)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListObjects - lists all objects for a given prefix, returns up to
|
||||
// maxKeys number of objects per call.
|
||||
func (fs Filesystem) ListObjects(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, *probe.Error) {
|
||||
result := ListObjectsInfo{}
|
||||
|
||||
// Input validation.
|
||||
bucket, e := fs.checkBucketArg(bucket)
|
||||
if e != nil {
|
||||
return result, probe.NewError(e)
|
||||
}
|
||||
bucketDir := filepath.Join(fs.diskPath, bucket)
|
||||
|
||||
if !IsValidObjectPrefix(prefix) {
|
||||
return result, probe.NewError(ObjectNameInvalid{Bucket: bucket, Object: prefix})
|
||||
}
|
||||
|
||||
// Verify if delimiter is anything other than '/', which we do not support.
|
||||
if delimiter != "" && delimiter != "/" {
|
||||
return result, probe.NewError(fmt.Errorf("delimiter '%s' is not supported. Only '/' is supported", delimiter))
|
||||
}
|
||||
|
||||
// Verify if marker has prefix.
|
||||
if marker != "" {
|
||||
if !strings.HasPrefix(marker, prefix) {
|
||||
return result, probe.NewError(fmt.Errorf("Invalid combination of marker '%s' and prefix '%s'", marker, prefix))
|
||||
}
|
||||
}
|
||||
|
||||
// Return empty response for a valid request when maxKeys is 0.
|
||||
if maxKeys == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Over flowing maxkeys - reset to listObjectsLimit.
|
||||
if maxKeys < 0 || maxKeys > listObjectsLimit {
|
||||
maxKeys = listObjectsLimit
|
||||
}
|
||||
|
||||
// Verify if prefix exists.
|
||||
prefixDir := filepath.Dir(filepath.FromSlash(prefix))
|
||||
rootDir := filepath.Join(bucketDir, prefixDir)
|
||||
if status, e := isDirExist(rootDir); !status {
|
||||
if e == nil {
|
||||
// Prefix does not exist, not an error just respond empty
|
||||
// list response.
|
||||
return result, nil
|
||||
}
|
||||
// Rest errors should be treated as failure.
|
||||
return result, probe.NewError(e)
|
||||
}
|
||||
|
||||
recursive := true
|
||||
if delimiter == "/" {
|
||||
recursive = false
|
||||
}
|
||||
|
||||
// Maximum 1000 objects returned in a single to listObjects.
|
||||
// Further calls will set right marker value to continue reading the rest of the objectList.
|
||||
// popTreeWalker returns nil if the call to ListObject is done for the first time.
|
||||
// On further calls to ListObjects to retrive more objects within the timeout period,
|
||||
// popTreeWalker returns the channel from which rest of the objects can be retrieved.
|
||||
walker := fs.lookupTreeWalk(listObjectParams{bucket, delimiter, marker, prefix})
|
||||
if walker == nil {
|
||||
walker = startTreeWalk(fs.diskPath, bucket, filepath.FromSlash(prefix), filepath.FromSlash(marker), recursive)
|
||||
}
|
||||
|
||||
nextMarker := ""
|
||||
for i := 0; i < maxKeys; {
|
||||
walkResult, ok := <-walker.ch
|
||||
if !ok {
|
||||
// Closed channel.
|
||||
return result, nil
|
||||
}
|
||||
// For any walk error return right away.
|
||||
if walkResult.err != nil {
|
||||
return ListObjectsInfo{}, probe.NewError(walkResult.err)
|
||||
}
|
||||
objInfo := walkResult.objectInfo
|
||||
objInfo.Name = filepath.ToSlash(objInfo.Name)
|
||||
|
||||
// Skip temporary files.
|
||||
if strings.Contains(objInfo.Name, "$multiparts") || strings.Contains(objInfo.Name, "$tmpobject") {
|
||||
continue
|
||||
}
|
||||
|
||||
// For objects being directory and delimited we set Prefixes.
|
||||
if objInfo.IsDir {
|
||||
result.Prefixes = append(result.Prefixes, objInfo.Name)
|
||||
} else {
|
||||
result.Objects = append(result.Objects, objInfo)
|
||||
}
|
||||
|
||||
// We have listed everything return.
|
||||
if walkResult.end {
|
||||
return result, nil
|
||||
}
|
||||
nextMarker = objInfo.Name
|
||||
i++
|
||||
}
|
||||
// We haven't exhaused the list yet, set IsTruncated to 'true' so
|
||||
// that the client can send another request.
|
||||
result.IsTruncated = true
|
||||
result.NextMarker = nextMarker
|
||||
fs.saveTreeWalk(listObjectParams{bucket, delimiter, nextMarker, prefix}, walker)
|
||||
return result, nil
|
||||
}
|
||||
-163
@@ -1,163 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
/// Bucket Operations
|
||||
|
||||
// DeleteBucket - delete a bucket.
|
||||
func (fs Filesystem) DeleteBucket(bucket string) *probe.Error {
|
||||
// Verify bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return probe.NewError(BucketNameInvalid{Bucket: bucket})
|
||||
}
|
||||
bucket = getActualBucketname(fs.diskPath, bucket)
|
||||
bucketDir := filepath.Join(fs.diskPath, bucket)
|
||||
if e := os.Remove(bucketDir); e != nil {
|
||||
// Error if there was no bucket in the first place.
|
||||
if os.IsNotExist(e) {
|
||||
return probe.NewError(BucketNotFound{Bucket: bucket})
|
||||
}
|
||||
// On windows the string is slightly different, handle it here.
|
||||
if strings.Contains(e.Error(), "directory is not empty") {
|
||||
return probe.NewError(BucketNotEmpty{Bucket: bucket})
|
||||
}
|
||||
// Hopefully for all other operating systems, this is
|
||||
// assumed to be consistent.
|
||||
if strings.Contains(e.Error(), "directory not empty") {
|
||||
return probe.NewError(BucketNotEmpty{Bucket: bucket})
|
||||
}
|
||||
return probe.NewError(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListBuckets - Get service.
|
||||
func (fs Filesystem) ListBuckets() ([]BucketInfo, *probe.Error) {
|
||||
files, e := ioutil.ReadDir(fs.diskPath)
|
||||
if e != nil {
|
||||
return []BucketInfo{}, probe.NewError(e)
|
||||
}
|
||||
var buckets []BucketInfo
|
||||
for _, file := range files {
|
||||
if !file.IsDir() {
|
||||
// If not directory, ignore all file types.
|
||||
continue
|
||||
}
|
||||
// If directories are found with odd names, skip them.
|
||||
dirName := strings.ToLower(file.Name())
|
||||
if !IsValidBucketName(dirName) {
|
||||
continue
|
||||
}
|
||||
bucket := BucketInfo{
|
||||
Name: dirName,
|
||||
Created: file.ModTime(),
|
||||
}
|
||||
buckets = append(buckets, bucket)
|
||||
}
|
||||
// Remove duplicated entries.
|
||||
buckets = removeDuplicateBuckets(buckets)
|
||||
return buckets, nil
|
||||
}
|
||||
|
||||
// removeDuplicateBuckets - remove duplicate buckets.
|
||||
func removeDuplicateBuckets(buckets []BucketInfo) []BucketInfo {
|
||||
length := len(buckets) - 1
|
||||
for i := 0; i < length; i++ {
|
||||
for j := i + 1; j <= length; j++ {
|
||||
if buckets[i].Name == buckets[j].Name {
|
||||
if buckets[i].Created.Sub(buckets[j].Created) > 0 {
|
||||
buckets[i] = buckets[length]
|
||||
} else {
|
||||
buckets[j] = buckets[length]
|
||||
}
|
||||
buckets = buckets[0:length]
|
||||
length--
|
||||
j--
|
||||
}
|
||||
}
|
||||
}
|
||||
return buckets
|
||||
}
|
||||
|
||||
// MakeBucket - PUT Bucket
|
||||
func (fs Filesystem) MakeBucket(bucket string) *probe.Error {
|
||||
if _, e := fs.checkBucketArg(bucket); e == nil {
|
||||
return probe.NewError(BucketExists{Bucket: bucket})
|
||||
} else if _, ok := e.(BucketNameInvalid); ok {
|
||||
return probe.NewError(BucketNameInvalid{Bucket: bucket})
|
||||
}
|
||||
bucketDir := filepath.Join(fs.diskPath, bucket)
|
||||
|
||||
// Make bucket.
|
||||
if e := os.Mkdir(bucketDir, 0700); e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getActualBucketname - will convert incoming bucket names to
|
||||
// corresponding actual bucketnames on the backend in a platform
|
||||
// compatible way for all operating systems.
|
||||
func getActualBucketname(fsPath, bucket string) string {
|
||||
fd, e := os.Open(fsPath)
|
||||
if e != nil {
|
||||
return bucket
|
||||
}
|
||||
buckets, e := fd.Readdirnames(-1)
|
||||
if e != nil {
|
||||
return bucket
|
||||
}
|
||||
for _, b := range buckets {
|
||||
// Verify if lowercase version of the bucket is equal
|
||||
// to the incoming bucket, then use the proper name.
|
||||
if strings.ToLower(b) == bucket {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return bucket
|
||||
}
|
||||
|
||||
// GetBucketInfo - get bucket metadata.
|
||||
func (fs Filesystem) GetBucketInfo(bucket string) (BucketInfo, *probe.Error) {
|
||||
if !IsValidBucketName(bucket) {
|
||||
return BucketInfo{}, probe.NewError(BucketNameInvalid{Bucket: bucket})
|
||||
}
|
||||
bucket = getActualBucketname(fs.diskPath, bucket)
|
||||
// Get bucket path.
|
||||
bucketDir := filepath.Join(fs.diskPath, bucket)
|
||||
fi, e := os.Stat(bucketDir)
|
||||
if e != nil {
|
||||
// Check if bucket exists.
|
||||
if os.IsNotExist(e) {
|
||||
return BucketInfo{}, probe.NewError(BucketNotFound{Bucket: bucket})
|
||||
}
|
||||
return BucketInfo{}, probe.NewError(e)
|
||||
}
|
||||
bucketMetadata := BucketInfo{}
|
||||
bucketMetadata.Name = fi.Name()
|
||||
bucketMetadata.Created = fi.ModTime()
|
||||
return bucketMetadata, nil
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The test not just includes asserting the correctness of the output,
|
||||
// But also includes test cases for which the function should fail.
|
||||
// For those cases for which it fails, its also asserted whether the function fails as expected.
|
||||
func TestGetBucketInfo(t *testing.T) {
|
||||
// Make a temporary directory to use as the fs.
|
||||
directory, e := ioutil.TempDir("", "minio-metadata-test")
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
defer os.RemoveAll(directory)
|
||||
|
||||
// Create the fs.
|
||||
fs, err := newFS(directory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Creating few buckets.
|
||||
for i := 0; i < 4; i++ {
|
||||
err = fs.MakeBucket("meta-test-bucket." + strconv.Itoa(i))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
metaData BucketInfo
|
||||
e error
|
||||
shouldPass bool
|
||||
}{
|
||||
// Test cases with invalid bucket names.
|
||||
{".test", BucketInfo{}, BucketNameInvalid{Bucket: ".test"}, false},
|
||||
{"Test", BucketInfo{}, BucketNameInvalid{Bucket: "Test"}, false},
|
||||
{"---", BucketInfo{}, BucketNameInvalid{Bucket: "---"}, false},
|
||||
{"ad", BucketInfo{}, BucketNameInvalid{Bucket: "ad"}, false},
|
||||
// Test cases with non-existent buckets.
|
||||
{"volatile-bucket-1", BucketInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false},
|
||||
{"volatile-bucket-2", BucketInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false},
|
||||
// Test cases with existing buckets.
|
||||
{"meta-test-bucket.0", BucketInfo{Name: "meta-test-bucket.0"}, nil, true},
|
||||
{"meta-test-bucket.1", BucketInfo{Name: "meta-test-bucket.1"}, nil, true},
|
||||
{"meta-test-bucket.2", BucketInfo{Name: "meta-test-bucket.2"}, nil, true},
|
||||
{"meta-test-bucket.3", BucketInfo{Name: "meta-test-bucket.3"}, nil, true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
// The err returned is of type *probe.Error.
|
||||
bucketInfo, err := fs.GetBucketInfo(testCase.bucketName)
|
||||
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Cause.Error())
|
||||
}
|
||||
if err == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, testCase.e.Error())
|
||||
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if err != nil && !testCase.shouldPass {
|
||||
if testCase.e.Error() != err.Cause.Error() {
|
||||
t.Errorf("Test %d: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, testCase.e.Error(), err.Cause.Error())
|
||||
}
|
||||
}
|
||||
// Since there are cases for which GetBucketInfo fails, this is necessary.
|
||||
// Test passes as expected, but the output values are verified for correctness here.
|
||||
if err == nil && testCase.shouldPass {
|
||||
if testCase.bucketName != bucketInfo.Name {
|
||||
t.Errorf("Test %d: Expected the bucket name to be \"%s\", but found \"%s\" instead", i+1, testCase.bucketName, bucketInfo.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListBuckets(t *testing.T) {
|
||||
// Make a temporary directory to use as the fs.
|
||||
directory, e := ioutil.TempDir("", "minio-benchmark")
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
defer os.RemoveAll(directory)
|
||||
|
||||
// Create the fs.
|
||||
fs, err := newFS(directory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a few buckets.
|
||||
for i := 0; i < 10; i++ {
|
||||
err = fs.MakeBucket("testbucket." + strconv.Itoa(i))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// List, and ensure that they are all there.
|
||||
metadatas, err := fs.ListBuckets()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(metadatas) != 10 {
|
||||
t.Errorf("incorrect length of metadatas (%d)\n", len(metadatas))
|
||||
}
|
||||
|
||||
// Iterate over the buckets, ensuring that the name is correct.
|
||||
for i := 0; i < len(metadatas); i++ {
|
||||
if !strings.Contains(metadatas[i].Name, "testbucket") {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteBucket(t *testing.T) {
|
||||
// Make a temporary directory to use as the fs.
|
||||
directory, e := ioutil.TempDir("", "minio-benchmark")
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
defer os.RemoveAll(directory)
|
||||
|
||||
// Create the fs.
|
||||
fs, err := newFS(directory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Deleting a bucket that doesn't exist should error.
|
||||
err = fs.DeleteBucket("bucket")
|
||||
if !strings.Contains(err.Cause.Error(), "Bucket not found:") {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkListBuckets(b *testing.B) {
|
||||
// Make a temporary directory to use as the fs.
|
||||
directory, e := ioutil.TempDir("", "minio-benchmark")
|
||||
if e != nil {
|
||||
b.Fatal(e)
|
||||
}
|
||||
defer os.RemoveAll(directory)
|
||||
|
||||
// Create the fs.
|
||||
fs, err := newFS(directory)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a few buckets.
|
||||
for i := 0; i < 20; i++ {
|
||||
err = fs.MakeBucket("bucket." + strconv.Itoa(i))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
// List the buckets over and over and over.
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err = fs.ListBuckets()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDeleteBucket(b *testing.B) {
|
||||
// Make a temporary directory to use as the fs.
|
||||
directory, e := ioutil.TempDir("", "minio-benchmark")
|
||||
if e != nil {
|
||||
b.Fatal(e)
|
||||
}
|
||||
defer os.RemoveAll(directory)
|
||||
|
||||
// Create the fs.
|
||||
fs, err := newFS(directory)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Creating buckets takes time, so stop and start the timer.
|
||||
b.StopTimer()
|
||||
|
||||
// Create and delete the bucket over and over.
|
||||
err = fs.MakeBucket("bucket")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.StartTimer()
|
||||
|
||||
err = fs.DeleteBucket("bucket")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetBucketInfo(b *testing.B) {
|
||||
// Make a temporary directory to use as the fs.
|
||||
directory, e := ioutil.TempDir("", "minio-benchmark")
|
||||
if e != nil {
|
||||
b.Fatal(e)
|
||||
}
|
||||
defer os.RemoveAll(directory)
|
||||
|
||||
// Create the fs.
|
||||
fs, err := newFS(directory)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// Put up a bucket with some metadata.
|
||||
err = fs.MakeBucket("bucket")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Retrieve the metadata!
|
||||
_, err := fs.GetBucketInfo("bucket")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import "io"
|
||||
|
||||
func fsCreateFile(disk StorageAPI, reader io.Reader, buf []byte, tmpBucket, tempObj string) (int64, error) {
|
||||
bytesWritten := int64(0)
|
||||
// Read the buffer till io.EOF and append the read data to the temporary file.
|
||||
for {
|
||||
n, rErr := reader.Read(buf)
|
||||
if rErr != nil && rErr != io.EOF {
|
||||
return 0, rErr
|
||||
}
|
||||
bytesWritten += int64(n)
|
||||
if n > 0 {
|
||||
wErr := disk.AppendFile(tmpBucket, tempObj, buf[0:n])
|
||||
if wErr != nil {
|
||||
return 0, wErr
|
||||
}
|
||||
}
|
||||
if rErr == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return bytesWritten, nil
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// fsDirent carries directory entries.
|
||||
type fsDirent struct {
|
||||
name string
|
||||
modTime time.Time // On Solaris and older unix distros this is empty.
|
||||
size int64 // On Solaris and older unix distros this is empty.
|
||||
mode os.FileMode
|
||||
}
|
||||
|
||||
// IsDir - returns true if fsDirent is a directory
|
||||
func (ent fsDirent) IsDir() bool {
|
||||
return ent.mode.IsDir()
|
||||
}
|
||||
|
||||
// IsSymlink - returns true if fsDirent is a symbolic link
|
||||
func (ent fsDirent) IsSymlink() bool {
|
||||
return ent.mode&os.ModeSymlink == os.ModeSymlink
|
||||
}
|
||||
|
||||
// IsRegular - returns true if fsDirent is a regular file
|
||||
func (ent fsDirent) IsRegular() bool {
|
||||
return ent.mode.IsRegular()
|
||||
}
|
||||
|
||||
// byDirentName is a collection satisfying sort.Interface.
|
||||
type byDirentName []fsDirent
|
||||
|
||||
func (d byDirentName) Len() int { return len(d) }
|
||||
func (d byDirentName) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
|
||||
func (d byDirentName) Less(i, j int) bool { return d[i].name < d[j].name }
|
||||
|
||||
// Using sort.Search() internally to jump to the file entry containing the prefix.
|
||||
func searchDirents(dirents []fsDirent, x string) int {
|
||||
processFunc := func(i int) bool {
|
||||
return dirents[i].name >= x
|
||||
}
|
||||
return sort.Search(len(dirents), processFunc)
|
||||
}
|
||||
|
||||
// Tree walk result carries results of tree walking.
|
||||
type treeWalkResult struct {
|
||||
objectInfo ObjectInfo
|
||||
err error
|
||||
end bool
|
||||
}
|
||||
|
||||
// Tree walk notify carries a channel which notifies tree walk
|
||||
// results, additionally it also carries information if treeWalk
|
||||
// should be timedOut.
|
||||
type treeWalker struct {
|
||||
ch <-chan treeWalkResult
|
||||
timedOut bool
|
||||
}
|
||||
|
||||
// treeWalk walks FS directory tree recursively pushing ObjectInfo into the channel as and when it encounters files.
|
||||
func treeWalk(bucketDir, prefixDir, entryPrefixMatch, marker string, recursive bool, send func(treeWalkResult) bool, count *int) bool {
|
||||
// Example:
|
||||
// if prefixDir="one/two/three/" and marker="four/five.txt" treeWalk is recursively
|
||||
// called with prefixDir="one/two/three/four/" and marker="five.txt"
|
||||
|
||||
// Convert dirent to ObjectInfo
|
||||
direntToObjectInfo := func(dirent fsDirent) (ObjectInfo, error) {
|
||||
objectInfo := ObjectInfo{}
|
||||
// Convert to full object name.
|
||||
objectInfo.Name = filepath.Join(prefixDir, dirent.name)
|
||||
if dirent.modTime.IsZero() && dirent.size == 0 {
|
||||
// ModifiedTime and Size are zero, Stat() and figure out
|
||||
// the actual values that need to be set.
|
||||
fi, err := os.Stat(filepath.Join(bucketDir, prefixDir, dirent.name))
|
||||
if err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
// Fill size and modtime.
|
||||
objectInfo.ModifiedTime = fi.ModTime()
|
||||
objectInfo.Size = fi.Size()
|
||||
objectInfo.IsDir = fi.IsDir()
|
||||
} else {
|
||||
// If ModifiedTime or Size are set then use them
|
||||
// without attempting another Stat operation.
|
||||
objectInfo.ModifiedTime = dirent.modTime
|
||||
objectInfo.Size = dirent.size
|
||||
objectInfo.IsDir = dirent.IsDir()
|
||||
}
|
||||
if objectInfo.IsDir {
|
||||
// Add os.PathSeparator suffix again for directories as
|
||||
// filepath.Join would have removed it.
|
||||
objectInfo.Size = 0
|
||||
objectInfo.Name += string(os.PathSeparator)
|
||||
}
|
||||
return objectInfo, nil
|
||||
}
|
||||
|
||||
var markerBase, markerDir string
|
||||
if marker != "" {
|
||||
// Ex: if marker="four/five.txt", markerDir="four/" markerBase="five.txt"
|
||||
markerSplit := strings.SplitN(marker, string(os.PathSeparator), 2)
|
||||
markerDir = markerSplit[0]
|
||||
if len(markerSplit) == 2 {
|
||||
markerDir += string(os.PathSeparator)
|
||||
markerBase = markerSplit[1]
|
||||
}
|
||||
}
|
||||
|
||||
// readDirAll returns entries that begins with entryPrefixMatch
|
||||
dirents, err := readDirAll(filepath.Join(bucketDir, prefixDir), entryPrefixMatch)
|
||||
if err != nil {
|
||||
send(treeWalkResult{err: err})
|
||||
return false
|
||||
}
|
||||
// example:
|
||||
// If markerDir="four/" searchDirents() returns the index of "four/" in the sorted
|
||||
// dirents list. We skip all the dirent entries till "four/"
|
||||
dirents = dirents[searchDirents(dirents, markerDir):]
|
||||
*count += len(dirents)
|
||||
for i, dirent := range dirents {
|
||||
if i == 0 && markerDir == dirent.name && !dirent.IsDir() {
|
||||
// If the first entry is not a directory
|
||||
// we need to skip this entry.
|
||||
*count--
|
||||
continue
|
||||
}
|
||||
if dirent.IsDir() && recursive {
|
||||
// If the entry is a directory, we will need recurse into it.
|
||||
markerArg := ""
|
||||
if dirent.name == markerDir {
|
||||
// We need to pass "five.txt" as marker only if we are
|
||||
// recursing into "four/"
|
||||
markerArg = markerBase
|
||||
}
|
||||
*count--
|
||||
if !treeWalk(bucketDir, filepath.Join(prefixDir, dirent.name), "", markerArg, recursive, send, count) {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
objectInfo, err := direntToObjectInfo(dirent)
|
||||
if err != nil {
|
||||
send(treeWalkResult{err: err})
|
||||
return false
|
||||
}
|
||||
*count--
|
||||
if !send(treeWalkResult{objectInfo: objectInfo}) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Initiate a new treeWalk in a goroutine.
|
||||
func startTreeWalk(fsPath, bucket, prefix, marker string, recursive bool) *treeWalker {
|
||||
// Example 1
|
||||
// If prefix is "one/two/three/" and marker is "one/two/three/four/five.txt"
|
||||
// treeWalk is called with prefixDir="one/two/three/" and marker="four/five.txt"
|
||||
// and entryPrefixMatch=""
|
||||
|
||||
// Example 2
|
||||
// if prefix is "one/two/th" and marker is "one/two/three/four/five.txt"
|
||||
// treeWalk is called with prefixDir="one/two/" and marker="three/four/five.txt"
|
||||
// and entryPrefixMatch="th"
|
||||
ch := make(chan treeWalkResult, listObjectsLimit)
|
||||
walkNotify := treeWalker{ch: ch}
|
||||
entryPrefixMatch := prefix
|
||||
prefixDir := ""
|
||||
lastIndex := strings.LastIndex(prefix, string(os.PathSeparator))
|
||||
if lastIndex != -1 {
|
||||
entryPrefixMatch = prefix[lastIndex+1:]
|
||||
prefixDir = prefix[:lastIndex+1]
|
||||
}
|
||||
count := 0
|
||||
marker = strings.TrimPrefix(marker, prefixDir)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
send := func(walkResult treeWalkResult) bool {
|
||||
// Add the bucket.
|
||||
walkResult.objectInfo.Bucket = bucket
|
||||
if count == 0 {
|
||||
walkResult.end = true
|
||||
}
|
||||
timer := time.After(time.Second * 60)
|
||||
select {
|
||||
case ch <- walkResult:
|
||||
return true
|
||||
case <-timer:
|
||||
walkNotify.timedOut = true
|
||||
return false
|
||||
}
|
||||
}
|
||||
bucketDir := filepath.Join(fsPath, bucket)
|
||||
treeWalk(bucketDir, prefixDir, entryPrefixMatch, marker, recursive, send, &count)
|
||||
}()
|
||||
return &walkNotify
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
// +build !linux,!darwin,!openbsd,!freebsd,!netbsd,!dragonfly
|
||||
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Read all directory entries, returns a list of lexically sorted entries.
|
||||
func readDirAll(readDirPath, entryPrefixMatch string) ([]fsDirent, error) {
|
||||
f, err := os.Open(readDirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
var dirents []fsDirent
|
||||
for {
|
||||
fis, err := f.Readdir(1000)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
for _, fi := range fis {
|
||||
dirent := fsDirent{
|
||||
name: fi.Name(),
|
||||
modTime: fi.ModTime(),
|
||||
size: fi.Size(),
|
||||
mode: fi.Mode(),
|
||||
}
|
||||
if dirent.IsDir() {
|
||||
dirent.name += string(os.PathSeparator)
|
||||
dirent.size = 0
|
||||
}
|
||||
if strings.HasPrefix(fi.Name(), entryPrefixMatch) {
|
||||
dirents = append(dirents, dirent)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort dirents.
|
||||
sort.Sort(byDirentName(dirents))
|
||||
return dirents, nil
|
||||
}
|
||||
|
||||
// scans the directory dirPath, calling filter() on each directory
|
||||
// entry. Entries for which filter() returns true are stored, lexically
|
||||
// sorted using sort.Sort(). If filter is NULL, all entries are selected.
|
||||
// If namesOnly is true, dirPath is not appended into entry name.
|
||||
func scandir(dirPath string, filter func(fsDirent) bool, namesOnly bool) ([]fsDirent, error) {
|
||||
d, err := os.Open(dirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
var dirents []fsDirent
|
||||
for {
|
||||
fis, err := d.Readdir(1000)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
for _, fi := range fis {
|
||||
dirent := fsDirent{
|
||||
name: fi.Name(),
|
||||
modTime: fi.ModTime(),
|
||||
size: fi.Size(),
|
||||
mode: fi.Mode(),
|
||||
}
|
||||
if !namesOnly {
|
||||
dirent.name = filepath.Join(dirPath, dirent.name)
|
||||
}
|
||||
if dirent.IsDir() {
|
||||
dirent.name += string(os.PathSeparator)
|
||||
}
|
||||
if filter == nil || filter(dirent) {
|
||||
dirents = append(dirents, dirent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(byDirentName(dirents))
|
||||
|
||||
return dirents, nil
|
||||
}
|
||||
-295
@@ -1,295 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
// InvalidArgument invalid argument
|
||||
type InvalidArgument struct{}
|
||||
|
||||
func (e InvalidArgument) Error() string {
|
||||
return "Invalid argument"
|
||||
}
|
||||
|
||||
// UnsupportedFilesystem unsupported filesystem type
|
||||
type UnsupportedFilesystem struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
func (e UnsupportedFilesystem) Error() string {
|
||||
return "Unsupported filesystem: " + e.Type
|
||||
}
|
||||
|
||||
// RootPathFull root path out of space
|
||||
type RootPathFull struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func (e RootPathFull) Error() string {
|
||||
return "Root path " + e.Path + " reached its minimum free disk threshold."
|
||||
}
|
||||
|
||||
// BucketNotFound bucket does not exist
|
||||
type BucketNotFound struct {
|
||||
Bucket string
|
||||
}
|
||||
|
||||
func (e BucketNotFound) Error() string {
|
||||
return "Bucket not found: " + e.Bucket
|
||||
}
|
||||
|
||||
// BucketNotEmpty bucket is not empty
|
||||
type BucketNotEmpty struct {
|
||||
Bucket string
|
||||
}
|
||||
|
||||
func (e BucketNotEmpty) Error() string {
|
||||
return "Bucket not empty: " + e.Bucket
|
||||
}
|
||||
|
||||
// ObjectNotFound object does not exist
|
||||
type ObjectNotFound struct {
|
||||
Bucket string
|
||||
Object string
|
||||
}
|
||||
|
||||
func (e ObjectNotFound) Error() string {
|
||||
return "Object not found: " + e.Bucket + "#" + e.Object
|
||||
}
|
||||
|
||||
// ObjectExistsAsPrefix object already exists with a requested prefix.
|
||||
type ObjectExistsAsPrefix struct {
|
||||
Bucket string
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func (e ObjectExistsAsPrefix) Error() string {
|
||||
return "Object exists on : " + e.Bucket + " as prefix " + e.Prefix
|
||||
}
|
||||
|
||||
// ObjectCorrupted object found to be corrupted
|
||||
type ObjectCorrupted struct {
|
||||
Object string
|
||||
}
|
||||
|
||||
func (e ObjectCorrupted) Error() string {
|
||||
return "Object found corrupted: " + e.Object
|
||||
}
|
||||
|
||||
// BucketExists bucket exists
|
||||
type BucketExists struct {
|
||||
Bucket string
|
||||
}
|
||||
|
||||
func (e BucketExists) Error() string {
|
||||
return "Bucket exists: " + e.Bucket
|
||||
}
|
||||
|
||||
// CorruptedBackend backend found to be corrupted
|
||||
type CorruptedBackend struct {
|
||||
Backend string
|
||||
}
|
||||
|
||||
func (e CorruptedBackend) Error() string {
|
||||
return "Corrupted backend: " + e.Backend
|
||||
}
|
||||
|
||||
// NotImplemented function not implemented
|
||||
type NotImplemented struct {
|
||||
Function string
|
||||
}
|
||||
|
||||
func (e NotImplemented) Error() string {
|
||||
return "Not implemented: " + e.Function
|
||||
}
|
||||
|
||||
// InvalidDisksArgument invalid number of disks per node
|
||||
type InvalidDisksArgument struct{}
|
||||
|
||||
func (e InvalidDisksArgument) Error() string {
|
||||
return "Invalid number of disks per node"
|
||||
}
|
||||
|
||||
// BadDigest - Content-MD5 you specified did not match what we received.
|
||||
type BadDigest struct {
|
||||
ExpectedMD5 string
|
||||
CalculatedMD5 string
|
||||
}
|
||||
|
||||
func (e BadDigest) Error() string {
|
||||
return "Bad digest expected " + e.ExpectedMD5 + " is not valid with what we calculated " + e.CalculatedMD5
|
||||
}
|
||||
|
||||
// InternalError - generic internal error
|
||||
type InternalError struct{}
|
||||
|
||||
// BackendError - generic disk backend error
|
||||
type BackendError struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// BackendCorrupted - path has corrupted data
|
||||
type BackendCorrupted BackendError
|
||||
|
||||
// APINotImplemented - generic API not implemented error
|
||||
type APINotImplemented struct {
|
||||
API string
|
||||
}
|
||||
|
||||
// GenericBucketError - generic bucket error
|
||||
type GenericBucketError struct {
|
||||
Bucket string
|
||||
}
|
||||
|
||||
// BucketPolicyNotFound - no bucket policy found.
|
||||
type BucketPolicyNotFound GenericBucketError
|
||||
|
||||
func (e BucketPolicyNotFound) Error() string {
|
||||
return "No bucket policy found for bucket: " + e.Bucket
|
||||
}
|
||||
|
||||
// GenericObjectError - generic object error
|
||||
type GenericObjectError struct {
|
||||
Bucket string
|
||||
Object string
|
||||
}
|
||||
|
||||
// ImplementationError - generic implementation error
|
||||
type ImplementationError struct {
|
||||
Bucket string
|
||||
Object string
|
||||
Err error
|
||||
}
|
||||
|
||||
/// Bucket related errors
|
||||
|
||||
// BucketNameInvalid - bucketname provided is invalid
|
||||
type BucketNameInvalid GenericBucketError
|
||||
|
||||
/// Object related errors
|
||||
|
||||
// ObjectNameInvalid - object name provided is invalid
|
||||
type ObjectNameInvalid GenericObjectError
|
||||
|
||||
// Return string an error formatted as the given text
|
||||
func (e ImplementationError) Error() string {
|
||||
error := ""
|
||||
if e.Bucket != "" {
|
||||
error = error + "Bucket: " + e.Bucket + " "
|
||||
}
|
||||
if e.Object != "" {
|
||||
error = error + "Object: " + e.Object + " "
|
||||
}
|
||||
error = error + "Error: " + e.Err.Error()
|
||||
return error
|
||||
}
|
||||
|
||||
// EmbedError - wrapper function for error object
|
||||
func EmbedError(bucket, object string, err error) ImplementationError {
|
||||
return ImplementationError{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Return string an error formatted as the given text
|
||||
func (e InternalError) Error() string {
|
||||
return "Internal error occured"
|
||||
}
|
||||
|
||||
// Return string an error formatted as the given text
|
||||
func (e APINotImplemented) Error() string {
|
||||
return "Api not implemented: " + e.API
|
||||
}
|
||||
|
||||
// Return string an error formatted as the given text
|
||||
func (e BucketNameInvalid) Error() string {
|
||||
return "Bucket name invalid: " + e.Bucket
|
||||
}
|
||||
|
||||
// Return string an error formatted as the given text
|
||||
func (e ObjectNameInvalid) Error() string {
|
||||
return "Object name invalid: " + e.Bucket + "#" + e.Object
|
||||
}
|
||||
|
||||
// IncompleteBody You did not provide the number of bytes specified by the Content-Length HTTP header
|
||||
type IncompleteBody GenericObjectError
|
||||
|
||||
// Return string an error formatted as the given text
|
||||
func (e IncompleteBody) Error() string {
|
||||
return e.Bucket + "#" + e.Object + "has incomplete body"
|
||||
}
|
||||
|
||||
// Return string an error formatted as the given text
|
||||
func (e BackendCorrupted) Error() string {
|
||||
return "Backend corrupted: " + e.Path
|
||||
}
|
||||
|
||||
// OperationNotPermitted - operation not permitted
|
||||
type OperationNotPermitted struct {
|
||||
Op string
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (e OperationNotPermitted) Error() string {
|
||||
return "Operation " + e.Op + " not permitted for reason: " + e.Reason
|
||||
}
|
||||
|
||||
// InvalidRange - invalid range
|
||||
type InvalidRange struct {
|
||||
Start int64
|
||||
Length int64
|
||||
}
|
||||
|
||||
func (e InvalidRange) Error() string {
|
||||
return fmt.Sprintf("Invalid range start:%d length:%d", e.Start, e.Length)
|
||||
}
|
||||
|
||||
/// Multipart related errors
|
||||
|
||||
// InvalidUploadID invalid upload id
|
||||
type InvalidUploadID struct {
|
||||
UploadID string
|
||||
}
|
||||
|
||||
func (e InvalidUploadID) Error() string {
|
||||
return "Invalid upload id " + e.UploadID
|
||||
}
|
||||
|
||||
// InvalidPart One or more of the specified parts could not be found
|
||||
type InvalidPart struct{}
|
||||
|
||||
func (e InvalidPart) Error() string {
|
||||
return "One or more of the specified parts could not be found"
|
||||
}
|
||||
|
||||
// InvalidPartOrder parts are not ordered as Requested
|
||||
type InvalidPartOrder struct {
|
||||
UploadID string
|
||||
}
|
||||
|
||||
func (e InvalidPartOrder) Error() string {
|
||||
return "Invalid part order sent for " + e.UploadID
|
||||
}
|
||||
|
||||
// MalformedXML invalid xml format
|
||||
type MalformedXML struct{}
|
||||
|
||||
func (e MalformedXML) Error() string {
|
||||
return "Malformed XML"
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func scanMultipartDir(bucketDir, prefixPath, markerPath, uploadIDMarker string, recursive bool) <-chan multipartObjectInfo {
|
||||
objectInfoCh := make(chan multipartObjectInfo, listObjectsLimit)
|
||||
|
||||
// TODO: check if bucketDir is absolute path
|
||||
scanDir := bucketDir
|
||||
dirDepth := bucketDir
|
||||
|
||||
if prefixPath != "" {
|
||||
if !filepath.IsAbs(prefixPath) {
|
||||
tmpPrefixPath := filepath.Join(bucketDir, prefixPath)
|
||||
if strings.HasSuffix(prefixPath, string(os.PathSeparator)) {
|
||||
tmpPrefixPath += string(os.PathSeparator)
|
||||
}
|
||||
prefixPath = tmpPrefixPath
|
||||
}
|
||||
|
||||
// TODO: check if prefixPath starts with bucketDir
|
||||
|
||||
// Case #1: if prefixPath is /mnt/mys3/mybucket/2012/photos/paris, then
|
||||
// dirDepth is /mnt/mys3/mybucket/2012/photos
|
||||
// Case #2: if prefixPath is /mnt/mys3/mybucket/2012/photos/, then
|
||||
// dirDepth is /mnt/mys3/mybucket/2012/photos
|
||||
dirDepth = filepath.Dir(prefixPath)
|
||||
scanDir = dirDepth
|
||||
} else {
|
||||
prefixPath = bucketDir
|
||||
}
|
||||
|
||||
if markerPath != "" {
|
||||
if !filepath.IsAbs(markerPath) {
|
||||
tmpMarkerPath := filepath.Join(bucketDir, markerPath)
|
||||
if strings.HasSuffix(markerPath, string(os.PathSeparator)) {
|
||||
tmpMarkerPath += string(os.PathSeparator)
|
||||
}
|
||||
|
||||
markerPath = tmpMarkerPath
|
||||
}
|
||||
|
||||
// TODO: check markerPath must be a file
|
||||
if uploadIDMarker != "" {
|
||||
markerPath = filepath.Join(markerPath, uploadIDMarker+multipartUploadIDSuffix)
|
||||
}
|
||||
|
||||
// TODO: check if markerPath starts with bucketDir
|
||||
// TODO: check if markerPath starts with prefixPath
|
||||
|
||||
// Case #1: if markerPath is /mnt/mys3/mybucket/2012/photos/gophercon.png, then
|
||||
// scanDir is /mnt/mys3/mybucket/2012/photos
|
||||
// Case #2: if markerPath is /mnt/mys3/mybucket/2012/photos/gophercon.png/1fbd117a-268a-4ed0-85c9-8cc3888cbf20.uploadid, then
|
||||
// scanDir is /mnt/mys3/mybucket/2012/photos/gophercon.png
|
||||
// Case #3: if markerPath is /mnt/mys3/mybucket/2012/photos/, then
|
||||
// scanDir is /mnt/mys3/mybucket/2012/photos
|
||||
|
||||
scanDir = filepath.Dir(markerPath)
|
||||
} else {
|
||||
markerPath = bucketDir
|
||||
}
|
||||
|
||||
// Have bucketDir ends with os.PathSeparator
|
||||
if !strings.HasSuffix(bucketDir, string(os.PathSeparator)) {
|
||||
bucketDir += string(os.PathSeparator)
|
||||
}
|
||||
|
||||
// Remove os.PathSeparator if scanDir ends with
|
||||
if strings.HasSuffix(scanDir, string(os.PathSeparator)) {
|
||||
scanDir = filepath.Dir(scanDir)
|
||||
}
|
||||
|
||||
// goroutine - retrieves directory entries, makes ObjectInfo and sends into the channel.
|
||||
go func() {
|
||||
defer close(objectInfoCh)
|
||||
|
||||
// send function - returns true if ObjectInfo is sent
|
||||
// within (time.Second * 15) else false on timeout.
|
||||
send := func(oi multipartObjectInfo) bool {
|
||||
timer := time.After(time.Second * 15)
|
||||
select {
|
||||
case objectInfoCh <- oi:
|
||||
return true
|
||||
case <-timer:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// filter function - filters directory entries matching multipart uploadids, prefix and marker
|
||||
direntFilterFn := func(dirent fsDirent) bool {
|
||||
// check if dirent is a directory (or) dirent is a regular file and it's name ends with Upload ID suffix string
|
||||
if dirent.IsDir() || (dirent.IsRegular() && strings.HasSuffix(dirent.name, multipartUploadIDSuffix)) {
|
||||
// return if dirent's name starts with prefixPath and lexically higher than markerPath
|
||||
return strings.HasPrefix(dirent.name, prefixPath) && dirent.name > markerPath
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// filter function - filters directory entries matching multipart uploadids
|
||||
subDirentFilterFn := func(dirent fsDirent) bool {
|
||||
// check if dirent is a directory (or) dirent is a regular file and it's name ends with Upload ID suffix string
|
||||
return dirent.IsDir() || (dirent.IsRegular() && strings.HasSuffix(dirent.name, multipartUploadIDSuffix))
|
||||
}
|
||||
|
||||
// lastObjInfo is used to save last object info which is sent at last with End=true
|
||||
var lastObjInfo *multipartObjectInfo
|
||||
|
||||
sendError := func(err error) {
|
||||
if lastObjInfo != nil {
|
||||
if !send(*lastObjInfo) {
|
||||
// as we got error sending lastObjInfo, we can't send the error
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
send(multipartObjectInfo{Err: err, End: true})
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
dirents, err := scandir(scanDir, direntFilterFn, false)
|
||||
if err != nil {
|
||||
sendError(err)
|
||||
return
|
||||
}
|
||||
|
||||
var dirent fsDirent
|
||||
for len(dirents) > 0 {
|
||||
dirent, dirents = dirents[0], dirents[1:]
|
||||
if dirent.IsRegular() {
|
||||
// Handle uploadid file
|
||||
name := strings.Replace(filepath.Dir(dirent.name), bucketDir, "", 1)
|
||||
if name == "" {
|
||||
// This should not happen ie uploadid file should not be in bucket directory
|
||||
sendError(errors.New("Corrupted metadata"))
|
||||
return
|
||||
}
|
||||
|
||||
uploadID := strings.Split(filepath.Base(dirent.name), multipartUploadIDSuffix)[0]
|
||||
|
||||
// Solaris and older unixes have modTime to be
|
||||
// empty, fallback to os.Stat() to fill missing values.
|
||||
if dirent.modTime.IsZero() {
|
||||
if fi, e := os.Stat(dirent.name); e == nil {
|
||||
dirent.modTime = fi.ModTime()
|
||||
} else {
|
||||
sendError(e)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
objInfo := multipartObjectInfo{
|
||||
Name: name,
|
||||
UploadID: uploadID,
|
||||
ModifiedTime: dirent.modTime,
|
||||
}
|
||||
|
||||
// as we got new object info, send last object info and keep new object info as last object info
|
||||
if lastObjInfo != nil {
|
||||
if !send(*lastObjInfo) {
|
||||
return
|
||||
}
|
||||
}
|
||||
lastObjInfo = &objInfo
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Fetch sub dirents.
|
||||
subDirents, err := scandir(dirent.name, subDirentFilterFn, false)
|
||||
if err != nil {
|
||||
sendError(err)
|
||||
return
|
||||
}
|
||||
|
||||
subDirFound := false
|
||||
uploadIDDirents := []fsDirent{}
|
||||
// If subDirents has a directory, then current dirent needs to be sent
|
||||
for _, subdirent := range subDirents {
|
||||
if subdirent.IsDir() {
|
||||
subDirFound = true
|
||||
|
||||
if recursive {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !recursive && subdirent.IsRegular() {
|
||||
uploadIDDirents = append(uploadIDDirents, subdirent)
|
||||
}
|
||||
}
|
||||
|
||||
// Send directory only for non-recursive listing
|
||||
if !recursive && (subDirFound || len(subDirents) == 0) {
|
||||
// Solaris and older unixes have modTime to be
|
||||
// empty, fallback to os.Stat() to fill missing values.
|
||||
if dirent.modTime.IsZero() {
|
||||
if fi, e := os.Stat(dirent.name); e == nil {
|
||||
dirent.modTime = fi.ModTime()
|
||||
} else {
|
||||
sendError(e)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
objInfo := multipartObjectInfo{
|
||||
Name: strings.Replace(dirent.name, bucketDir, "", 1),
|
||||
ModifiedTime: dirent.modTime,
|
||||
IsDir: true,
|
||||
}
|
||||
|
||||
// as we got new object info, send last object info and keep new object info as last object info
|
||||
if lastObjInfo != nil {
|
||||
if !send(*lastObjInfo) {
|
||||
return
|
||||
}
|
||||
}
|
||||
lastObjInfo = &objInfo
|
||||
}
|
||||
|
||||
if recursive {
|
||||
dirents = append(subDirents, dirents...)
|
||||
} else {
|
||||
dirents = append(uploadIDDirents, dirents...)
|
||||
}
|
||||
}
|
||||
|
||||
if !recursive {
|
||||
break
|
||||
}
|
||||
|
||||
markerPath = scanDir + string(os.PathSeparator)
|
||||
if scanDir = filepath.Dir(scanDir); scanDir < dirDepth {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lastObjInfo != nil {
|
||||
// we got last object
|
||||
lastObjInfo.End = true
|
||||
if !send(*lastObjInfo) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return objectInfoCh
|
||||
}
|
||||
|
||||
// multipartObjectInfo - Multipart object info
|
||||
type multipartObjectInfo struct {
|
||||
Name string
|
||||
UploadID string
|
||||
ModifiedTime time.Time
|
||||
IsDir bool
|
||||
Err error
|
||||
End bool
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user