mirror of
https://github.com/pgsty/minio.git
synced 2026-08-01 08:30:47 +03:00
Compare commits
526 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 8112291d43 | |||
| 93666827f4 | |||
| ac30bef72a | |||
| bea6f33b08 | |||
| 9fb1c79456 | |||
| 6b5699b15f | |||
| 33cd910d3a | |||
| 6af761c86c | |||
| bedd867c0b | |||
| 37330bda98 | |||
| 8603185f2f | |||
| fbd02d530d | |||
| bad2f2afbb | |||
| b182e94acc | |||
| 4e6c4da518 | |||
| e8cd1aad8d | |||
| 8b4a5f07b4 | |||
| ff4e04d942 | |||
| dfba4ff397 | |||
| 06e3171076 | |||
| 2b3a118636 | |||
| 8986a6802a | |||
| 3fcc60de91 | |||
| 9632c94e7a | |||
| 083e4e9479 | |||
| c69fdf0cf2 | |||
| 85ab1df5a8 | |||
| 7623e0f8e8 | |||
| 9843aa1f7a | |||
| 0479d4976b | |||
| 12515eabe2 | |||
| a6a4e7e297 | |||
| 2c793a2ea7 | |||
| 2bb262cc56 | |||
| 379e0abf03 | |||
| 484ba91b08 | |||
| 6037fe66e9 | |||
| ba3a5805c1 | |||
| 33830bfcae | |||
| efc80343e3 | |||
| 272c5165aa | |||
| e318925f62 | |||
| 2395c42fb5 | |||
| 9333dc3294 | |||
| 30fc970eab | |||
| 331890c4c8 | |||
| ae5c65d3c6 | |||
| 02ad48466d | |||
| a84c466a40 | |||
| 956142be37 | |||
| 5201905ad0 | |||
| 186998ad99 | |||
| aa8c9bad54 | |||
| 59ee5a547c | |||
| 1502e2f29f | |||
| 90a46faf31 | |||
| 9dca46e156 | |||
| 663f24064b | |||
| 1b0bc814c4 | |||
| cd5992c6db | |||
| 1ef5ab3c28 | |||
| 5bd47861d6 | |||
| 3538c9f598 | |||
| 24ae5467c8 | |||
| 36267eb6e2 | |||
| 8255590b3c | |||
| 4f6cf5a6b2 | |||
| aaf97ea02c | |||
| 85e50f2bb9 | |||
| c8570edaab | |||
| f1161d830f | |||
| 4aa098ede9 | |||
| 76bda0d8f1 | |||
| 996d2e2a10 | |||
| 2edf32adfa | |||
| e3a3283883 | |||
| 7be79b507b | |||
| 600a932acb | |||
| ff41c050d5 | |||
| 7f993bb5e6 | |||
| 7a97622fed | |||
| da691dc100 | |||
| e2c515b334 | |||
| b55922effe | |||
| 407316c631 | |||
| 902aa05021 | |||
| d95aac4b36 | |||
| fc72a0362f | |||
| 1900d16d22 | |||
| 41cba3a457 | |||
| a90faf5996 | |||
| e7e2fae156 | |||
| 99af0444b7 | |||
| 2357e00317 | |||
| 27c75d8be4 | |||
| 72d364cbf2 | |||
| a729d02c31 | |||
| c80c06680d | |||
| facd5a9ffd | |||
| 34f2c5bcdf | |||
| fd943c704d | |||
| e5b411caf4 | |||
| c784707ed8 | |||
| 88714e7c8e | |||
| 2e3e164f16 | |||
| 15b5f4d7a3 | |||
| 83f9a50137 | |||
| 583e4ecff6 | |||
| b2257682e4 | |||
| 09789de586 | |||
| 9a5e3299fc | |||
| 2c09df24ee | |||
| 53a76439a2 | |||
| 6b0af08885 | |||
| 5282a79eda | |||
| 3d8b9afa8f | |||
| 52751d81cb | |||
| c81f4b0228 | |||
| ec8c1d4ef6 | |||
| 62bd44f873 | |||
| b5c77b641d | |||
| fdf3d64793 | |||
| 373d335d94 | |||
| b16025abf4 | |||
| 166ef09c3d | |||
| e54aa10201 | |||
| 5606232567 | |||
| 9352cb87c6 | |||
| e781959d5b | |||
| af295f3600 | |||
| c70bc2209e | |||
| 5cb546d288 | |||
| 010e775b17 | |||
| 2cba605514 | |||
| d740d3aaae | |||
| 5ac4afa4d1 | |||
| ea7ea427ca | |||
| 027f7efbdb | |||
| d5057b3c51 | |||
| 846410c563 | |||
| f268762ec7 | |||
| 41739e0913 | |||
| cd3eb63c4a | |||
| a5d0bef4e2 | |||
| 114f9de5eb | |||
| 761cb2c740 | |||
| 59fbb6d081 | |||
| fab45aae40 | |||
| 7399d8ceaa | |||
| 0a0451a0fb | |||
| 294ea814bf | |||
| d54a8a9c07 | |||
| aed62788d9 | |||
| 6f842124ad | |||
| adf74ffdb0 | |||
| e883583804 | |||
| 164dfe2ec9 | |||
| 91800cff53 | |||
| 44b2037667 | |||
| 8bf882e5c0 | |||
| 95d3ecb9ce | |||
| 97472e76b0 | |||
| df0bce374c | |||
| 356b889e66 | |||
| f111997184 | |||
| 5f2cfdfbe2 | |||
| 17d145df3a | |||
| b37fbabe7f | |||
| d7767d6431 | |||
| 6651f5b368 | |||
| dc622674e1 | |||
| c7021f6a95 | |||
| 53ca192fe7 | |||
| ee1b86e517 | |||
| 0b2e449727 | |||
| 3ff8a1b719 | |||
| 2520298734 | |||
| af7170675d | |||
| 7780a1ce4c | |||
| ae6e774377 | |||
| 2ec211e52a | |||
| 9122f06307 | |||
| 04d6408c31 | |||
| 024c00addd | |||
| 2c26b98242 | |||
| 223245cc45 | |||
| 781540081d | |||
| acb44e294c | |||
| 408aa72146 | |||
| 04424ae9c2 | |||
| 2181003609 | |||
| 997141d031 | |||
| e509bcb2b9 | |||
| 07da31f8b8 | |||
| 653ceee9ee | |||
| b31dac9162 | |||
| 31c941d320 | |||
| 678585c513 | |||
| 800b19d8e5 | |||
| bd6850e79f | |||
| 18fd0a0f81 | |||
| 8824b18c29 | |||
| 5da1972d1f | |||
| d3966d1dde | |||
| 393636f74b | |||
| 91a7b13529 | |||
| 185d29a899 | |||
| 342f8fbe5d | |||
| 4db136c19c | |||
| a18620fa86 | |||
| d4c91426a7 | |||
| 51e611a380 | |||
| 354229732b | |||
| 1c33926239 | |||
| 91a092792a | |||
| d561f0cc0b | |||
| f53e9dd1b8 | |||
| 2a6bc604db | |||
| dd9aaa855c | |||
| 5a9333a67b | |||
| b531bb31bb | |||
| 9e10ee7e47 | |||
| a173313bc2 | |||
| bbca70e13b | |||
| e73301944a | |||
| fbab7128d5 | |||
| d38bbf3562 | |||
| e59ceba51b | |||
| 40e8893fb9 | |||
| ebdbe2db44 | |||
| 3fd637c1f4 | |||
| 56ffe79f25 | |||
| 6ad39cb386 | |||
| f98675660b | |||
| 62f6ffb6db | |||
| f0e2edd632 | |||
| 5e32dec4fb | |||
| 33bd97d581 | |||
| 70bbf4c8ec | |||
| 785a1f0eb0 | |||
| 6e9d73426b | |||
| 5e4b13f4bd | |||
| 255505a83b | |||
| 31dbdb1787 | |||
| 7c38430710 | |||
| 318265ecaf | |||
| 98ee5fcf55 | |||
| 9b29af8bbe | |||
| 4ef01bc225 | |||
| bf6078df13 | |||
| 4e328d7b2c | |||
| 42fcb27308 |
@@ -0,0 +1,13 @@
|
||||
## Expected behaviour
|
||||
|
||||
## Actual behaviour
|
||||
|
||||
## Steps to reproduce the behaviour
|
||||
|
||||
## Minio version
|
||||
- (paste output of `minio version`)
|
||||
|
||||
## System information
|
||||
|
||||
|
||||
|
||||
+6
-2
@@ -9,5 +9,9 @@ site/
|
||||
/.idea/
|
||||
/Minio.iml
|
||||
**/access.log
|
||||
ui-assets.go
|
||||
ui-assets.asc
|
||||
build
|
||||
vendor/**/*.js
|
||||
vendor/**/*.json
|
||||
release
|
||||
.DS_Store
|
||||
*.syso
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"numFilesToCheck": 10,
|
||||
"requiredOrgs": ["minio"]
|
||||
}
|
||||
+3
-3
@@ -1,4 +1,5 @@
|
||||
sudo: false
|
||||
sudo: required
|
||||
dist: trusty
|
||||
language: go
|
||||
|
||||
os:
|
||||
@@ -16,8 +17,7 @@ script:
|
||||
- make test GOFLAGS="-race"
|
||||
|
||||
go:
|
||||
- 1.5.2
|
||||
- 1.5.3
|
||||
- 1.6
|
||||
|
||||
notifications:
|
||||
slack:
|
||||
|
||||
+2
-2
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
## 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
|
||||
```
|
||||
@@ -1,32 +1,28 @@
|
||||
## Running Minio in Docker.
|
||||
|
||||
### Installing Docker.
|
||||
### Run Minio docker image
|
||||
|
||||
## 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
|
||||
sudo apt-get install Docker.io
|
||||
docker run -p 9000:9000 minio/minio /export
|
||||
```
|
||||
|
||||
### Generating `minio configs` for the first time.
|
||||
## 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
|
||||
docker run -p 9000:9001 minio/minio:latest
|
||||
docker run -p 9000:9000 --name minio1 \
|
||||
-v /mnt/export/minio1:/export \
|
||||
-v /mnt/config/minio1:/root/.minio \
|
||||
minio/minio /export
|
||||
```
|
||||
|
||||
### Persist `minio configs`.
|
||||
|
||||
## 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
|
||||
docker commit <running_minio_container_id> minio/my-minio
|
||||
docker stop <running_minio_container_id>
|
||||
```
|
||||
|
||||
### Create a data volume container.
|
||||
|
||||
```bash
|
||||
docker create -v /export --name minio-export minio/my-minio /bin/true
|
||||
```
|
||||
|
||||
You can then use the `--volumes-from` flag to mount the `/export` volume in another container.
|
||||
|
||||
```bash
|
||||
docker run -p 9000:9001 --volumes-from minio-export --name minio1 minio/my-minio
|
||||
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 /export
|
||||
```
|
||||
|
||||
+14
-8
@@ -1,9 +1,15 @@
|
||||
# use "make dockerimage" to build
|
||||
FROM scratch
|
||||
ENV DOCKERIMAGE 1
|
||||
ADD minio.dockerimage /minio
|
||||
ADD export /export
|
||||
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/Docker.md /export/docker/
|
||||
|
||||
EXPOSE 9000
|
||||
EXPOSE 9001
|
||||
ENTRYPOINT ["/minio"]
|
||||
CMD ["server", "/export"]
|
||||
ENTRYPOINT ["go-wrapper", "run", "server"]
|
||||
CMD ["/export"]
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
### 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`
|
||||
```
|
||||
zfs_enable="YES"
|
||||
```
|
||||
|
||||
Start ZFS service
|
||||
```
|
||||
# service zfs start
|
||||
```
|
||||
|
||||
```
|
||||
# dd if=/dev/zero of=/zfs bs=1M count=4000
|
||||
```
|
||||
|
||||
Configure a loopback device on the `/zfs` file.
|
||||
```
|
||||
# mdconfig -a -t vnode -f /zfs
|
||||
```
|
||||
|
||||
Create zfs pool
|
||||
```
|
||||
# zpool create minio-example /dev/md0
|
||||
```
|
||||
|
||||
```
|
||||
# df /minio-example
|
||||
Filesystem 512-blocks Used Avail Capacity Mounted on
|
||||
minio-example 7872440 38 7872402 0% /minio-example
|
||||
```
|
||||
|
||||
Verify if it is writable
|
||||
```
|
||||
# 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 futher 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.
|
||||
|
||||
|
||||
```
|
||||
# zfs create minio-example/compressed-objects
|
||||
# zfs set compression=lz4 minio-example/compressed-objects
|
||||
```
|
||||
|
||||
To keep monitoring your pool use
|
||||
|
||||
```
|
||||
# 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
|
||||
|
||||
```
|
||||
# chown -R minio-user:minio-user /minio-example/compressed-objects
|
||||
```
|
||||
|
||||
Now login as ``minio-user`` and start minio server.
|
||||
|
||||
```
|
||||
$ curl https://dl.minio.io/server/minio/release/freebsd-amd64/minio > minio
|
||||
$ chmod 755 minio
|
||||
$ ./minio server /minio-example/compressed-objects
|
||||
|
||||
AccessKey: X3RU3Q6B4T20TYB281W5 SecretKey: bvp0DF8c+6MzL60UxFqOZGlA5Z8UPfIAATUQuzLS Region: us-east-1
|
||||
|
||||
Minio Object Storage:
|
||||
http://159.203.234.49:9000
|
||||
http://10.12.0.5:9000
|
||||
http://127.0.0.1:9000
|
||||
|
||||
Minio Browser:
|
||||
http://159.203.234.49:9000
|
||||
http://10.12.0.5:9000
|
||||
http://127.0.0.1:9000
|
||||
|
||||
To configure Minio Client:
|
||||
$ wget https://dl.minio.io/client/mc/release/freebsd-amd64/mc
|
||||
$ chmod 755 mc
|
||||
$ ./mc config host add myminio http://localhost:9000 X3RU3Q6B4T20TYB281W5 bvp0DF8c+6MzL60UxFqOZGlA5Z8UPfIAATUQuzLS
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
$ 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:
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
$ 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.
|
||||
+6
-6
@@ -7,15 +7,15 @@ This installation document assumes Ubuntu 14.04+ on x86-64 platform.
|
||||
$ sudo apt-get install git build-essential
|
||||
```
|
||||
|
||||
##### Install Go 1.5+
|
||||
##### Install Go 1.6+
|
||||
|
||||
Download Go 1.5+ from [https://golang.org/dl/](https://golang.org/dl/).
|
||||
Download Go 1.6+ from [https://golang.org/dl/](https://golang.org/dl/).
|
||||
|
||||
```sh
|
||||
$ wget https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz
|
||||
$ 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.5.1.linux-amd64.tar.gz
|
||||
$ tar -C ${HOME}/bin/ -xzf go1.6.linux-amd64.tar.gz
|
||||
```
|
||||
##### Setup GOROOT and GOPATH
|
||||
|
||||
@@ -25,7 +25,7 @@ and GOPATH specifies the location of your project workspace.
|
||||
```sh
|
||||
export GOROOT=${HOME}/bin/go
|
||||
export GOPATH=${HOME}/go
|
||||
export PATH=$PATH:${HOME}/bin/go/bin:${GOPATH}/bin
|
||||
export PATH=${HOME}/bin/go/bin:${GOPATH}/bin:$PATH
|
||||
```
|
||||
##### Source the new enviornment
|
||||
|
||||
@@ -71,7 +71,7 @@ and GOPATH specifies the location of your project workspace.
|
||||
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=$PATH:${GOPATH}/bin
|
||||
export PATH=${GOPATH}/bin:$PATH
|
||||
```
|
||||
##### Source the new enviornment
|
||||
```sh
|
||||
|
||||
+1
-1
@@ -34,4 +34,4 @@ $
|
||||
|
||||
`minio` doesn't follow semantic versioning style, `minio` instead uses the release date and time as the release versions.
|
||||
|
||||
`make release` will install new released binary into your `GOPATH`
|
||||
`make release` will generate new binary into `release` directory.
|
||||
|
||||
@@ -1,39 +1,79 @@
|
||||
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
|
||||
UI_ASSETS := ui-assets.go
|
||||
UI_ASSETS_ARMOR := ui-assets.asc
|
||||
|
||||
HOST ?= $(shell uname)
|
||||
CPU ?= $(shell uname -m)
|
||||
|
||||
# if no host is identifed (no uname tool)
|
||||
# we assume a Linux-64bit build
|
||||
ifeq ($(HOST),)
|
||||
HOST = Linux
|
||||
endif
|
||||
|
||||
# identify CPU
|
||||
ifeq ($(CPU), x86_64)
|
||||
HOST := $(HOST)64
|
||||
else
|
||||
ifeq ($(CPU), amd64)
|
||||
HOST := $(HOST)64
|
||||
else
|
||||
ifeq ($(CPU), i686)
|
||||
HOST := $(HOST)32
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
#############################################
|
||||
# now we find out the target OS for
|
||||
# which we are going to compile in case
|
||||
# the caller didn't yet define OS himself
|
||||
ifndef (OS)
|
||||
ifeq ($(HOST), Linux64)
|
||||
arch = gcc
|
||||
else
|
||||
ifeq ($(HOST), Linux32)
|
||||
arch = 32
|
||||
else
|
||||
ifeq ($(HOST), Darwin64)
|
||||
arch = clang
|
||||
else
|
||||
ifeq ($(HOST), Darwin32)
|
||||
arch = clang
|
||||
else
|
||||
ifeq ($(HOST), FreeBSD64)
|
||||
arch = gcc
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
all: install
|
||||
|
||||
checkdeps:
|
||||
checks:
|
||||
@echo "Checking deps:"
|
||||
@(env bash $(PWD)/buildscripts/checkdeps.sh)
|
||||
|
||||
checkgopath:
|
||||
@echo "Checking if project is at ${GOPATH}"
|
||||
@for miniopath in $(echo ${GOPATH} | sed 's/:/\n/g'); do if [ ! -d ${miniopath}/src/github.com/minio/minio ]; then echo "Project not found in ${miniopath}, please follow instructions provided at https://github.com/minio/minio/blob/master/CONTRIBUTING.md#setup-your-minio-github-repository" && exit 1; fi done
|
||||
@echo "Setting BUILD_LDFLAGS: ${BUILD_LDFLAGS}"
|
||||
@(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: checkdeps checkgopath
|
||||
getdeps: checks
|
||||
@go get -u github.com/golang/lint/golint && echo "Installed golint:"
|
||||
@go get -u golang.org/x/tools/cmd/vet && echo "Installed vet:"
|
||||
@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:"
|
||||
|
||||
$(UI_ASSETS):
|
||||
@curl -s https://dl.minio.io/assets/server/$(UI_ASSETS_ARMOR) 2>&1 > $(UI_ASSETS_ARMOR) && echo "Downloading signature file $(UI_ASSETS_ARMOR) for verification:"
|
||||
@gpg --batch --no-tty --yes --keyserver pgp.mit.edu --recv-keys F9AAC728 2>&1 > /dev/null && echo "Importing public key:"
|
||||
@curl -s https://dl.minio.io/assets/server/$@ 2>&1 > $@ && echo "Downloading UI assets file $@:"
|
||||
@gpg --batch --no-tty --verify $(UI_ASSETS_ARMOR) $@ 2>&1 > /dev/null && echo "Verifying signature of downloaded assets."
|
||||
|
||||
verifiers: getdeps vet fmt lint cyclo
|
||||
verifiers: vet fmt lint cyclo spelling
|
||||
|
||||
vet:
|
||||
@echo "Running $@:"
|
||||
@@ -52,52 +92,64 @@ 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
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/gocyclo -over 65 pkg
|
||||
|
||||
build: getdeps verifiers $(UI_ASSETS)
|
||||
@echo "Installing minio:"
|
||||
|
||||
deadcode:
|
||||
@GO15VENDOREXPERIMENT=1 deadcode
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/deadcode
|
||||
|
||||
spelling:
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell *.go
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell pkg/**/*
|
||||
|
||||
test: build
|
||||
@echo "Running all testing:"
|
||||
@echo "Running all minio testing:"
|
||||
@GO15VENDOREXPERIMENT=1 go test $(GOFLAGS) .
|
||||
@GO15VENDOREXPERIMENT=1 go test $(GOFLAGS) github.com/minio/minio/pkg...
|
||||
|
||||
gomake-all: build
|
||||
@echo "Installing minio:"
|
||||
@GO15VENDOREXPERIMENT=1 go build --ldflags $(BUILD_LDFLAGS) -o $(GOPATH)/bin/minio
|
||||
|
||||
pkg-add:
|
||||
@GO15VENDOREXPERIMENT=1 govendor add $(PKG)
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/govendor add $(PKG)
|
||||
|
||||
pkg-update:
|
||||
@GO15VENDOREXPERIMENT=1 govendor update $(PKG)
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/govendor update $(PKG)
|
||||
|
||||
pkg-remove:
|
||||
@GO15VENDOREXPERIMENT=1 govendor remove $(PKG)
|
||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/govendor remove $(PKG)
|
||||
|
||||
pkg-list:
|
||||
@GO15VENDOREXPERIMENT=1 $(GOPATH)/bin/govendor list
|
||||
|
||||
install: gomake-all
|
||||
|
||||
dockerimage: checkdocker verifiers $(UI_ASSETS)
|
||||
dockerimage: checkdocker getdeps verifiers $(UI_ASSETS)
|
||||
@echo "Building docker image:" minio:$(TAG)
|
||||
@GO15VENDOREXPERIMENT=1 go build --ldflags $(DOCKER_LDFLAGS) -o minio.dockerimage
|
||||
@mkdir -p export
|
||||
@sudo docker build --rm --tag=minio/minio:$(TAG) .
|
||||
@rmdir export
|
||||
@rm minio.dockerimage
|
||||
@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:
|
||||
@./release.sh
|
||||
release: verifiers
|
||||
@MINIO_RELEASE=RELEASE ./buildscripts/build.sh
|
||||
|
||||
experimental: verifiers
|
||||
@MINIO_RELEASE=EXPERIMENTAL ./buildscripts/build.sh
|
||||
|
||||
clean:
|
||||
@echo "Cleaning up all the generated files:"
|
||||
@rm -fv cover.out
|
||||
@rm -fv minio
|
||||
@rm -fv minio.test
|
||||
@rm -fv pkg/fs/fs.test
|
||||
@rm -fv ui-assets.go
|
||||
@rm -fv ui-assets.asc
|
||||
@rm -fv minio minio.test cover.out
|
||||
@find . -name '*.test' | xargs rm -fv
|
||||
@rm -rf isa-l
|
||||
@rm -rf build
|
||||
@rm -rf release
|
||||
|
||||
@@ -5,5 +5,5 @@ This product includes software developed at Minio, Inc.
|
||||
|
||||
The Minio project contains unmodified/modified subcomponents too with
|
||||
separate copyright notices and license terms. Your use of the source
|
||||
code for the these subcomponents is subject to the terms and conditions
|
||||
code for these subcomponents is subject to the terms and conditions
|
||||
of the following licenses.
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
## Minio [](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
Minio is a distributed object storage server written in Golang. Source is available under free software / open source [Apache license 2.0](./LICENSE). API compatible with Amazon S3 cloud storage service.
|
||||
Minio is an object storage server compatible with Amazon S3 and licensed under [Apache license 2.0](./LICENSE).
|
||||
|
||||
## Description
|
||||
|
||||
Micro services environment provisions one Minio server per application instance. Scalability is achieved through large number of smaller personalized instances. This version of the Minio binary is built using Filesystem storage backend for magnetic and solid state disks.
|
||||
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 currently implements two backends
|
||||
|
||||
- Filesystem (FS) - is available and ready for general purpose use.
|
||||
- ErasureCoded (XL) - is available, but it is not ready for general purpose use.
|
||||
|
||||
## Minio Client
|
||||
|
||||
@@ -52,6 +58,16 @@ Download ``minio`` for:
|
||||
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
|
||||
~~~
|
||||
Read more here on [How to configure Minio on FreeBSD with ZFS backend.](./FreeBSD.md)
|
||||
|
||||
#### Docker container
|
||||
|
||||
Download ``minio`` for docker.
|
||||
@@ -64,7 +80,7 @@ Read more here on [How to configure data volume containers for Minio?](./Docker.
|
||||
|
||||
#### 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.
|
||||
NOTE: Source installation is intended for only developers and advanced users. For general use, please download official releases from https://minio.io/downloads.
|
||||
</blockquote>
|
||||
|
||||
If you do not have a working Golang environment, please follow [Install Golang](./INSTALLGO.md).
|
||||
@@ -77,46 +93,20 @@ $ make
|
||||
|
||||
### How to use Minio?
|
||||
|
||||
```
|
||||
NAME:
|
||||
minio server - Start Minio cloud storage server.
|
||||
|
||||
USAGE:
|
||||
minio server [OPTION VALUE] PATH
|
||||
|
||||
OPTION = expiry VALUE = NN[h|m|s] [DEFAULT=Unlimited]
|
||||
OPTION = min-free-disk VALUE = NN% [DEFAULT: 10%]
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio server on Linux.
|
||||
$ minio server /home/shared
|
||||
|
||||
2. Start minio server on Windows.
|
||||
$ minio server C:\MyShare
|
||||
|
||||
3. Start minio server bound to a specific IP:PORT, when you have multiple network interfaces.
|
||||
$ minio --address 192.168.1.101:9000 server /home/shared
|
||||
|
||||
4. Start minio server with minimum free disk threshold to 5%
|
||||
$ minio server min-free-disk 5% /home/shared/Pictures
|
||||
|
||||
5. Start minio server with minimum free disk threshold to 15% with auto expiration set to 1h
|
||||
$ minio server min-free-disk 15% expiry 1h /home/shared/Documents
|
||||
```
|
||||
|
||||
#### Start Minio server.
|
||||
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.0.0.3:9000
|
||||
http://10.1.10.177:9000
|
||||
|
||||
Minio Browser:
|
||||
http://127.0.0.1:9000
|
||||
http://10.0.0.3:9000
|
||||
http://10.1.10.177:9000
|
||||
|
||||
To configure Minio Client:
|
||||
$ wget https://dl.minio.io/client/mc/release/darwin-amd64/mc
|
||||
@@ -149,16 +139,41 @@ $ 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
|
||||
2016-03-27 02:06:30 deebucket
|
||||
2016-03-28 21:53:49 guestbucket
|
||||
2016-03-29 13:34:34 mbtest
|
||||
2016-03-26 22:01:36 mybucket
|
||||
2016-03-26 15:37:02 testbucket
|
||||
```
|
||||
|
||||
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
|
||||
$ aws --endpoint-url http://localhost:9000 s3 ls s3://mybucket
|
||||
2016-03-30 00:26:53 69297 argparse-1.2.1.tar.gz
|
||||
2016-03-30 00:35:37 67250 simplejson-3.3.0.tar.gz
|
||||
```
|
||||
|
||||
To make a bucket.
|
||||
```
|
||||
$ aws --endpoint-url http://localhost:9000 s3 mb s3://mybucket
|
||||
make_bucket: s3://mybucket/
|
||||
```
|
||||
|
||||
To add an object to a bucket.
|
||||
```
|
||||
$ aws --endpoint-url http://localhost:9000 s3 cp simplejson-3.3.0.tar.gz s3://mybucket
|
||||
upload: ./simplejson-3.3.0.tar.gz to s3://mybucket/simplejson-3.3.0.tar.gz
|
||||
```
|
||||
|
||||
Delete an object from a bucket.
|
||||
```
|
||||
$ aws --endpoint-url http://localhost:9000 s3 rm s3://mybucket/argparse-1.2.1.tar.gzdelete: s3://mybucket/argparse-1.2.1.tar.gz
|
||||
```
|
||||
|
||||
Remove a bucket.
|
||||
```
|
||||
$ aws --endpoint-url http://localhost:9000 s3 rb s3://mybucket
|
||||
remove_bucket: s3://mybucket/
|
||||
```
|
||||
|
||||
#### How to use AWS SDK with Minio?
|
||||
@@ -178,21 +193,65 @@ host_base = localhost:9000
|
||||
host_bucket = localhost:9000
|
||||
access_key = YOUR_ACCESS_KEY_HERE
|
||||
secret_key = YOUR_SECRET_KEY_HERE
|
||||
signature_v2 = False
|
||||
bucket_location = us-east-1
|
||||
```
|
||||
|
||||
To list your buckets.
|
||||
To make a bucket.
|
||||
```
|
||||
$ s3cmd mb s3://mybucket
|
||||
Bucket 's3://mybucket/' created
|
||||
```
|
||||
|
||||
To copy an object to bucket.
|
||||
```
|
||||
$ s3cmd put newfile s3://testbucket
|
||||
upload: 'newfile' -> 's3://testbucket/newfile'
|
||||
```
|
||||
|
||||
To copy an object to local system.
|
||||
```
|
||||
$ s3cmd get s3://testbucket/newfile
|
||||
download: 's3://testbucket/newfile' -> './newfile'
|
||||
```
|
||||
|
||||
To sync local file/directory to a bucket.
|
||||
```
|
||||
$ s3cmd sync newdemo s3://testbucket
|
||||
upload: 'newdemo/newdemofile.txt' -> 's3://testbucket/newdemo/newdemofile.txt'
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
To list contents inside buckets.
|
||||
To list contents inside bucket.
|
||||
```
|
||||
$ s3cmd ls s3://testbucket/
|
||||
DIR s3://testbucket/test/
|
||||
2015-12-09 16:05 138504 s3://testbucket/newfile
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// credential container for access and secret keys.
|
||||
type credential struct {
|
||||
AccessKeyID string `json:"accessKey"`
|
||||
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
|
||||
)
|
||||
|
||||
// isValidSecretKey - validate secret key.
|
||||
var isValidSecretKey = regexp.MustCompile(`^.{8,40}$`)
|
||||
|
||||
// isValidAccessKey - validate access key.
|
||||
var isValidAccessKey = regexp.MustCompile(`^[a-zA-Z0-9\\-\\.\\_\\~]{5,20}$`)
|
||||
|
||||
// mustGenAccessKeys - must generate access credentials.
|
||||
func mustGenAccessKeys() (creds credential) {
|
||||
creds, err := genAccessKeys()
|
||||
fatalIf(err, "Unable to generate access keys.")
|
||||
return creds
|
||||
}
|
||||
|
||||
// genAccessKeys - generate access credentials.
|
||||
func genAccessKeys() (credential, error) {
|
||||
accessKeyID, err := genAccessKeyID()
|
||||
if err != nil {
|
||||
return credential{}, err
|
||||
}
|
||||
secretAccessKey, err := genSecretAccessKey()
|
||||
if err != nil {
|
||||
return credential{}, err
|
||||
}
|
||||
creds := credential{
|
||||
AccessKeyID: string(accessKeyID),
|
||||
SecretAccessKey: string(secretAccessKey),
|
||||
}
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
// genAccessKeyID - generate random alpha numeric value using only uppercase characters
|
||||
// takes input as size in integer
|
||||
func genAccessKeyID() ([]byte, error) {
|
||||
alpha := make([]byte, minioAccessID)
|
||||
if _, err := rand.Read(alpha); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < minioAccessID; i++ {
|
||||
alpha[i] = alphaNumericTable[alpha[i]%byte(len(alphaNumericTable))]
|
||||
}
|
||||
return alpha, nil
|
||||
}
|
||||
|
||||
// genSecretAccessKey - generate random base64 numeric value from a random seed.
|
||||
func genSecretAccessKey() ([]byte, error) {
|
||||
rb := make([]byte, minioSecretID)
|
||||
if _, err := rand.Read(rb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(base64.StdEncoding.EncodeToString(rb))[:minioSecretID], nil
|
||||
}
|
||||
+9
-11
@@ -22,8 +22,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-xl/pkg/probe"
|
||||
)
|
||||
|
||||
type accessLogHandler struct {
|
||||
@@ -60,14 +58,14 @@ type LogMessage struct {
|
||||
|
||||
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)
|
||||
fatalIf(err, "Unable to parse HTTP request and response fields.")
|
||||
_, err = h.accessLogFile.Write(message)
|
||||
fatalIf(err, "Unable to log HTTP access.")
|
||||
|
||||
h.Handler.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func getLogMessage(w http.ResponseWriter, req *http.Request) ([]byte, *probe.Error) {
|
||||
func getLogMessage(w http.ResponseWriter, req *http.Request) ([]byte, error) {
|
||||
logMessage := &LogMessage{
|
||||
StartTime: time.Now().UTC(),
|
||||
}
|
||||
@@ -103,9 +101,9 @@ func getLogMessage(w http.ResponseWriter, req *http.Request) ([]byte, *probe.Err
|
||||
|
||||
// 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, err := json.Marshal(logMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
js = append(js, byte('\n')) // append a new line
|
||||
return js, nil
|
||||
@@ -113,8 +111,8 @@ func getLogMessage(w http.ResponseWriter, req *http.Request) ([]byte, *probe.Err
|
||||
|
||||
// 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)
|
||||
file, err := os.OpenFile("access.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
fatalIf(err, "Failed top open access log.")
|
||||
|
||||
return &accessLogHandler{Handler: h, accessLogFile: file}
|
||||
}
|
||||
|
||||
@@ -1,76 +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 (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"regexp"
|
||||
|
||||
"github.com/minio/minio-xl/pkg/probe"
|
||||
)
|
||||
|
||||
const (
|
||||
minioAccessID = 20
|
||||
minioSecretID = 40
|
||||
)
|
||||
|
||||
// isValidAccessKey - validate access key
|
||||
func isValidAccessKey(accessKeyID string) bool {
|
||||
if accessKeyID == "" {
|
||||
return true
|
||||
}
|
||||
regex := regexp.MustCompile("^[A-Z0-9\\-\\.\\_\\~]{20}$")
|
||||
return regex.MatchString(accessKeyID)
|
||||
}
|
||||
|
||||
// generateAccessKeyID - generate random alpha numeric value using only uppercase characters
|
||||
// takes input as size in integer
|
||||
func generateAccessKeyID() ([]byte, *probe.Error) {
|
||||
alpha := make([]byte, minioAccessID)
|
||||
if _, e := rand.Read(alpha); e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
for i := 0; i < minioAccessID; i++ {
|
||||
alpha[i] = alphaNumericTable[alpha[i]%byte(len(alphaNumericTable))]
|
||||
}
|
||||
return alpha, nil
|
||||
}
|
||||
|
||||
// generateSecretAccessKey - generate random base64 numeric value from a random seed.
|
||||
func generateSecretAccessKey() ([]byte, *probe.Error) {
|
||||
rb := make([]byte, minioSecretID)
|
||||
if _, e := rand.Read(rb); e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
return []byte(base64.StdEncoding.EncodeToString(rb))[:minioSecretID], nil
|
||||
}
|
||||
|
||||
// mustGenerateAccessKeyID - must generate random alpha numeric value using only uppercase characters
|
||||
// takes input as size in integer
|
||||
func mustGenerateAccessKeyID() []byte {
|
||||
alpha, err := generateAccessKeyID()
|
||||
fatalIf(err.Trace(), "Unable to generate accessKeyID.", nil)
|
||||
return alpha
|
||||
}
|
||||
|
||||
// mustGenerateSecretAccessKey - generate random base64 numeric value from a random seed.
|
||||
func mustGenerateSecretAccessKey() []byte {
|
||||
secretKey, err := generateSecretAccessKey()
|
||||
fatalIf(err.Trace(), "Unable to generate secretAccessKey.", nil)
|
||||
return secretKey
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// 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
|
||||
Quiet bool
|
||||
// List of objects to be deleted
|
||||
Objects []ObjectIdentifier `xml:"Object"`
|
||||
}
|
||||
@@ -1,225 +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 "encoding/xml"
|
||||
|
||||
// Limit number of objects in a given response.
|
||||
const (
|
||||
maxObjectList = 1000
|
||||
)
|
||||
|
||||
// LocationResponse - format for location response.
|
||||
type LocationResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ LocationConstraint" json:"-"`
|
||||
Location string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// AccessControlPolicyResponse - format for get bucket acl response.
|
||||
type AccessControlPolicyResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ AccessControlPolicy" json:"-"`
|
||||
|
||||
AccessControlList struct {
|
||||
Grant []Grant
|
||||
}
|
||||
Owner Owner
|
||||
}
|
||||
|
||||
// Grant container for grantee and permission.
|
||||
type Grant struct {
|
||||
Grantee struct {
|
||||
ID string
|
||||
DisplayName string
|
||||
EmailAddress string
|
||||
Type string
|
||||
URI string
|
||||
}
|
||||
Permission string
|
||||
}
|
||||
|
||||
// ListObjectsResponse - format for list objects response.
|
||||
type ListObjectsResponse 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
|
||||
Marker 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.
|
||||
NextMarker string
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// Part container for part metadata.
|
||||
type Part struct {
|
||||
PartNumber int
|
||||
ETag string
|
||||
LastModified string
|
||||
Size int64
|
||||
}
|
||||
|
||||
// ListPartsResponse - format for list parts response.
|
||||
type ListPartsResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListPartsResult" json:"-"`
|
||||
|
||||
Bucket string
|
||||
Key string
|
||||
UploadID string `xml:"UploadId"`
|
||||
|
||||
Initiator Initiator
|
||||
Owner Owner
|
||||
|
||||
// The class of storage used to store the object.
|
||||
StorageClass string
|
||||
|
||||
PartNumberMarker int
|
||||
NextPartNumberMarker int
|
||||
MaxParts int
|
||||
IsTruncated bool
|
||||
|
||||
// List of parts.
|
||||
Part []*Part
|
||||
}
|
||||
|
||||
// ListMultipartUploadsResponse - format for list multipart uploads response.
|
||||
type ListMultipartUploadsResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListMultipartUploadsResult" json:"-"`
|
||||
|
||||
Bucket string
|
||||
KeyMarker string
|
||||
UploadIDMarker string `xml:"UploadIdMarker"`
|
||||
NextKeyMarker string
|
||||
NextUploadIDMarker string `xml:"NextUploadIdMarker"`
|
||||
EncodingType string
|
||||
MaxUploads int
|
||||
IsTruncated bool
|
||||
Upload []*Upload
|
||||
Prefix string
|
||||
Delimiter string
|
||||
CommonPrefixes []*CommonPrefix
|
||||
}
|
||||
|
||||
// ListBucketsResponse - format for list buckets response
|
||||
type ListBucketsResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListAllMyBucketsResult" json:"-"`
|
||||
// Container for one or more buckets.
|
||||
Buckets struct {
|
||||
Bucket []*Bucket
|
||||
} // Buckets are nested
|
||||
Owner Owner
|
||||
}
|
||||
|
||||
// Upload container for in progress multipart upload
|
||||
type Upload struct {
|
||||
Key string
|
||||
UploadID string `xml:"UploadId"`
|
||||
Initiator Initiator
|
||||
Owner Owner
|
||||
StorageClass string
|
||||
Initiated string
|
||||
}
|
||||
|
||||
// CommonPrefix container for prefix response in ListObjectsResponse
|
||||
type CommonPrefix struct {
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// Bucket container for bucket metadata
|
||||
type Bucket struct {
|
||||
Name string
|
||||
CreationDate string
|
||||
}
|
||||
|
||||
// Object container for object metadata
|
||||
type Object struct {
|
||||
ETag string
|
||||
Key string
|
||||
LastModified string
|
||||
Size int64
|
||||
|
||||
Owner Owner
|
||||
|
||||
// The class of storage used to store the object.
|
||||
StorageClass string
|
||||
}
|
||||
|
||||
// Initiator inherit from Owner struct, fields are same
|
||||
type Initiator Owner
|
||||
|
||||
// Owner - bucket owner/principal
|
||||
type Owner struct {
|
||||
ID string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
// InitiateMultipartUploadResponse container for InitiateMultiPartUpload response, provides uploadID to start MultiPart upload
|
||||
type InitiateMultipartUploadResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ InitiateMultipartUploadResult" json:"-"`
|
||||
|
||||
Bucket string
|
||||
Key string
|
||||
UploadID string `xml:"UploadId"`
|
||||
}
|
||||
|
||||
// CompleteMultipartUploadResponse container for completed multipart upload response
|
||||
type CompleteMultipartUploadResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CompleteMultipartUploadResult" json:"-"`
|
||||
|
||||
Location string
|
||||
Bucket string
|
||||
Key string
|
||||
ETag string
|
||||
}
|
||||
|
||||
// List of not implemented bucket queries
|
||||
var notimplementedBucketResourceNames = map[string]bool{
|
||||
"policy": true,
|
||||
"cors": true,
|
||||
"lifecycle": true,
|
||||
"logging": true,
|
||||
"notification": true,
|
||||
"replication": true,
|
||||
"tagging": true,
|
||||
"versions": true,
|
||||
"requestPayment": true,
|
||||
"versioning": true,
|
||||
"website": true,
|
||||
}
|
||||
|
||||
// List of not implemented object queries
|
||||
var notimplementedObjectResourceNames = map[string]bool{
|
||||
"torrent": true,
|
||||
"acl": true,
|
||||
"policy": true,
|
||||
}
|
||||
+298
-78
@@ -40,220 +40,440 @@ type APIErrorResponse struct {
|
||||
HostID string `xml:"HostId"`
|
||||
}
|
||||
|
||||
// APIErrorCode type of error status.
|
||||
type APIErrorCode int
|
||||
|
||||
// Error codes, non exhaustive list - http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
|
||||
const (
|
||||
AccessDenied = iota
|
||||
BadDigest
|
||||
BucketAlreadyExists
|
||||
EntityTooSmall
|
||||
EntityTooLarge
|
||||
IncompleteBody
|
||||
InternalError
|
||||
InvalidAccessKeyID
|
||||
InvalidBucketName
|
||||
InvalidDigest
|
||||
InvalidRange
|
||||
InvalidMaxKeys
|
||||
InvalidMaxUploads
|
||||
InvalidMaxParts
|
||||
InvalidPartNumberMarker
|
||||
MalformedXML
|
||||
MissingContentLength
|
||||
MissingRequestBodyError
|
||||
NoSuchBucket
|
||||
NoSuchKey
|
||||
NoSuchUpload
|
||||
NotImplemented
|
||||
RequestTimeTooSkewed
|
||||
SignatureDoesNotMatch
|
||||
MethodNotAllowed
|
||||
InvalidPart
|
||||
InvalidPartOrder
|
||||
AuthorizationHeaderMalformed
|
||||
MalformedPOSTRequest
|
||||
SignatureVersionNotSupported
|
||||
BucketNotEmpty
|
||||
RootPathFull
|
||||
ObjectExistsAsPrefix
|
||||
ErrNone APIErrorCode = iota
|
||||
ErrAccessDenied
|
||||
ErrBadDigest
|
||||
ErrBucketAlreadyExists
|
||||
ErrEntityTooSmall
|
||||
ErrEntityTooLarge
|
||||
ErrIncompleteBody
|
||||
ErrInternalError
|
||||
ErrInvalidAccessKeyID
|
||||
ErrInvalidBucketName
|
||||
ErrInvalidDigest
|
||||
ErrInvalidRange
|
||||
ErrInvalidMaxKeys
|
||||
ErrInvalidMaxUploads
|
||||
ErrInvalidMaxParts
|
||||
ErrInvalidPartNumberMarker
|
||||
ErrInvalidRequestBody
|
||||
ErrInvalidCopySource
|
||||
ErrInvalidCopyDest
|
||||
ErrInvalidPolicyDocument
|
||||
ErrMalformedXML
|
||||
ErrMissingContentLength
|
||||
ErrMissingContentMD5
|
||||
ErrMissingRequestBodyError
|
||||
ErrNoSuchBucket
|
||||
ErrNoSuchBucketPolicy
|
||||
ErrNoSuchKey
|
||||
ErrNoSuchUpload
|
||||
ErrNotImplemented
|
||||
ErrRequestTimeTooSkewed
|
||||
ErrSignatureDoesNotMatch
|
||||
ErrMethodNotAllowed
|
||||
ErrInvalidPart
|
||||
ErrInvalidPartOrder
|
||||
ErrAuthorizationHeaderMalformed
|
||||
ErrMalformedPOSTRequest
|
||||
ErrSignatureVersionNotSupported
|
||||
ErrBucketNotEmpty
|
||||
ErrAllAccessDisabled
|
||||
ErrMalformedPolicy
|
||||
ErrMissingFields
|
||||
ErrMissingCredTag
|
||||
ErrCredMalformed
|
||||
ErrInvalidRegion
|
||||
ErrInvalidService
|
||||
ErrInvalidRequestVersion
|
||||
ErrMissingSignTag
|
||||
ErrMissingSignHeadersTag
|
||||
ErrPolicyAlreadyExpired
|
||||
ErrMalformedDate
|
||||
ErrMalformedExpires
|
||||
ErrAuthHeaderEmpty
|
||||
ErrExpiredPresignRequest
|
||||
ErrMissingDateHeader
|
||||
ErrInvalidQuerySignatureAlgo
|
||||
ErrInvalidQueryParams
|
||||
ErrBucketAlreadyOwnedByYou
|
||||
// Add new error codes here.
|
||||
|
||||
// Minio extended errors.
|
||||
ErrReadQuorum
|
||||
ErrWriteQuorum
|
||||
ErrStorageFull
|
||||
ErrObjectExistsAsDirectory
|
||||
)
|
||||
|
||||
// APIError code to Error structure map
|
||||
var errorCodeResponse = map[int]APIError{
|
||||
InvalidMaxUploads: {
|
||||
// error code to APIError structure, these fields carry respective
|
||||
// descriptions for all the error responses.
|
||||
var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
ErrInvalidCopyDest: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "This copy request is illegal because it is trying to copy an object to itself.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidCopySource: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Copy Source must mention the source bucket and key: sourcebucket/sourcekey.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidRequestBody: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Body shouldn't be set for this request.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidMaxUploads: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Argument maxUploads must be an integer between 0 and 2147483647.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
InvalidMaxKeys: {
|
||||
ErrInvalidMaxKeys: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Argument maxKeys must be an integer between 0 and 2147483647.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
InvalidMaxParts: {
|
||||
ErrInvalidMaxParts: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Argument maxParts must be an integer between 1 and 10000.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
InvalidPartNumberMarker: {
|
||||
ErrInvalidPartNumberMarker: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Argument partNumberMarker must be an integer.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
AccessDenied: {
|
||||
ErrInvalidPolicyDocument: {
|
||||
Code: "InvalidPolicyDocument",
|
||||
Description: "The content of the form does not meet the conditions specified in the policy document.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAccessDenied: {
|
||||
Code: "AccessDenied",
|
||||
Description: "Access Denied.",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
BadDigest: {
|
||||
ErrBadDigest: {
|
||||
Code: "BadDigest",
|
||||
Description: "The Content-MD5 you specified did not match what we received.",
|
||||
Description: "The Content-Md5 you specified did not match what we received.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
BucketAlreadyExists: {
|
||||
ErrBucketAlreadyExists: {
|
||||
Code: "BucketAlreadyExists",
|
||||
Description: "The requested bucket name is not available.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
EntityTooSmall: {
|
||||
ErrEntityTooSmall: {
|
||||
Code: "EntityTooSmall",
|
||||
Description: "Your proposed upload is smaller than the minimum allowed object size.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
EntityTooLarge: {
|
||||
ErrEntityTooLarge: {
|
||||
Code: "EntityTooLarge",
|
||||
Description: "Your proposed upload exceeds the maximum allowed object size.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
IncompleteBody: {
|
||||
ErrIncompleteBody: {
|
||||
Code: "IncompleteBody",
|
||||
Description: "You did not provide the number of bytes specified by the Content-Length HTTP header.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
InternalError: {
|
||||
ErrInternalError: {
|
||||
Code: "InternalError",
|
||||
Description: "We encountered an internal error, please try again.",
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
InvalidAccessKeyID: {
|
||||
ErrInvalidAccessKeyID: {
|
||||
Code: "InvalidAccessKeyID",
|
||||
Description: "The access key ID you provided does not exist in our records.",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
InvalidBucketName: {
|
||||
ErrInvalidBucketName: {
|
||||
Code: "InvalidBucketName",
|
||||
Description: "The specified bucket is not valid.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
InvalidDigest: {
|
||||
ErrInvalidDigest: {
|
||||
Code: "InvalidDigest",
|
||||
Description: "The Content-MD5 you specified is not valid.",
|
||||
Description: "The Content-Md5 you specified is not valid.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
InvalidRange: {
|
||||
ErrInvalidRange: {
|
||||
Code: "InvalidRange",
|
||||
Description: "The requested range cannot be satisfied.",
|
||||
HTTPStatusCode: http.StatusRequestedRangeNotSatisfiable,
|
||||
},
|
||||
MalformedXML: {
|
||||
ErrMalformedXML: {
|
||||
Code: "MalformedXML",
|
||||
Description: "The XML you provided was not well-formed or did not validate against our published schema.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
MissingContentLength: {
|
||||
ErrMissingContentLength: {
|
||||
Code: "MissingContentLength",
|
||||
Description: "You must provide the Content-Length HTTP header.",
|
||||
HTTPStatusCode: http.StatusLengthRequired,
|
||||
},
|
||||
MissingRequestBodyError: {
|
||||
ErrMissingContentMD5: {
|
||||
Code: "MissingContentMD5",
|
||||
Description: "Missing required header for this request: Content-Md5.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingRequestBodyError: {
|
||||
Code: "MissingRequestBodyError",
|
||||
Description: "Request body is empty.",
|
||||
HTTPStatusCode: http.StatusLengthRequired,
|
||||
},
|
||||
NoSuchBucket: {
|
||||
ErrNoSuchBucket: {
|
||||
Code: "NoSuchBucket",
|
||||
Description: "The specified bucket does not exist.",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
NoSuchKey: {
|
||||
ErrNoSuchBucketPolicy: {
|
||||
Code: "NoSuchBucketPolicy",
|
||||
Description: "The specified bucket does not have a bucket policy.",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
ErrNoSuchKey: {
|
||||
Code: "NoSuchKey",
|
||||
Description: "The specified key does not exist.",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
NoSuchUpload: {
|
||||
ErrNoSuchUpload: {
|
||||
Code: "NoSuchUpload",
|
||||
Description: "The specified multipart upload does not exist.",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
NotImplemented: {
|
||||
ErrNotImplemented: {
|
||||
Code: "NotImplemented",
|
||||
Description: "A header you provided implies functionality that is not implemented.",
|
||||
HTTPStatusCode: http.StatusNotImplemented,
|
||||
},
|
||||
RequestTimeTooSkewed: {
|
||||
ErrRequestTimeTooSkewed: {
|
||||
Code: "RequestTimeTooSkewed",
|
||||
Description: "The difference between the request time and the server's time is too large.",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
SignatureDoesNotMatch: {
|
||||
ErrSignatureDoesNotMatch: {
|
||||
Code: "SignatureDoesNotMatch",
|
||||
Description: "The request signature we calculated does not match the signature you provided.",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
MethodNotAllowed: {
|
||||
ErrMethodNotAllowed: {
|
||||
Code: "MethodNotAllowed",
|
||||
Description: "The specified method is not allowed against this resource.",
|
||||
HTTPStatusCode: http.StatusMethodNotAllowed,
|
||||
},
|
||||
InvalidPart: {
|
||||
ErrInvalidPart: {
|
||||
Code: "InvalidPart",
|
||||
Description: "One or more of the specified parts could not be found.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
InvalidPartOrder: {
|
||||
ErrInvalidPartOrder: {
|
||||
Code: "InvalidPartOrder",
|
||||
Description: "The list of parts was not in ascending order. The parts list must be specified in order by part number.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
AuthorizationHeaderMalformed: {
|
||||
ErrAuthorizationHeaderMalformed: {
|
||||
Code: "AuthorizationHeaderMalformed",
|
||||
Description: "The authorization header is malformed; the region is wrong; expecting 'us-east-1'.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
MalformedPOSTRequest: {
|
||||
ErrMalformedPOSTRequest: {
|
||||
Code: "MalformedPOSTRequest",
|
||||
Description: "The body of your POST request is not well-formed multipart/form-data.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
SignatureVersionNotSupported: {
|
||||
ErrSignatureVersionNotSupported: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
BucketNotEmpty: {
|
||||
ErrBucketNotEmpty: {
|
||||
Code: "BucketNotEmpty",
|
||||
Description: "The bucket you tried to delete is not empty.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
RootPathFull: {
|
||||
Code: "RootPathFull",
|
||||
Description: "Root path has reached its minimum free disk threshold. Please delete few objects to proceed.",
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
ErrAllAccessDisabled: {
|
||||
Code: "AllAccessDisabled",
|
||||
Description: "All access to this bucket has been disabled.",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
ObjectExistsAsPrefix: {
|
||||
Code: "ObjectExistsAsPrefix",
|
||||
Description: "An object already exists as your prefix, choose a different prefix to proceed.",
|
||||
ErrMalformedPolicy: {
|
||||
Code: "MalformedPolicy",
|
||||
Description: "Policy has invalid resource.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingFields: {
|
||||
Code: "MissingFields",
|
||||
Description: "Missing fields in request.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingCredTag: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "Missing Credential field for this request.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrCredMalformed: {
|
||||
Code: "CredentialMalformed",
|
||||
Description: "Credential field malformed does not follow accessKeyID/credScope.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMalformedDate: {
|
||||
Code: "MalformedDate",
|
||||
Description: "Invalid date format header, expected to be in ISO8601, RFC1123 or RFC1123Z time format.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidRegion: {
|
||||
Code: "InvalidRegion",
|
||||
Description: "Region does not match.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidService: {
|
||||
Code: "AccessDenied",
|
||||
Description: "Service scope should be of value 's3'.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidRequestVersion: {
|
||||
Code: "AccessDenied",
|
||||
Description: "Request scope should be of value 'aws4_request'.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingSignTag: {
|
||||
Code: "AccessDenied",
|
||||
Description: "Signature header missing Signature field.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingSignHeadersTag: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Signature header missing SignedHeaders field.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrPolicyAlreadyExpired: {
|
||||
Code: "AccessDenied",
|
||||
Description: "Invalid according to Policy: Policy expired.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMalformedExpires: {
|
||||
Code: "MalformedExpires",
|
||||
Description: "Malformed expires header, expected non-zero number.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAuthHeaderEmpty: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Authorization header is invalid -- one and only one ' ' (space) required.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingDateHeader: {
|
||||
Code: "AccessDenied",
|
||||
Description: "AWS authentication requires a valid Date or x-amz-date header",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidQuerySignatureAlgo: {
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
Description: "X-Amz-Algorithm only supports \"AWS4-HMAC-SHA256\".",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrExpiredPresignRequest: {
|
||||
Code: "AccessDenied",
|
||||
Description: "Request has expired.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidQueryParams: {
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
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,
|
||||
},
|
||||
/// 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,
|
||||
},
|
||||
// Add your error structure here.
|
||||
}
|
||||
|
||||
// errorCodeError provides errorCode to Error. It returns empty if the code provided is unknown
|
||||
func getErrorCode(code int) APIError {
|
||||
// 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.
|
||||
if err == errSignatureMismatch {
|
||||
return ErrSignatureDoesNotMatch
|
||||
}
|
||||
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 = ErrNoSuchKey
|
||||
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]
|
||||
}
|
||||
|
||||
// getErrorResponse gets in standard error and resource value and
|
||||
// provides a encodable populated response values
|
||||
func getErrorResponse(err APIError, resource string) APIErrorResponse {
|
||||
func getAPIErrorResponse(err APIError, resource string) APIErrorResponse {
|
||||
var data = APIErrorResponse{}
|
||||
data.Code = err.Code
|
||||
data.Message = err.Description
|
||||
|
||||
+17
-34
@@ -23,8 +23,6 @@ import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/minio/minio/pkg/fs"
|
||||
)
|
||||
|
||||
//// helpers
|
||||
@@ -50,10 +48,9 @@ func setCommonHeaders(w http.ResponseWriter) {
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
}
|
||||
|
||||
// Write error response headers
|
||||
func encodeErrorResponse(response interface{}) []byte {
|
||||
// Encodes the response headers into XML format.
|
||||
func encodeResponse(response interface{}) []byte {
|
||||
var bytesBuffer bytes.Buffer
|
||||
// write common headers
|
||||
bytesBuffer.WriteString(xml.Header)
|
||||
e := xml.NewEncoder(&bytesBuffer)
|
||||
e.Encode(response)
|
||||
@@ -61,42 +58,28 @@ func encodeErrorResponse(response interface{}) []byte {
|
||||
}
|
||||
|
||||
// Write object header
|
||||
func setObjectHeaders(w http.ResponseWriter, metadata fs.ObjectMetadata, contentRange *httpRange) {
|
||||
func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *httpRange) {
|
||||
// set common headers
|
||||
if contentRange != nil {
|
||||
if contentRange.length > 0 {
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(contentRange.length, 10))
|
||||
setCommonHeaders(w)
|
||||
} else {
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(metadata.Size, 10))
|
||||
setCommonHeaders(w)
|
||||
}
|
||||
} else {
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(metadata.Size, 10))
|
||||
setCommonHeaders(w)
|
||||
}
|
||||
// set object headers
|
||||
lastModified := metadata.Created.Format(http.TimeFormat)
|
||||
// object related headers
|
||||
w.Header().Set("Content-Type", metadata.ContentType)
|
||||
if metadata.MD5 != "" {
|
||||
w.Header().Set("ETag", "\""+metadata.MD5+"\"")
|
||||
}
|
||||
setCommonHeaders(w)
|
||||
|
||||
// set object-related metadata headers
|
||||
lastModified := objInfo.ModTime.UTC().Format(http.TimeFormat)
|
||||
w.Header().Set("Last-Modified", lastModified)
|
||||
|
||||
// set content range
|
||||
w.Header().Set("Content-Type", objInfo.ContentType)
|
||||
if objInfo.MD5Sum != "" {
|
||||
w.Header().Set("ETag", "\""+objInfo.MD5Sum+"\"")
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(objInfo.Size, 10))
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func encodeSuccessResponse(response interface{}) []byte {
|
||||
var bytesBuffer bytes.Buffer
|
||||
bytesBuffer.WriteString(xml.Header)
|
||||
e := xml.NewEncoder(&bytesBuffer)
|
||||
e.Encode(response)
|
||||
return bytesBuffer.Bytes()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Go Library for Amazon S3 Compatible 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.
|
||||
@@ -14,24 +14,27 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package minio
|
||||
package main
|
||||
|
||||
// SignatureType is type of Authorization requested for a given HTTP request.
|
||||
type SignatureType int
|
||||
|
||||
// Different types of supported signatures - default is Latest i.e SignatureV4.
|
||||
const (
|
||||
Latest SignatureType = iota
|
||||
SignatureV4
|
||||
SignatureV2
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// isV2 - is signature SignatureV2?
|
||||
func (s SignatureType) isV2() bool {
|
||||
return s == SignatureV2
|
||||
}
|
||||
func TestGenerateRequestID(t *testing.T) {
|
||||
// Ensure that it returns an alphanumeric result of length 16.
|
||||
var id = generateRequestID()
|
||||
|
||||
// isV4 - is signature SignatureV4?
|
||||
func (s SignatureType) isV4() bool {
|
||||
return s == SignatureV4 || s == Latest
|
||||
if len(id) != 16 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
var e rune
|
||||
for _, char := range id {
|
||||
e = rune(char)
|
||||
|
||||
// Ensure that it is alphanumeric, in this case, between 0-9 and A-Z.
|
||||
if !(('0' <= e && e <= '9') || ('A' <= e && e <= 'Z')) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
-19
@@ -19,36 +19,78 @@ package main
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/minio/minio/pkg/fs"
|
||||
)
|
||||
|
||||
// parse bucket url queries
|
||||
func getBucketResources(values url.Values) (prefix, marker, delimiter string, maxkeys int, encodingType string) {
|
||||
// Parse bucket url queries
|
||||
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")
|
||||
maxkeys, _ = strconv.Atoi(values.Get("max-keys"))
|
||||
if values.Get("max-keys") != "" {
|
||||
maxkeys, _ = strconv.Atoi(values.Get("max-keys"))
|
||||
} else {
|
||||
maxkeys = maxObjectList
|
||||
}
|
||||
encodingType = values.Get("encoding-type")
|
||||
return
|
||||
}
|
||||
|
||||
// part bucket url queries for ?uploads
|
||||
func getBucketMultipartResources(values url.Values) (v fs.BucketMultipartResourcesMetadata) {
|
||||
v.Prefix = values.Get("prefix")
|
||||
v.KeyMarker = values.Get("key-marker")
|
||||
v.MaxUploads, _ = strconv.Atoi(values.Get("max-uploads"))
|
||||
v.Delimiter = values.Get("delimiter")
|
||||
v.EncodingType = values.Get("encoding-type")
|
||||
v.UploadIDMarker = values.Get("upload-id-marker")
|
||||
// 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 object url queries
|
||||
func getObjectResources(values url.Values) (v fs.ObjectResourcesMetadata) {
|
||||
v.UploadID = values.Get("uploadId")
|
||||
v.PartNumberMarker, _ = strconv.Atoi(values.Get("part-number-marker"))
|
||||
v.MaxParts, _ = strconv.Atoi(values.Get("max-parts"))
|
||||
v.EncodingType = values.Get("encoding-type")
|
||||
// Parse bucket url queries
|
||||
func getBucketResources(values url.Values) (listType int, prefix, marker, delimiter string, maxkeys int, encodingType string) {
|
||||
if values.Get("list-type") != "" {
|
||||
listType, _ = strconv.Atoi(values.Get("list-type"))
|
||||
}
|
||||
prefix = values.Get("prefix")
|
||||
marker = values.Get("marker")
|
||||
delimiter = values.Get("delimiter")
|
||||
if values.Get("max-keys") != "" {
|
||||
maxkeys, _ = strconv.Atoi(values.Get("max-keys"))
|
||||
} else {
|
||||
maxkeys = maxObjectList
|
||||
}
|
||||
encodingType = values.Get("encoding-type")
|
||||
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")
|
||||
keyMarker = values.Get("key-marker")
|
||||
uploadIDMarker = values.Get("upload-id-marker")
|
||||
delimiter = values.Get("delimiter")
|
||||
if values.Get("max-uploads") != "" {
|
||||
maxUploads, _ = strconv.Atoi(values.Get("max-uploads"))
|
||||
} else {
|
||||
maxUploads = maxUploadsList
|
||||
}
|
||||
encodingType = values.Get("encoding-type")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse object url queries
|
||||
func getObjectResources(values url.Values) (uploadID string, partNumberMarker, maxParts int, encodingType string) {
|
||||
uploadID = values.Get("uploadId")
|
||||
partNumberMarker, _ = strconv.Atoi(values.Get("part-number-marker"))
|
||||
if values.Get("max-parts") != "" {
|
||||
maxParts, _ = strconv.Atoi(values.Get("max-parts"))
|
||||
} else {
|
||||
maxParts = maxPartsList
|
||||
}
|
||||
encodingType = values.Get("encoding-type")
|
||||
return
|
||||
}
|
||||
|
||||
+357
-90
@@ -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.
|
||||
@@ -17,24 +17,260 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/minio/pkg/fs"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Reply date format
|
||||
const (
|
||||
rfcFormat = "2006-01-02T15:04:05.000Z"
|
||||
timeFormatAMZ = "2006-01-02T15:04:05.000Z" // Reply date format
|
||||
maxObjectList = 1000 // Limit number of objects in a listObjectsResponse.
|
||||
maxUploadsList = 1000 // Limit number of uploads in a listUploadsResponse.
|
||||
maxPartsList = 1000 // Limit number of parts in a listPartsResponse.
|
||||
)
|
||||
|
||||
// LocationResponse - format for location response.
|
||||
type LocationResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ LocationConstraint" json:"-"`
|
||||
Location string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// ListObjectsResponse - format for list objects response.
|
||||
type ListObjectsResponse 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
|
||||
Marker 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.
|
||||
NextMarker string
|
||||
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
|
||||
ETag string
|
||||
LastModified string
|
||||
Size int64
|
||||
}
|
||||
|
||||
// ListPartsResponse - format for list parts response.
|
||||
type ListPartsResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListPartsResult" json:"-"`
|
||||
|
||||
Bucket string
|
||||
Key string
|
||||
UploadID string `xml:"UploadId"`
|
||||
|
||||
Initiator Initiator
|
||||
Owner Owner
|
||||
|
||||
// The class of storage used to store the object.
|
||||
StorageClass string
|
||||
|
||||
PartNumberMarker int
|
||||
NextPartNumberMarker int
|
||||
MaxParts int
|
||||
IsTruncated bool
|
||||
|
||||
// List of parts.
|
||||
Parts []Part `xml:"Part"`
|
||||
}
|
||||
|
||||
// ListMultipartUploadsResponse - format for list multipart uploads response.
|
||||
type ListMultipartUploadsResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListMultipartUploadsResult" json:"-"`
|
||||
|
||||
Bucket string
|
||||
KeyMarker string
|
||||
UploadIDMarker string `xml:"UploadIdMarker"`
|
||||
NextKeyMarker string
|
||||
NextUploadIDMarker string `xml:"NextUploadIdMarker"`
|
||||
EncodingType string
|
||||
MaxUploads int
|
||||
IsTruncated bool
|
||||
Uploads []Upload `xml:"Upload"`
|
||||
Prefix string
|
||||
Delimiter string
|
||||
CommonPrefixes []CommonPrefix
|
||||
}
|
||||
|
||||
// ListBucketsResponse - format for list buckets response
|
||||
type ListBucketsResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListAllMyBucketsResult" json:"-"`
|
||||
// Container for one or more buckets.
|
||||
Buckets struct {
|
||||
Buckets []Bucket `xml:"Bucket"`
|
||||
} // Buckets are nested
|
||||
Owner Owner
|
||||
}
|
||||
|
||||
// Upload container for in progress multipart upload
|
||||
type Upload struct {
|
||||
Key string
|
||||
UploadID string `xml:"UploadId"`
|
||||
Initiator Initiator
|
||||
Owner Owner
|
||||
StorageClass string
|
||||
Initiated string
|
||||
}
|
||||
|
||||
// CommonPrefix container for prefix response in ListObjectsResponse
|
||||
type CommonPrefix struct {
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// Bucket container for bucket metadata
|
||||
type Bucket struct {
|
||||
Name string
|
||||
CreationDate string // time string of format "2006-01-02T15:04:05.000Z"
|
||||
}
|
||||
|
||||
// Object container for object metadata
|
||||
type Object struct {
|
||||
ETag string
|
||||
Key string
|
||||
LastModified string // time string of format "2006-01-02T15:04:05.000Z"
|
||||
Size int64
|
||||
|
||||
Owner Owner
|
||||
|
||||
// The class of storage used to store the object.
|
||||
StorageClass string
|
||||
}
|
||||
|
||||
// CopyObjectResponse container returns ETag and LastModified of the
|
||||
// successfully copied object
|
||||
type CopyObjectResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CopyObjectResult" json:"-"`
|
||||
ETag string
|
||||
LastModified string // time string of format "2006-01-02T15:04:05.000Z"
|
||||
}
|
||||
|
||||
// Initiator inherit from Owner struct, fields are same
|
||||
type Initiator Owner
|
||||
|
||||
// Owner - bucket owner/principal
|
||||
type Owner struct {
|
||||
ID string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
// InitiateMultipartUploadResponse container for InitiateMultiPartUpload response, provides uploadID to start MultiPart upload
|
||||
type InitiateMultipartUploadResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ InitiateMultipartUploadResult" json:"-"`
|
||||
|
||||
Bucket string
|
||||
Key string
|
||||
UploadID string `xml:"UploadId"`
|
||||
}
|
||||
|
||||
// CompleteMultipartUploadResponse container for completed multipart upload response
|
||||
type CompleteMultipartUploadResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CompleteMultipartUploadResult" json:"-"`
|
||||
|
||||
Location string
|
||||
Bucket string
|
||||
Key string
|
||||
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
|
||||
Message string
|
||||
Key string
|
||||
}
|
||||
|
||||
// DeleteObjectsResponse container for multiple object deletes.
|
||||
type DeleteObjectsResponse struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ DeleteResult" json:"-"`
|
||||
|
||||
// Collection of all deleted objects
|
||||
DeletedObjects []ObjectIdentifier `xml:"Deleted,omitempty"`
|
||||
|
||||
// Collection of errors deleting certain objects.
|
||||
Errors []DeleteError `xml:"Error,omitempty"`
|
||||
}
|
||||
|
||||
// getLocation get URL location.
|
||||
func getLocation(r *http.Request) string {
|
||||
return r.URL.Path
|
||||
}
|
||||
|
||||
// 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
|
||||
// input:
|
||||
// array of bucket metadata
|
||||
//
|
||||
// output:
|
||||
// populated struct that can be serialized to match xml and json api spec output
|
||||
func generateListBucketsResponse(buckets []fs.BucketMetadata) ListBucketsResponse {
|
||||
var listbuckets []*Bucket
|
||||
func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
|
||||
var listbuckets []Bucket
|
||||
var data = ListBucketsResponse{}
|
||||
var owner = Owner{}
|
||||
|
||||
@@ -42,59 +278,22 @@ func generateListBucketsResponse(buckets []fs.BucketMetadata) ListBucketsRespons
|
||||
owner.DisplayName = "minio"
|
||||
|
||||
for _, bucket := range buckets {
|
||||
var listbucket = &Bucket{}
|
||||
var listbucket = Bucket{}
|
||||
listbucket.Name = bucket.Name
|
||||
listbucket.CreationDate = bucket.Created.Format(rfcFormat)
|
||||
listbucket.CreationDate = bucket.Created.Format(timeFormatAMZ)
|
||||
listbuckets = append(listbuckets, listbucket)
|
||||
}
|
||||
|
||||
data.Owner = owner
|
||||
data.Buckets.Bucket = listbuckets
|
||||
data.Buckets.Buckets = listbuckets
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// generates an AccessControlPolicy response for the said ACL.
|
||||
func generateAccessControlPolicyResponse(acl fs.BucketACL) AccessControlPolicyResponse {
|
||||
accessCtrlPolicyResponse := AccessControlPolicyResponse{}
|
||||
accessCtrlPolicyResponse.Owner = Owner{
|
||||
ID: "minio",
|
||||
DisplayName: "minio",
|
||||
}
|
||||
defaultGrant := Grant{}
|
||||
defaultGrant.Grantee.ID = "minio"
|
||||
defaultGrant.Grantee.DisplayName = "minio"
|
||||
defaultGrant.Permission = "FULL_CONTROL"
|
||||
accessCtrlPolicyResponse.AccessControlList.Grant = append(accessCtrlPolicyResponse.AccessControlList.Grant, defaultGrant)
|
||||
switch {
|
||||
case acl.IsPublicRead():
|
||||
publicReadGrant := Grant{}
|
||||
publicReadGrant.Grantee.ID = "minio"
|
||||
publicReadGrant.Grantee.DisplayName = "minio"
|
||||
publicReadGrant.Grantee.URI = "http://acs.amazonaws.com/groups/global/AllUsers"
|
||||
publicReadGrant.Permission = "READ"
|
||||
accessCtrlPolicyResponse.AccessControlList.Grant = append(accessCtrlPolicyResponse.AccessControlList.Grant, publicReadGrant)
|
||||
case acl.IsPublicReadWrite():
|
||||
publicReadGrant := Grant{}
|
||||
publicReadGrant.Grantee.ID = "minio"
|
||||
publicReadGrant.Grantee.DisplayName = "minio"
|
||||
publicReadGrant.Grantee.URI = "http://acs.amazonaws.com/groups/global/AllUsers"
|
||||
publicReadGrant.Permission = "READ"
|
||||
publicReadWriteGrant := Grant{}
|
||||
publicReadWriteGrant.Grantee.ID = "minio"
|
||||
publicReadWriteGrant.Grantee.DisplayName = "minio"
|
||||
publicReadWriteGrant.Grantee.URI = "http://acs.amazonaws.com/groups/global/AllUsers"
|
||||
publicReadWriteGrant.Permission = "WRITE"
|
||||
accessCtrlPolicyResponse.AccessControlList.Grant = append(accessCtrlPolicyResponse.AccessControlList.Grant, publicReadGrant)
|
||||
accessCtrlPolicyResponse.AccessControlList.Grant = append(accessCtrlPolicyResponse.AccessControlList.Grant, publicReadWriteGrant)
|
||||
}
|
||||
return accessCtrlPolicyResponse
|
||||
}
|
||||
|
||||
// generates an ListObjects response for the said bucket with other enumerated options.
|
||||
func generateListObjectsResponse(bucket, prefix, marker, delimiter string, maxKeys int, resp fs.ListObjectsResult) ListObjectsResponse {
|
||||
var contents []*Object
|
||||
var prefixes []*CommonPrefix
|
||||
func generateListObjectsResponse(bucket, prefix, marker, delimiter string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
|
||||
var contents []Object
|
||||
var prefixes []CommonPrefix
|
||||
var owner = Owner{}
|
||||
var data = ListObjectsResponse{}
|
||||
|
||||
@@ -102,14 +301,14 @@ func generateListObjectsResponse(bucket, prefix, marker, delimiter string, maxKe
|
||||
owner.DisplayName = "minio"
|
||||
|
||||
for _, object := range resp.Objects {
|
||||
var content = &Object{}
|
||||
if object.Object == "" {
|
||||
var content = Object{}
|
||||
if object.Name == "" {
|
||||
continue
|
||||
}
|
||||
content.Key = object.Object
|
||||
content.LastModified = object.Created.Format(rfcFormat)
|
||||
if object.MD5 != "" {
|
||||
content.ETag = "\"" + object.MD5 + "\""
|
||||
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"
|
||||
@@ -128,7 +327,7 @@ func generateListObjectsResponse(bucket, prefix, marker, delimiter string, maxKe
|
||||
data.NextMarker = resp.NextMarker
|
||||
data.IsTruncated = resp.IsTruncated
|
||||
for _, prefix := range resp.Prefixes {
|
||||
var prefixItem = &CommonPrefix{}
|
||||
var prefixItem = CommonPrefix{}
|
||||
prefixItem.Prefix = prefix
|
||||
prefixes = append(prefixes, prefixItem)
|
||||
}
|
||||
@@ -136,6 +335,59 @@ func generateListObjectsResponse(bucket, prefix, marker, delimiter string, maxKe
|
||||
return data
|
||||
}
|
||||
|
||||
// generates an ListObjects 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{
|
||||
ETag: "\"" + etag + "\"",
|
||||
LastModified: lastModified.UTC().Format(timeFormatAMZ),
|
||||
}
|
||||
}
|
||||
|
||||
// generateInitiateMultipartUploadResponse
|
||||
func generateInitiateMultipartUploadResponse(bucket, key, uploadID string) InitiateMultipartUploadResponse {
|
||||
return InitiateMultipartUploadResponse{
|
||||
@@ -156,60 +408,75 @@ func generateCompleteMultpartUploadResponse(bucket, key, location, etag string)
|
||||
}
|
||||
|
||||
// generateListPartsResult
|
||||
func generateListPartsResponse(objectMetadata fs.ObjectResourcesMetadata) ListPartsResponse {
|
||||
func generateListPartsResponse(partsInfo ListPartsInfo) ListPartsResponse {
|
||||
// TODO - support EncodingType in xml decoding
|
||||
listPartsResponse := ListPartsResponse{}
|
||||
listPartsResponse.Bucket = objectMetadata.Bucket
|
||||
listPartsResponse.Key = objectMetadata.Object
|
||||
listPartsResponse.UploadID = objectMetadata.UploadID
|
||||
listPartsResponse.Bucket = partsInfo.Bucket
|
||||
listPartsResponse.Key = partsInfo.Object
|
||||
listPartsResponse.UploadID = partsInfo.UploadID
|
||||
listPartsResponse.StorageClass = "STANDARD"
|
||||
listPartsResponse.Initiator.ID = "minio"
|
||||
listPartsResponse.Initiator.DisplayName = "minio"
|
||||
listPartsResponse.Owner.ID = "minio"
|
||||
listPartsResponse.Owner.DisplayName = "minio"
|
||||
|
||||
listPartsResponse.MaxParts = objectMetadata.MaxParts
|
||||
listPartsResponse.PartNumberMarker = objectMetadata.PartNumberMarker
|
||||
listPartsResponse.IsTruncated = objectMetadata.IsTruncated
|
||||
listPartsResponse.NextPartNumberMarker = objectMetadata.NextPartNumberMarker
|
||||
listPartsResponse.MaxParts = partsInfo.MaxParts
|
||||
listPartsResponse.PartNumberMarker = partsInfo.PartNumberMarker
|
||||
listPartsResponse.IsTruncated = partsInfo.IsTruncated
|
||||
listPartsResponse.NextPartNumberMarker = partsInfo.NextPartNumberMarker
|
||||
|
||||
listPartsResponse.Part = make([]*Part, len(objectMetadata.Part))
|
||||
for _, part := range objectMetadata.Part {
|
||||
newPart := &Part{}
|
||||
listPartsResponse.Parts = make([]Part, len(partsInfo.Parts))
|
||||
for index, part := range partsInfo.Parts {
|
||||
newPart := Part{}
|
||||
newPart.PartNumber = part.PartNumber
|
||||
newPart.ETag = "\"" + part.ETag + "\""
|
||||
newPart.Size = part.Size
|
||||
newPart.LastModified = part.LastModified.Format(rfcFormat)
|
||||
listPartsResponse.Part = append(listPartsResponse.Part, newPart)
|
||||
newPart.LastModified = part.LastModified.UTC().Format(timeFormatAMZ)
|
||||
listPartsResponse.Parts[index] = newPart
|
||||
}
|
||||
return listPartsResponse
|
||||
}
|
||||
|
||||
// generateListMultipartUploadsResponse
|
||||
func generateListMultipartUploadsResponse(bucket string, metadata fs.BucketMultipartResourcesMetadata) ListMultipartUploadsResponse {
|
||||
func generateListMultipartUploadsResponse(bucket string, multipartsInfo ListMultipartsInfo) ListMultipartUploadsResponse {
|
||||
listMultipartUploadsResponse := ListMultipartUploadsResponse{}
|
||||
listMultipartUploadsResponse.Bucket = bucket
|
||||
listMultipartUploadsResponse.Delimiter = metadata.Delimiter
|
||||
listMultipartUploadsResponse.IsTruncated = metadata.IsTruncated
|
||||
listMultipartUploadsResponse.EncodingType = metadata.EncodingType
|
||||
listMultipartUploadsResponse.Prefix = metadata.Prefix
|
||||
listMultipartUploadsResponse.KeyMarker = metadata.KeyMarker
|
||||
listMultipartUploadsResponse.NextKeyMarker = metadata.NextKeyMarker
|
||||
listMultipartUploadsResponse.MaxUploads = metadata.MaxUploads
|
||||
listMultipartUploadsResponse.NextUploadIDMarker = metadata.NextUploadIDMarker
|
||||
listMultipartUploadsResponse.UploadIDMarker = metadata.UploadIDMarker
|
||||
|
||||
listMultipartUploadsResponse.Upload = make([]*Upload, len(metadata.Upload))
|
||||
for _, upload := range metadata.Upload {
|
||||
newUpload := &Upload{}
|
||||
listMultipartUploadsResponse.Delimiter = multipartsInfo.Delimiter
|
||||
listMultipartUploadsResponse.IsTruncated = multipartsInfo.IsTruncated
|
||||
listMultipartUploadsResponse.EncodingType = multipartsInfo.EncodingType
|
||||
listMultipartUploadsResponse.Prefix = multipartsInfo.Prefix
|
||||
listMultipartUploadsResponse.KeyMarker = multipartsInfo.KeyMarker
|
||||
listMultipartUploadsResponse.NextKeyMarker = multipartsInfo.NextKeyMarker
|
||||
listMultipartUploadsResponse.MaxUploads = multipartsInfo.MaxUploads
|
||||
listMultipartUploadsResponse.NextUploadIDMarker = multipartsInfo.NextUploadIDMarker
|
||||
listMultipartUploadsResponse.UploadIDMarker = multipartsInfo.UploadIDMarker
|
||||
listMultipartUploadsResponse.CommonPrefixes = make([]CommonPrefix, len(multipartsInfo.CommonPrefixes))
|
||||
for index, commonPrefix := range multipartsInfo.CommonPrefixes {
|
||||
listMultipartUploadsResponse.CommonPrefixes[index] = CommonPrefix{
|
||||
Prefix: commonPrefix,
|
||||
}
|
||||
}
|
||||
listMultipartUploadsResponse.Uploads = make([]Upload, len(multipartsInfo.Uploads))
|
||||
for index, upload := range multipartsInfo.Uploads {
|
||||
newUpload := Upload{}
|
||||
newUpload.UploadID = upload.UploadID
|
||||
newUpload.Key = upload.Object
|
||||
newUpload.Initiated = upload.Initiated.Format(rfcFormat)
|
||||
listMultipartUploadsResponse.Upload = append(listMultipartUploadsResponse.Upload, newUpload)
|
||||
newUpload.Initiated = upload.Initiated.UTC().Format(timeFormatAMZ)
|
||||
listMultipartUploadsResponse.Uploads[index] = newUpload
|
||||
}
|
||||
return listMultipartUploadsResponse
|
||||
}
|
||||
|
||||
// generate multi objects delete response.
|
||||
func generateMultiDeleteResponse(quiet bool, deletedObjects []ObjectIdentifier, errs []DeleteError) DeleteObjectsResponse {
|
||||
deleteResp := DeleteObjectsResponse{}
|
||||
if !quiet {
|
||||
deleteResp.DeletedObjects = deletedObjects
|
||||
}
|
||||
deleteResp.Errors = errs
|
||||
return deleteResp
|
||||
}
|
||||
|
||||
// writeSuccessResponse write success headers and response if any.
|
||||
func writeSuccessResponse(w http.ResponseWriter, response []byte) {
|
||||
setCommonHeaders(w)
|
||||
@@ -228,11 +495,11 @@ func writeSuccessNoContent(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
// writeErrorRespone write error headers
|
||||
func writeErrorResponse(w http.ResponseWriter, req *http.Request, errorType int, resource string) {
|
||||
error := getErrorCode(errorType)
|
||||
func writeErrorResponse(w http.ResponseWriter, req *http.Request, errorCode APIErrorCode, resource string) {
|
||||
error := getAPIError(errorCode)
|
||||
// generate error response
|
||||
errorResponse := getErrorResponse(error, resource)
|
||||
encodedErrorResponse := encodeErrorResponse(errorResponse)
|
||||
errorResponse := getAPIErrorResponse(error, resource)
|
||||
encodedErrorResponse := encodeResponse(errorResponse)
|
||||
// set common headers
|
||||
setCommonHeaders(w)
|
||||
// write Header
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import router "github.com/gorilla/mux"
|
||||
|
||||
// objectAPIHandler implements and provides http handlers for S3 API.
|
||||
type objectAPIHandlers struct {
|
||||
ObjectAPI ObjectLayer
|
||||
}
|
||||
|
||||
// registerAPIRouter - registers S3 compatible APIs.
|
||||
func registerAPIRouter(mux *router.Router, api objectAPIHandlers) {
|
||||
// API Router
|
||||
apiRouter := mux.NewRoute().PathPrefix("/").Subrouter()
|
||||
|
||||
// Bucket router
|
||||
bucket := apiRouter.PathPrefix("/{bucket}").Subrouter()
|
||||
|
||||
/// Object operations
|
||||
|
||||
// HeadObject
|
||||
bucket.Methods("HEAD").Path("/{object:.+}").HandlerFunc(api.HeadObjectHandler)
|
||||
// PutObjectPart
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(api.PutObjectPartHandler).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||
// ListObjectPxarts
|
||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(api.ListObjectPartsHandler).Queries("uploadId", "{uploadId:.*}")
|
||||
// CompleteMultipartUpload
|
||||
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(api.CompleteMultipartUploadHandler).Queries("uploadId", "{uploadId:.*}")
|
||||
// NewMultipartUpload
|
||||
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(api.NewMultipartUploadHandler).Queries("uploads", "")
|
||||
// AbortMultipartUpload
|
||||
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(api.AbortMultipartUploadHandler).Queries("uploadId", "{uploadId:.*}")
|
||||
// GetObject
|
||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(api.GetObjectHandler)
|
||||
// CopyObject
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/).*?").HandlerFunc(api.CopyObjectHandler)
|
||||
// PutObject
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(api.PutObjectHandler)
|
||||
// DeleteObject
|
||||
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(api.DeleteObjectHandler)
|
||||
|
||||
/// Bucket operations
|
||||
|
||||
// GetBucketLocation
|
||||
bucket.Methods("GET").HandlerFunc(api.GetBucketLocationHandler).Queries("location", "")
|
||||
// GetBucketPolicy
|
||||
bucket.Methods("GET").HandlerFunc(api.GetBucketPolicyHandler).Queries("policy", "")
|
||||
// ListMultipartUploads
|
||||
bucket.Methods("GET").HandlerFunc(api.ListMultipartUploadsHandler).Queries("uploads", "")
|
||||
// ListObjects
|
||||
bucket.Methods("GET").HandlerFunc(api.ListObjectsHandler)
|
||||
// PutBucketPolicy
|
||||
bucket.Methods("PUT").HandlerFunc(api.PutBucketPolicyHandler).Queries("policy", "")
|
||||
// PutBucket
|
||||
bucket.Methods("PUT").HandlerFunc(api.PutBucketHandler)
|
||||
// HeadBucket
|
||||
bucket.Methods("HEAD").HandlerFunc(api.HeadBucketHandler)
|
||||
// PostPolicy
|
||||
bucket.Methods("POST").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(api.PostPolicyBucketHandler)
|
||||
// DeleteMultipleObjects
|
||||
bucket.Methods("POST").HandlerFunc(api.DeleteMultipleObjectsHandler)
|
||||
// DeleteBucketPolicy
|
||||
bucket.Methods("DELETE").HandlerFunc(api.DeleteBucketPolicyHandler).Queries("policy", "")
|
||||
// DeleteBucket
|
||||
bucket.Methods("DELETE").HandlerFunc(api.DeleteBucketHandler)
|
||||
|
||||
/// Root operation
|
||||
|
||||
// ListBuckets
|
||||
apiRouter.Methods("GET").HandlerFunc(api.ListBucketsHandler)
|
||||
}
|
||||
@@ -1,304 +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 (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-xl/pkg/probe"
|
||||
"github.com/minio/minio/pkg/fs"
|
||||
)
|
||||
|
||||
const (
|
||||
authHeaderPrefix = "AWS4-HMAC-SHA256"
|
||||
iso8601Format = "20060102T150405Z"
|
||||
)
|
||||
|
||||
// getCredentialsFromAuth parse credentials tag from authorization value
|
||||
func getCredentialsFromAuth(authValue string) ([]string, *probe.Error) {
|
||||
if authValue == "" {
|
||||
return nil, probe.NewError(errMissingAuthHeaderValue)
|
||||
}
|
||||
// replace all spaced strings
|
||||
authValue = strings.Replace(authValue, " ", "", -1)
|
||||
if !strings.HasPrefix(authValue, authHeaderPrefix) {
|
||||
return nil, probe.NewError(errMissingFieldsAuthHeader)
|
||||
}
|
||||
if !strings.HasPrefix(strings.TrimPrefix(authValue, authHeaderPrefix), "Credential") {
|
||||
return nil, probe.NewError(errInvalidAuthHeaderPrefix)
|
||||
}
|
||||
authValue = strings.TrimPrefix(authValue, authHeaderPrefix)
|
||||
authFields := strings.Split(strings.TrimSpace(authValue), ",")
|
||||
if len(authFields) != 3 {
|
||||
return nil, probe.NewError(errInvalidAuthHeaderValue)
|
||||
}
|
||||
credentials := strings.Split(strings.TrimSpace(authFields[0]), "=")
|
||||
if len(credentials) != 2 {
|
||||
return nil, probe.NewError(errMissingFieldsCredentialTag)
|
||||
}
|
||||
credentialElements := strings.Split(strings.TrimSpace(credentials[1]), "/")
|
||||
if len(credentialElements) != 5 {
|
||||
return nil, probe.NewError(errCredentialTagMalformed)
|
||||
}
|
||||
return credentialElements, nil
|
||||
}
|
||||
|
||||
func getSignatureFromAuth(authHeaderValue string) (string, *probe.Error) {
|
||||
authValue := strings.TrimPrefix(authHeaderValue, authHeaderPrefix)
|
||||
authFields := strings.Split(strings.TrimSpace(authValue), ",")
|
||||
if len(authFields) != 3 {
|
||||
return "", probe.NewError(errInvalidAuthHeaderValue)
|
||||
}
|
||||
if len(strings.Split(strings.TrimSpace(authFields[2]), "=")) != 2 {
|
||||
return "", probe.NewError(errMissingFieldsSignatureTag)
|
||||
}
|
||||
signature := strings.Split(strings.TrimSpace(authFields[2]), "=")[1]
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
func getSignedHeadersFromAuth(authHeaderValue string) ([]string, *probe.Error) {
|
||||
authValue := strings.TrimPrefix(authHeaderValue, authHeaderPrefix)
|
||||
authFields := strings.Split(strings.TrimSpace(authValue), ",")
|
||||
if len(authFields) != 3 {
|
||||
return nil, probe.NewError(errInvalidAuthHeaderValue)
|
||||
}
|
||||
if len(strings.Split(strings.TrimSpace(authFields[1]), "=")) != 2 {
|
||||
return nil, probe.NewError(errMissingFieldsSignedHeadersTag)
|
||||
}
|
||||
signedHeaders := strings.Split(strings.Split(strings.TrimSpace(authFields[1]), "=")[1], ";")
|
||||
return signedHeaders, nil
|
||||
}
|
||||
|
||||
// verify if region value is valid with configured minioRegion.
|
||||
func isValidRegion(region string, minioRegion string) *probe.Error {
|
||||
if minioRegion == "" {
|
||||
minioRegion = "us-east-1"
|
||||
}
|
||||
if region != minioRegion && region != "US" {
|
||||
return probe.NewError(errInvalidRegion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// stripRegion - strip only region from auth header.
|
||||
func stripRegion(authHeaderValue string) (string, *probe.Error) {
|
||||
credentialElements, err := getCredentialsFromAuth(authHeaderValue)
|
||||
if err != nil {
|
||||
return "", err.Trace(authHeaderValue)
|
||||
}
|
||||
region := credentialElements[2]
|
||||
return region, nil
|
||||
}
|
||||
|
||||
// stripAccessKeyID - strip only access key id from auth header.
|
||||
func stripAccessKeyID(authHeaderValue string) (string, *probe.Error) {
|
||||
credentialElements, err := getCredentialsFromAuth(authHeaderValue)
|
||||
if err != nil {
|
||||
return "", err.Trace()
|
||||
}
|
||||
accessKeyID := credentialElements[0]
|
||||
if !isValidAccessKey(accessKeyID) {
|
||||
return "", probe.NewError(errAccessKeyIDInvalid)
|
||||
}
|
||||
return accessKeyID, nil
|
||||
}
|
||||
|
||||
// initSignatureV4 initializing signature verification.
|
||||
func initSignatureV4(req *http.Request) (*fs.Signature, *probe.Error) {
|
||||
// strip auth from authorization header.
|
||||
authHeaderValue := req.Header.Get("Authorization")
|
||||
|
||||
config, err := loadConfigV2()
|
||||
if err != nil {
|
||||
return nil, err.Trace()
|
||||
}
|
||||
|
||||
region, err := stripRegion(authHeaderValue)
|
||||
if err != nil {
|
||||
return nil, err.Trace(authHeaderValue)
|
||||
}
|
||||
|
||||
if err = isValidRegion(region, config.Credentials.Region); err != nil {
|
||||
return nil, err.Trace(authHeaderValue)
|
||||
}
|
||||
|
||||
accessKeyID, err := stripAccessKeyID(authHeaderValue)
|
||||
if err != nil {
|
||||
return nil, err.Trace(authHeaderValue)
|
||||
}
|
||||
signature, err := getSignatureFromAuth(authHeaderValue)
|
||||
if err != nil {
|
||||
return nil, err.Trace(authHeaderValue)
|
||||
}
|
||||
signedHeaders, err := getSignedHeadersFromAuth(authHeaderValue)
|
||||
if err != nil {
|
||||
return nil, err.Trace(authHeaderValue)
|
||||
}
|
||||
if config.Credentials.AccessKeyID == accessKeyID {
|
||||
signature := &fs.Signature{
|
||||
AccessKeyID: config.Credentials.AccessKeyID,
|
||||
SecretAccessKey: config.Credentials.SecretAccessKey,
|
||||
Region: region,
|
||||
Signature: signature,
|
||||
SignedHeaders: signedHeaders,
|
||||
Request: req,
|
||||
}
|
||||
return signature, nil
|
||||
}
|
||||
return nil, probe.NewError(errAccessKeyIDInvalid)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func applyPolicy(formValues map[string]string) *probe.Error {
|
||||
if formValues["X-Amz-Algorithm"] != "AWS4-HMAC-SHA256" {
|
||||
return probe.NewError(errUnsupportedAlgorithm)
|
||||
}
|
||||
/// Decoding policy
|
||||
policyBytes, e := base64.StdEncoding.DecodeString(formValues["Policy"])
|
||||
if e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
postPolicyForm, err := fs.ParsePostPolicyForm(string(policyBytes))
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
}
|
||||
if !postPolicyForm.Expiration.After(time.Now().UTC()) {
|
||||
return probe.NewError(errPolicyAlreadyExpired)
|
||||
}
|
||||
if postPolicyForm.Conditions.Policies["$bucket"].Operator == "eq" {
|
||||
if formValues["Bucket"] != postPolicyForm.Conditions.Policies["$bucket"].Value {
|
||||
return probe.NewError(errPolicyMissingFields)
|
||||
}
|
||||
}
|
||||
if postPolicyForm.Conditions.Policies["$x-amz-date"].Operator == "eq" {
|
||||
if formValues["X-Amz-Date"] != postPolicyForm.Conditions.Policies["$x-amz-date"].Value {
|
||||
return probe.NewError(errPolicyMissingFields)
|
||||
}
|
||||
}
|
||||
if postPolicyForm.Conditions.Policies["$Content-Type"].Operator == "starts-with" {
|
||||
if !strings.HasPrefix(formValues["Content-Type"], postPolicyForm.Conditions.Policies["$Content-Type"].Value) {
|
||||
return probe.NewError(errPolicyMissingFields)
|
||||
}
|
||||
}
|
||||
if postPolicyForm.Conditions.Policies["$Content-Type"].Operator == "eq" {
|
||||
if formValues["Content-Type"] != postPolicyForm.Conditions.Policies["$Content-Type"].Value {
|
||||
return probe.NewError(errPolicyMissingFields)
|
||||
}
|
||||
}
|
||||
if postPolicyForm.Conditions.Policies["$key"].Operator == "starts-with" {
|
||||
if !strings.HasPrefix(formValues["Key"], postPolicyForm.Conditions.Policies["$key"].Value) {
|
||||
return probe.NewError(errPolicyMissingFields)
|
||||
}
|
||||
}
|
||||
if postPolicyForm.Conditions.Policies["$key"].Operator == "eq" {
|
||||
if formValues["Key"] != postPolicyForm.Conditions.Policies["$key"].Value {
|
||||
return probe.NewError(errPolicyMissingFields)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// initPostPresignedPolicyV4 initializing post policy signature verification
|
||||
func initPostPresignedPolicyV4(formValues map[string]string) (*fs.Signature, *probe.Error) {
|
||||
credentialElements := strings.Split(strings.TrimSpace(formValues["X-Amz-Credential"]), "/")
|
||||
if len(credentialElements) != 5 {
|
||||
return nil, probe.NewError(errCredentialTagMalformed)
|
||||
}
|
||||
accessKeyID := credentialElements[0]
|
||||
if !isValidAccessKey(accessKeyID) {
|
||||
return nil, probe.NewError(errAccessKeyIDInvalid)
|
||||
}
|
||||
config, err := loadConfigV2()
|
||||
if err != nil {
|
||||
return nil, err.Trace()
|
||||
}
|
||||
region := credentialElements[2]
|
||||
if config.Credentials.AccessKeyID == accessKeyID {
|
||||
signature := &fs.Signature{
|
||||
AccessKeyID: config.Credentials.AccessKeyID,
|
||||
SecretAccessKey: config.Credentials.SecretAccessKey,
|
||||
Region: region,
|
||||
Signature: formValues["X-Amz-Signature"],
|
||||
PresignedPolicy: formValues["Policy"],
|
||||
}
|
||||
return signature, nil
|
||||
}
|
||||
return nil, probe.NewError(errAccessKeyIDInvalid)
|
||||
}
|
||||
|
||||
// initPresignedSignatureV4 initializing presigned signature verification
|
||||
func initPresignedSignatureV4(req *http.Request) (*fs.Signature, *probe.Error) {
|
||||
credentialElements := strings.Split(strings.TrimSpace(req.URL.Query().Get("X-Amz-Credential")), "/")
|
||||
if len(credentialElements) != 5 {
|
||||
return nil, probe.NewError(errCredentialTagMalformed)
|
||||
}
|
||||
accessKeyID := credentialElements[0]
|
||||
if !isValidAccessKey(accessKeyID) {
|
||||
return nil, probe.NewError(errAccessKeyIDInvalid)
|
||||
}
|
||||
config, err := loadConfigV2()
|
||||
if err != nil {
|
||||
return nil, err.Trace()
|
||||
}
|
||||
region := credentialElements[2]
|
||||
signedHeaders := strings.Split(strings.TrimSpace(req.URL.Query().Get("X-Amz-SignedHeaders")), ";")
|
||||
signature := strings.TrimSpace(req.URL.Query().Get("X-Amz-Signature"))
|
||||
if config.Credentials.AccessKeyID == accessKeyID {
|
||||
signature := &fs.Signature{
|
||||
AccessKeyID: config.Credentials.AccessKeyID,
|
||||
SecretAccessKey: config.Credentials.SecretAccessKey,
|
||||
Region: region,
|
||||
Signature: signature,
|
||||
SignedHeaders: signedHeaders,
|
||||
Presigned: true,
|
||||
Request: req,
|
||||
}
|
||||
return signature, nil
|
||||
}
|
||||
return nil, probe.NewError(errAccessKeyIDInvalid)
|
||||
}
|
||||
+9
-10
@@ -1,30 +1,29 @@
|
||||
# version format
|
||||
version: "{build}"
|
||||
|
||||
# Operating system (build VM template)
|
||||
os: Windows Server 2012 R2
|
||||
os: Visual Studio 2015
|
||||
|
||||
platform: x64
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\minio\minio
|
||||
|
||||
# environment variables
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
GO_EXTLINK_ENABLED: 0
|
||||
GO15VENDOREXPERIMENT: 1
|
||||
UI_ASSETS: ui-assets.go
|
||||
UI_ASSETS_ARMOR: ui-assets.asc
|
||||
|
||||
# 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:
|
||||
- curl -o ui-assets.go -L https://dl.minio.io/assets/server/%UI_ASSETS%
|
||||
- curl -o ui-assets.asc -L https://dl.minio.io/assets/server/%UI_ASSETS_ARMOR%
|
||||
- gpg --batch --no-tty --yes --keyserver pgp.mit.edu --recv-keys F9AAC728
|
||||
- gpg --batch --no-tty --verify %UI_ASSETS_ARMOR% %UI_ASSETS%
|
||||
- go test .
|
||||
- go test -race .
|
||||
- go test github.com/minio/minio/pkg...
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* 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 (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Verify if request has AWS Presignature Version '4'.
|
||||
func isRequestPresignedSignatureV4(r *http.Request) bool {
|
||||
if _, ok := r.URL.Query()["X-Amz-Credential"]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Authorization type.
|
||||
type authType int
|
||||
|
||||
// List of all supported auth types.
|
||||
const (
|
||||
authTypeUnknown authType = iota
|
||||
authTypeAnonymous
|
||||
authTypePresigned
|
||||
authTypePostPolicy
|
||||
authTypeSigned
|
||||
authTypeJWT
|
||||
)
|
||||
|
||||
// Get request authentication type.
|
||||
func getRequestAuthType(r *http.Request) authType {
|
||||
if isRequestSignatureV4(r) {
|
||||
return authTypeSigned
|
||||
} else if isRequestPresignedSignatureV4(r) {
|
||||
return authTypePresigned
|
||||
} else if isRequestJWT(r) {
|
||||
return authTypeJWT
|
||||
} else if isRequestPostPolicySignatureV4(r) {
|
||||
return authTypePostPolicy
|
||||
} else if _, ok := r.Header["Authorization"]; !ok {
|
||||
return authTypeAnonymous
|
||||
}
|
||||
return authTypeUnknown
|
||||
}
|
||||
|
||||
// sum256 calculate sha256 sum for an input byte array
|
||||
func sum256(data []byte) []byte {
|
||||
hash := sha256.New()
|
||||
hash.Write(data)
|
||||
return hash.Sum(nil)
|
||||
}
|
||||
|
||||
// sumMD5 calculate md5 sum for an input byte array
|
||||
func sumMD5(data []byte) []byte {
|
||||
hash := md5.New()
|
||||
hash.Write(data)
|
||||
return hash.Sum(nil)
|
||||
}
|
||||
|
||||
// Verify if request has valid AWS Signature Version '4'.
|
||||
func isReqAuthenticated(r *http.Request) (s3Error APIErrorCode) {
|
||||
if r == nil {
|
||||
return ErrInternalError
|
||||
}
|
||||
payload, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return ErrInternalError
|
||||
}
|
||||
// Verify Content-Md5, if payload is set.
|
||||
if r.Header.Get("Content-Md5") != "" {
|
||||
if r.Header.Get("Content-Md5") != base64.StdEncoding.EncodeToString(sumMD5(payload)) {
|
||||
return ErrBadDigest
|
||||
}
|
||||
}
|
||||
// Populate back the payload.
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
||||
validateRegion := true // Validate region.
|
||||
if isRequestSignatureV4(r) {
|
||||
return doesSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
|
||||
} else if isRequestPresignedSignatureV4(r) {
|
||||
return doesPresignedSignatureMatch(hex.EncodeToString(sum256(payload)), r, validateRegion)
|
||||
}
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
// authHandler - handles all the incoming authorization headers and
|
||||
// validates them if possible.
|
||||
type authHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
// setAuthHandler to validate authorization header for the incoming request.
|
||||
func setAuthHandler(h http.Handler) http.Handler {
|
||||
return authHandler{h}
|
||||
}
|
||||
|
||||
// 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.
|
||||
a.handler.ServeHTTP(w, r)
|
||||
return
|
||||
case 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
|
||||
}
|
||||
}
|
||||
@@ -1,71 +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 "net/http"
|
||||
|
||||
// Please read for more information - http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
|
||||
//
|
||||
// Here We are only supporting 'acl's through request headers not through their request body
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#setting-acls
|
||||
|
||||
// Minio only supports three types for now i.e 'private, public-read, public-read-write'
|
||||
|
||||
// ACLType - different acl types
|
||||
type ACLType int
|
||||
|
||||
const (
|
||||
unsupportedACLType ACLType = iota
|
||||
privateACLType
|
||||
publicReadACLType
|
||||
publicReadWriteACLType
|
||||
)
|
||||
|
||||
// Get acl type requested from 'x-amz-acl' header
|
||||
func getACLType(req *http.Request) ACLType {
|
||||
aclHeader := req.Header.Get("x-amz-acl")
|
||||
if aclHeader != "" {
|
||||
switch {
|
||||
case aclHeader == "private":
|
||||
return privateACLType
|
||||
case aclHeader == "public-read":
|
||||
return publicReadACLType
|
||||
case aclHeader == "public-read-write":
|
||||
return publicReadWriteACLType
|
||||
default:
|
||||
return unsupportedACLType
|
||||
}
|
||||
}
|
||||
// make it default private
|
||||
return privateACLType
|
||||
}
|
||||
|
||||
// ACL type to human readable string
|
||||
func getACLTypeString(acl ACLType) string {
|
||||
switch acl {
|
||||
case privateACLType:
|
||||
return "private"
|
||||
case publicReadACLType:
|
||||
return "public-read"
|
||||
case publicReadWriteACLType:
|
||||
return "public-read-write"
|
||||
case unsupportedACLType:
|
||||
return ""
|
||||
default:
|
||||
return "private"
|
||||
}
|
||||
}
|
||||
+442
-329
@@ -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.
|
||||
@@ -17,47 +17,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio-xl/pkg/crypto/sha256"
|
||||
"github.com/minio/minio-xl/pkg/probe"
|
||||
"github.com/minio/minio/pkg/fs"
|
||||
mux "github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// 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, "Unable read bucket policy.")
|
||||
switch err.(type) {
|
||||
case BucketNotFound:
|
||||
return ErrNoSuchBucket
|
||||
case BucketNameInvalid:
|
||||
return ErrInvalidBucketName
|
||||
default:
|
||||
// For any other error just return AccessDenied.
|
||||
return ErrAccessDenied
|
||||
}
|
||||
}
|
||||
// Parse the saved policy.
|
||||
bucketPolicy, err := parseBucketPolicy(policy)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to parse bucket policy.")
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
// Construct resource in 'arn:aws:s3:::examplebucket/object' format.
|
||||
resource := AWSResourcePrefix + strings.TrimPrefix(reqURL.Path, "/")
|
||||
|
||||
// Get conditions for policy verification.
|
||||
conditions := make(map[string]string)
|
||||
for queryParam := range reqURL.Query() {
|
||||
conditions[queryParam] = reqURL.Query().Get(queryParam)
|
||||
}
|
||||
|
||||
// Validate action, resource and conditions with current policy statements.
|
||||
if !bucketPolicyEvalStatements(action, resource, conditions, bucketPolicy.Statements) {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// GetBucketLocationHandler - GET Bucket location.
|
||||
// -------------------------
|
||||
// This operation returns bucket location.
|
||||
func (api CloudStorageAPI) GetBucketLocationHandler(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
if isRequestRequiresACLCheck(req) {
|
||||
writeErrorResponse(w, req, AccessDenied, req.URL.Path)
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := api.Filesystem.GetBucketMetadata(bucket)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "GetBucketMetadata failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case fs.BucketNotFound:
|
||||
writeErrorResponse(w, req, NoSuchBucket, req.URL.Path)
|
||||
case fs.BucketNameInvalid:
|
||||
writeErrorResponse(w, req, InvalidBucketName, req.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, req, InternalError, req.URL.Path)
|
||||
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 {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
case authTypeSigned, authTypePresigned:
|
||||
payload, err := ioutil.ReadAll(r.Body)
|
||||
if err != 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
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := api.ObjectAPI.GetBucketInfo(bucket); err != nil {
|
||||
errorIf(err, "Unable to fetch bucket info.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Location value for LocationResponse is deliberately not used, until
|
||||
// we bring in a mechanism of configurable regions. For the time being
|
||||
// default region is empty i.e 'us-east-1'.
|
||||
encodedSuccessResponse := encodeSuccessResponse(LocationResponse{}) // generate response
|
||||
setCommonHeaders(w) // write headers
|
||||
// Generate response.
|
||||
encodedSuccessResponse := encodeResponse(LocationResponse{})
|
||||
// Get current region.
|
||||
region := serverConfig.GetRegion()
|
||||
if region != "us-east-1" {
|
||||
encodedSuccessResponse = encodeResponse(LocationResponse{
|
||||
Location: region,
|
||||
})
|
||||
}
|
||||
setCommonHeaders(w) // Write headers.
|
||||
writeSuccessResponse(w, encodedSuccessResponse)
|
||||
}
|
||||
|
||||
@@ -69,38 +143,50 @@ func (api CloudStorageAPI) GetBucketLocationHandler(w http.ResponseWriter, req *
|
||||
// completed or aborted. This operation returns at most 1,000 multipart
|
||||
// uploads in the response.
|
||||
//
|
||||
func (api CloudStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
if isRequestRequiresACLCheck(req) {
|
||||
writeErrorResponse(w, req, AccessDenied, req.URL.Path)
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
resources := getBucketMultipartResources(req.URL.Query())
|
||||
if resources.MaxUploads < 0 {
|
||||
writeErrorResponse(w, req, InvalidMaxUploads, req.URL.Path)
|
||||
return
|
||||
}
|
||||
if resources.MaxUploads == 0 {
|
||||
resources.MaxUploads = maxObjectList
|
||||
}
|
||||
|
||||
resources, err := api.Filesystem.ListMultipartUploads(bucket, resources)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "ListMultipartUploads failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case fs.BucketNotFound:
|
||||
writeErrorResponse(w, req, NoSuchBucket, req.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, req, InternalError, req.URL.Path)
|
||||
case authTypeAnonymous:
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/mpuAndPermissions.html
|
||||
if s3Error := enforceBucketPolicy("s3:ListBucketMultipartUploads", bucket, r.URL); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
case authTypePresigned, authTypeSigned:
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, _ := getBucketMultipartResources(r.URL.Query())
|
||||
if maxUploads < 0 {
|
||||
writeErrorResponse(w, r, ErrInvalidMaxUploads, r.URL.Path)
|
||||
return
|
||||
}
|
||||
if keyMarker != "" {
|
||||
// Marker not common with prefix is not implemented.
|
||||
if !strings.HasPrefix(keyMarker, prefix) {
|
||||
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
listMultipartsInfo, err := api.ObjectAPI.ListMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to list multipart uploads.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
// generate response
|
||||
response := generateListMultipartUploadsResponse(bucket, resources)
|
||||
encodedSuccessResponse := encodeSuccessResponse(response)
|
||||
response := generateListMultipartUploadsResponse(bucket, listMultipartsInfo)
|
||||
encodedSuccessResponse := encodeResponse(response)
|
||||
// write headers.
|
||||
setCommonHeaders(w)
|
||||
// write success response.
|
||||
@@ -108,325 +194,345 @@ func (api CloudStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, re
|
||||
}
|
||||
|
||||
// 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 CloudStorageAPI) ListObjectsHandler(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
func (api objectAPIHandlers) ListObjectsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
if isRequestRequiresACLCheck(req) {
|
||||
if api.Filesystem.IsPrivateBucket(bucket) {
|
||||
writeErrorResponse(w, req, AccessDenied, req.URL.Path)
|
||||
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
|
||||
}
|
||||
}
|
||||
var prefix, marker, token, delimiter, startAfter string
|
||||
var maxkeys int
|
||||
var listV2 bool
|
||||
// TODO handle encoding type.
|
||||
if r.URL.Query().Get("list-type") == "2" {
|
||||
listV2 = true
|
||||
prefix, token, startAfter, delimiter, maxkeys, _ = getListObjectsV2Args(r.URL.Query())
|
||||
// For ListV2 "start-after" is considered only if "continuation-token" is empty.
|
||||
if token == "" {
|
||||
marker = startAfter
|
||||
} else {
|
||||
marker = token
|
||||
}
|
||||
} else {
|
||||
prefix, marker, delimiter, maxkeys, _ = getListObjectsV1Args(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 != "" {
|
||||
// Marker not common with prefix is not implemented.
|
||||
if !strings.HasPrefix(marker, prefix) {
|
||||
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO handle encoding type.
|
||||
prefix, marker, delimiter, maxkeys, _ := getBucketResources(req.URL.Query())
|
||||
if maxkeys < 0 {
|
||||
writeErrorResponse(w, req, InvalidMaxKeys, req.URL.Path)
|
||||
return
|
||||
}
|
||||
if maxkeys == 0 {
|
||||
maxkeys = maxObjectList
|
||||
}
|
||||
listObjectsInfo, err := api.ObjectAPI.ListObjects(bucket, prefix, marker, delimiter, maxkeys)
|
||||
|
||||
listResp, err := api.Filesystem.ListObjects(bucket, prefix, marker, delimiter, maxkeys)
|
||||
if err == nil {
|
||||
var encodedSuccessResponse []byte
|
||||
// generate response
|
||||
response := generateListObjectsResponse(bucket, prefix, marker, delimiter, maxkeys, listResp)
|
||||
encodedSuccessResponse := encodeSuccessResponse(response)
|
||||
if listV2 {
|
||||
response := generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter, maxkeys, listObjectsInfo)
|
||||
encodedSuccessResponse = encodeResponse(response)
|
||||
} else {
|
||||
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 fs.BucketNameInvalid:
|
||||
writeErrorResponse(w, req, InvalidBucketName, req.URL.Path)
|
||||
case fs.BucketNotFound:
|
||||
writeErrorResponse(w, req, NoSuchBucket, req.URL.Path)
|
||||
case fs.ObjectNotFound:
|
||||
writeErrorResponse(w, req, NoSuchKey, req.URL.Path)
|
||||
case fs.ObjectNameInvalid:
|
||||
writeErrorResponse(w, req, NoSuchKey, req.URL.Path)
|
||||
default:
|
||||
errorIf(err.Trace(), "ListObjects failed.", nil)
|
||||
writeErrorResponse(w, req, InternalError, req.URL.Path)
|
||||
}
|
||||
errorIf(err, "Unable to list objects.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), 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 CloudStorageAPI) ListBucketsHandler(w http.ResponseWriter, req *http.Request) {
|
||||
if isRequestRequiresACLCheck(req) {
|
||||
writeErrorResponse(w, req, AccessDenied, req.URL.Path)
|
||||
func (api objectAPIHandlers) 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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
buckets, err := api.Filesystem.ListBuckets()
|
||||
bucketsInfo, err := api.ObjectAPI.ListBuckets()
|
||||
if err == nil {
|
||||
// generate response
|
||||
response := generateListBucketsResponse(buckets)
|
||||
encodedSuccessResponse := encodeSuccessResponse(response)
|
||||
response := generateListBucketsResponse(bucketsInfo)
|
||||
encodedSuccessResponse := encodeResponse(response)
|
||||
// write headers
|
||||
setCommonHeaders(w)
|
||||
// write response
|
||||
writeSuccessResponse(w, encodedSuccessResponse)
|
||||
return
|
||||
}
|
||||
errorIf(err.Trace(), "ListBuckets failed.", nil)
|
||||
writeErrorResponse(w, req, InternalError, req.URL.Path)
|
||||
errorIf(err, "Unable to list buckets.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
}
|
||||
|
||||
// DeleteMultipleObjectsHandler - deletes multiple objects.
|
||||
func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
return
|
||||
case authTypeAnonymous:
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
||||
if s3Error := enforceBucketPolicy("s3:DeleteObject", bucket, r.URL); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
case authTypePresigned, authTypeSigned:
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Content-Length is required and should be non-zero
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||
if r.ContentLength <= 0 {
|
||||
writeErrorResponse(w, r, ErrMissingContentLength, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Content-Md5 is requied should be set
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||
if _, ok := r.Header["Content-Md5"]; !ok {
|
||||
writeErrorResponse(w, r, ErrMissingContentMD5, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Allocate incoming content length bytes.
|
||||
deleteXMLBytes := make([]byte, r.ContentLength)
|
||||
|
||||
// Read incoming body XML bytes.
|
||||
if _, err := io.ReadFull(r.Body, deleteXMLBytes); err != nil {
|
||||
errorIf(err, "Unable to read HTTP body.")
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Unmarshal list of keys to be deleted.
|
||||
deleteObjects := &DeleteObjectsRequest{}
|
||||
if err := xml.Unmarshal(deleteXMLBytes, deleteObjects); err != nil {
|
||||
errorIf(err, "Unable to unmarshal delete objects request XML.")
|
||||
writeErrorResponse(w, r, ErrMalformedXML, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
var deleteErrors []DeleteError
|
||||
var deletedObjects []ObjectIdentifier
|
||||
// Loop through all the objects and delete them sequentially.
|
||||
for _, object := range deleteObjects.Objects {
|
||||
err := api.ObjectAPI.DeleteObject(bucket, object.ObjectName)
|
||||
if err == nil {
|
||||
deletedObjects = append(deletedObjects, ObjectIdentifier{
|
||||
ObjectName: object.ObjectName,
|
||||
})
|
||||
} else {
|
||||
errorIf(err, "Unable to delete object.")
|
||||
deleteErrors = append(deleteErrors, DeleteError{
|
||||
Code: errorCodeResponse[toAPIErrorCode(err)].Code,
|
||||
Message: errorCodeResponse[toAPIErrorCode(err)].Description,
|
||||
Key: object.ObjectName,
|
||||
})
|
||||
}
|
||||
}
|
||||
// Generate response
|
||||
response := generateMultiDeleteResponse(deleteObjects.Quiet, deletedObjects, deleteErrors)
|
||||
encodedSuccessResponse := encodeResponse(response)
|
||||
// Write headers
|
||||
setCommonHeaders(w)
|
||||
// Write success response.
|
||||
writeSuccessResponse(w, encodedSuccessResponse)
|
||||
}
|
||||
|
||||
// PutBucketHandler - PUT Bucket
|
||||
// ----------
|
||||
// This implementation of the PUT operation creates a new bucket for authenticated request
|
||||
func (api CloudStorageAPI) PutBucketHandler(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
if isRequestRequiresACLCheck(req) {
|
||||
writeErrorResponse(w, req, AccessDenied, req.URL.Path)
|
||||
// Set http request for signature.
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// read from 'x-amz-acl'
|
||||
aclType := getACLType(req)
|
||||
if aclType == unsupportedACLType {
|
||||
writeErrorResponse(w, req, NotImplemented, req.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
var signature *fs.Signature
|
||||
// Init signature V4 verification
|
||||
if isRequestSignatureV4(req) {
|
||||
var err *probe.Error
|
||||
signature, err = initSignatureV4(req)
|
||||
if err != nil {
|
||||
switch err.ToGoError() {
|
||||
case errInvalidRegion:
|
||||
errorIf(err.Trace(), "Unknown region in authorization header.", nil)
|
||||
writeErrorResponse(w, req, AuthorizationHeaderMalformed, req.URL.Path)
|
||||
return
|
||||
case errAccessKeyIDInvalid:
|
||||
errorIf(err.Trace(), "Invalid access key id.", nil)
|
||||
writeErrorResponse(w, req, InvalidAccessKeyID, req.URL.Path)
|
||||
return
|
||||
default:
|
||||
errorIf(err.Trace(), "Initializing signature v4 failed.", nil)
|
||||
writeErrorResponse(w, req, InternalError, req.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if body of request is non-nil then check for validity of Content-Length
|
||||
if req.Body != nil {
|
||||
/// if Content-Length is unknown/missing, deny the request
|
||||
if req.ContentLength == -1 && !contains(req.TransferEncoding, "chunked") {
|
||||
writeErrorResponse(w, req, MissingContentLength, req.URL.Path)
|
||||
case authTypePresigned, authTypeSigned:
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
if signature != nil {
|
||||
locationBytes, e := ioutil.ReadAll(req.Body)
|
||||
if e != nil {
|
||||
errorIf(probe.NewError(e), "MakeBucket failed.", nil)
|
||||
writeErrorResponse(w, req, InternalError, req.URL.Path)
|
||||
return
|
||||
}
|
||||
sh := sha256.New()
|
||||
sh.Write(locationBytes)
|
||||
ok, err := signature.DoesSignatureMatch(hex.EncodeToString(sh.Sum(nil)))
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "MakeBucket failed.", nil)
|
||||
writeErrorResponse(w, req, InternalError, req.URL.Path)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
writeErrorResponse(w, req, SignatureDoesNotMatch, req.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := api.Filesystem.MakeBucket(bucket, getACLTypeString(aclType))
|
||||
// the location value in the request body should match the Region in serverConfig.
|
||||
// other values of location are not accepted.
|
||||
// make bucket fails in such cases.
|
||||
errCode := isValidLocationContraint(r.Body, serverConfig.GetRegion())
|
||||
if errCode != ErrNone {
|
||||
writeErrorResponse(w, r, errCode, r.URL.Path)
|
||||
return
|
||||
}
|
||||
// Make bucket.
|
||||
err := api.ObjectAPI.MakeBucket(bucket)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "MakeBucket failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case fs.BucketNameInvalid:
|
||||
writeErrorResponse(w, req, InvalidBucketName, req.URL.Path)
|
||||
case fs.BucketExists:
|
||||
writeErrorResponse(w, req, BucketAlreadyExists, req.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, req, InternalError, req.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
|
||||
w.Header().Set("Location", "/"+bucket)
|
||||
w.Header().Set("Location", getLocation(r))
|
||||
writeSuccessResponse(w, nil)
|
||||
}
|
||||
|
||||
func extractHTTPFormValues(reader *multipart.Reader) (io.Reader, map[string]string, error) {
|
||||
/// HTML Form values
|
||||
formValues := make(map[string]string)
|
||||
filePart := new(bytes.Buffer)
|
||||
var err error
|
||||
for err == nil {
|
||||
var part *multipart.Part
|
||||
part, err = reader.NextPart()
|
||||
if part != nil {
|
||||
if part.FileName() == "" {
|
||||
var buffer []byte
|
||||
buffer, err = ioutil.ReadAll(part)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
formValues[http.CanonicalHeaderKey(part.FormName())] = string(buffer)
|
||||
} else {
|
||||
if _, err = io.Copy(filePart, part); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 CloudStorageAPI) PostPolicyBucketHandler(w http.ResponseWriter, req *http.Request) {
|
||||
// if body of request is non-nil then check for validity of Content-Length
|
||||
if req.Body != nil {
|
||||
/// if Content-Length is unknown/missing, deny the request
|
||||
if req.ContentLength == -1 {
|
||||
writeErrorResponse(w, req, MissingContentLength, req.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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 := req.MultipartReader()
|
||||
if e != nil {
|
||||
errorIf(probe.NewError(e), "Unable to initialize multipart reader.", nil)
|
||||
writeErrorResponse(w, req, MalformedPOSTRequest, req.URL.Path)
|
||||
// be loaded in memory, the remaining being put in temporary files.
|
||||
reader, err := r.MultipartReader()
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to initialize multipart reader.")
|
||||
writeErrorResponse(w, r, ErrMalformedPOSTRequest, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
fileBody, formValues, err := extractHTTPFormValues(reader)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "Unable to parse form values.", nil)
|
||||
writeErrorResponse(w, req, MalformedPOSTRequest, req.URL.Path)
|
||||
errorIf(err, "Unable to parse form values.")
|
||||
writeErrorResponse(w, r, ErrMalformedPOSTRequest, r.URL.Path)
|
||||
return
|
||||
}
|
||||
bucket := mux.Vars(req)["bucket"]
|
||||
bucket := mux.Vars(r)["bucket"]
|
||||
formValues["Bucket"] = bucket
|
||||
object := formValues["Key"]
|
||||
signature, err := initPostPresignedPolicyV4(formValues)
|
||||
|
||||
// Verify policy signature.
|
||||
apiErr := doesPolicySignatureMatch(formValues)
|
||||
if apiErr != ErrNone {
|
||||
writeErrorResponse(w, r, apiErr, r.URL.Path)
|
||||
return
|
||||
}
|
||||
if apiErr = checkPostPolicy(formValues); apiErr != ErrNone {
|
||||
writeErrorResponse(w, r, apiErr, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Save metadata.
|
||||
metadata := make(map[string]string)
|
||||
// Nothing to store right now.
|
||||
|
||||
md5Sum, err := api.ObjectAPI.PutObject(bucket, object, -1, fileBody, metadata)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "Unable to initialize post policy presigned.", nil)
|
||||
writeErrorResponse(w, req, MalformedPOSTRequest, req.URL.Path)
|
||||
errorIf(err, "Unable to create object.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
if ok, err = signature.DoesPolicySignatureMatch(formValues["X-Amz-Date"]); err != nil {
|
||||
errorIf(err.Trace(), "Unable to verify signature.", nil)
|
||||
writeErrorResponse(w, req, SignatureDoesNotMatch, req.URL.Path)
|
||||
return
|
||||
if md5Sum != "" {
|
||||
w.Header().Set("ETag", "\""+md5Sum+"\"")
|
||||
}
|
||||
if ok == false {
|
||||
writeErrorResponse(w, req, SignatureDoesNotMatch, req.URL.Path)
|
||||
return
|
||||
}
|
||||
if err = applyPolicy(formValues); err != nil {
|
||||
errorIf(err.Trace(), "Invalid request, policy doesn't match with the endpoint.", nil)
|
||||
writeErrorResponse(w, req, MalformedPOSTRequest, req.URL.Path)
|
||||
return
|
||||
}
|
||||
metadata, err := api.Filesystem.CreateObject(bucket, object, "", 0, fileBody, nil)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "CreateObject failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case fs.RootPathFull:
|
||||
writeErrorResponse(w, req, RootPathFull, req.URL.Path)
|
||||
case fs.BucketNotFound:
|
||||
writeErrorResponse(w, req, NoSuchBucket, req.URL.Path)
|
||||
case fs.BucketNameInvalid:
|
||||
writeErrorResponse(w, req, InvalidBucketName, req.URL.Path)
|
||||
case fs.BadDigest:
|
||||
writeErrorResponse(w, req, BadDigest, req.URL.Path)
|
||||
case fs.SignatureDoesNotMatch:
|
||||
writeErrorResponse(w, req, SignatureDoesNotMatch, req.URL.Path)
|
||||
case fs.IncompleteBody:
|
||||
writeErrorResponse(w, req, IncompleteBody, req.URL.Path)
|
||||
case fs.EntityTooLarge:
|
||||
writeErrorResponse(w, req, EntityTooLarge, req.URL.Path)
|
||||
case fs.InvalidDigest:
|
||||
writeErrorResponse(w, req, InvalidDigest, req.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, req, InternalError, req.URL.Path)
|
||||
}
|
||||
return
|
||||
}
|
||||
if metadata.MD5 != "" {
|
||||
w.Header().Set("ETag", "\""+metadata.MD5+"\"")
|
||||
}
|
||||
writeSuccessResponse(w, nil)
|
||||
}
|
||||
|
||||
// PutBucketACLHandler - PUT Bucket ACL
|
||||
// ----------
|
||||
// This implementation of the PUT operation modifies the bucketACL for authenticated request
|
||||
func (api CloudStorageAPI) PutBucketACLHandler(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
if isRequestRequiresACLCheck(req) {
|
||||
writeErrorResponse(w, req, AccessDenied, req.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// read from 'x-amz-acl'
|
||||
aclType := getACLType(req)
|
||||
if aclType == unsupportedACLType {
|
||||
writeErrorResponse(w, req, NotImplemented, req.URL.Path)
|
||||
return
|
||||
}
|
||||
err := api.Filesystem.SetBucketMetadata(bucket, map[string]string{"acl": getACLTypeString(aclType)})
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "PutBucketACL failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case fs.BucketNameInvalid:
|
||||
writeErrorResponse(w, req, InvalidBucketName, req.URL.Path)
|
||||
case fs.BucketNotFound:
|
||||
writeErrorResponse(w, req, NoSuchBucket, req.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, req, InternalError, req.URL.Path)
|
||||
}
|
||||
return
|
||||
}
|
||||
writeSuccessResponse(w, nil)
|
||||
}
|
||||
|
||||
// GetBucketACLHandler - GET ACL on a Bucket
|
||||
// ----------
|
||||
// This operation uses acl subresource to the return the ``acl``
|
||||
// of a bucket. One must have permission to access the bucket to
|
||||
// know its ``acl``. This operation willl return response of 404
|
||||
// if bucket not found and 403 for invalid credentials.
|
||||
func (api CloudStorageAPI) GetBucketACLHandler(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
if isRequestRequiresACLCheck(req) {
|
||||
writeErrorResponse(w, req, AccessDenied, req.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
bucketMetadata, err := api.Filesystem.GetBucketMetadata(bucket)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "GetBucketMetadata failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case fs.BucketNotFound:
|
||||
writeErrorResponse(w, req, NoSuchBucket, req.URL.Path)
|
||||
case fs.BucketNameInvalid:
|
||||
writeErrorResponse(w, req, InvalidBucketName, req.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, req, InternalError, req.URL.Path)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Generate response
|
||||
response := generateAccessControlPolicyResponse(bucketMetadata.ACL)
|
||||
encodedSuccessResponse := encodeSuccessResponse(response)
|
||||
// Write headers
|
||||
encodedSuccessResponse := encodeResponse(PostResponse{
|
||||
Location: getObjectLocation(bucket, object), // TODO Full URL is preferred
|
||||
Bucket: bucket,
|
||||
Key: object,
|
||||
ETag: md5Sum,
|
||||
})
|
||||
setCommonHeaders(w)
|
||||
// Write success response.
|
||||
writeSuccessResponse(w, encodedSuccessResponse)
|
||||
}
|
||||
|
||||
@@ -436,55 +542,62 @@ func (api CloudStorageAPI) GetBucketACLHandler(w http.ResponseWriter, req *http.
|
||||
// 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 CloudStorageAPI) HeadBucketHandler(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
if isRequestRequiresACLCheck(req) {
|
||||
if api.Filesystem.IsPrivateBucket(bucket) {
|
||||
writeErrorResponse(w, req, AccessDenied, req.URL.Path)
|
||||
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 authTypePresigned, authTypeSigned:
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err := api.Filesystem.GetBucketMetadata(bucket)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "GetBucketMetadata failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case fs.BucketNotFound:
|
||||
writeErrorResponse(w, req, NoSuchBucket, req.URL.Path)
|
||||
case fs.BucketNameInvalid:
|
||||
writeErrorResponse(w, req, InvalidBucketName, req.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, req, InternalError, req.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 CloudStorageAPI) DeleteBucketHandler(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
if isRequestRequiresACLCheck(req) {
|
||||
writeErrorResponse(w, req, AccessDenied, req.URL.Path)
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
return
|
||||
case authTypePresigned, authTypeSigned:
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := api.ObjectAPI.DeleteBucket(bucket); err != nil {
|
||||
errorIf(err, "Unable to delete a bucket.")
|
||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
err := api.Filesystem.DeleteBucket(bucket)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(), "DeleteBucket failed.", nil)
|
||||
switch err.ToGoError().(type) {
|
||||
case fs.BucketNotFound:
|
||||
writeErrorResponse(w, req, NoSuchBucket, req.URL.Path)
|
||||
case fs.BucketNotEmpty:
|
||||
writeErrorResponse(w, req, BucketNotEmpty, req.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, req, InternalError, req.URL.Path)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Delete bucket access policy, if present - ignore any errors.
|
||||
removeBucketPolicy(bucket)
|
||||
|
||||
// Write success response.
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* 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"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
mux "github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// maximum supported access policy size.
|
||||
const maxAccessPolicySize = 20 * 1024 * 1024 // 20KiB.
|
||||
|
||||
// Verify if a given action is valid for the url path based on the
|
||||
// existing bucket access policy.
|
||||
func bucketPolicyEvalStatements(action string, resource string, conditions map[string]string, statements []policyStatement) bool {
|
||||
for _, statement := range statements {
|
||||
if bucketPolicyMatchStatement(action, resource, conditions, statement) {
|
||||
if statement.Effect == "Allow" {
|
||||
return true
|
||||
}
|
||||
// Do not uncomment kept here for readability.
|
||||
// else statement.Effect == "Deny"
|
||||
return false
|
||||
}
|
||||
}
|
||||
// None match so deny.
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify if action, resource and conditions match input policy statement.
|
||||
func bucketPolicyMatchStatement(action string, resource string, conditions map[string]string, statement policyStatement) bool {
|
||||
// Verify if action matches.
|
||||
if bucketPolicyActionMatch(action, statement) {
|
||||
// Verify if resource matches.
|
||||
if bucketPolicyResourceMatch(resource, statement) {
|
||||
// Verify if condition matches.
|
||||
if bucketPolicyConditionMatch(conditions, statement) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify if given action matches with policy statement.
|
||||
func bucketPolicyActionMatch(action string, statement policyStatement) bool {
|
||||
for _, policyAction := range statement.Actions {
|
||||
// Policy action can be a regex, validate the action with matching string.
|
||||
matched, err := regexp.MatchString(policyAction, action)
|
||||
fatalIf(err, "Invalid action \"%s\" in bucket policy.", action)
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Match function matches wild cards in 'pattern' for resource.
|
||||
func resourceMatch(pattern, resource string) bool {
|
||||
if pattern == "" {
|
||||
return resource == pattern
|
||||
}
|
||||
if pattern == "*" {
|
||||
return true
|
||||
}
|
||||
parts := strings.Split(pattern, "*")
|
||||
if len(parts) == 1 {
|
||||
return resource == pattern
|
||||
}
|
||||
tGlob := strings.HasSuffix(pattern, "*")
|
||||
end := len(parts) - 1
|
||||
if !strings.HasPrefix(resource, parts[0]) {
|
||||
return false
|
||||
}
|
||||
for i := 1; i < end; i++ {
|
||||
if !strings.Contains(resource, parts[i]) {
|
||||
return false
|
||||
}
|
||||
idx := strings.Index(resource, parts[i]) + len(parts[i])
|
||||
resource = resource[idx:]
|
||||
}
|
||||
return tGlob || strings.HasSuffix(resource, parts[end])
|
||||
}
|
||||
|
||||
// Verify if given resource matches with policy statement.
|
||||
func bucketPolicyResourceMatch(resource string, statement policyStatement) bool {
|
||||
for _, resourcep := range statement.Resources {
|
||||
// the resource rule for object could contain "*" wild card.
|
||||
// the requested object can be given access based on the already set bucket policy if
|
||||
// the match is successful.
|
||||
// More info: http://docs.aws.amazon.com/AmazonS3/latest/dev/s3-arn-format.html .
|
||||
if matched := resourceMatch(resourcep, resource); !matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Verify if given condition matches with policy statement.
|
||||
func bucketPolicyConditionMatch(conditions map[string]string, statement policyStatement) bool {
|
||||
// Supports following conditions.
|
||||
// - StringEquals
|
||||
// - StringNotEquals
|
||||
//
|
||||
// Supported applicable condition keys for each conditions.
|
||||
// - s3:prefix
|
||||
// - s3:max-keys
|
||||
var conditionMatches = true
|
||||
for condition, conditionKeys := range statement.Conditions {
|
||||
if condition == "StringEquals" {
|
||||
if conditionKeys["s3:prefix"] != conditions["prefix"] {
|
||||
conditionMatches = false
|
||||
break
|
||||
}
|
||||
if conditionKeys["s3:max-keys"] != conditions["max-keys"] {
|
||||
conditionMatches = false
|
||||
break
|
||||
}
|
||||
} else if condition == "StringNotEquals" {
|
||||
if conditionKeys["s3:prefix"] == conditions["prefix"] {
|
||||
conditionMatches = false
|
||||
break
|
||||
}
|
||||
if conditionKeys["s3:max-keys"] == conditions["max-keys"] {
|
||||
conditionMatches = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return conditionMatches
|
||||
}
|
||||
|
||||
// PutBucketPolicyHandler - PUT Bucket policy
|
||||
// -----------------
|
||||
// This implementation of the PUT operation uses the policy
|
||||
// subresource to add to or replace a policy on a bucket
|
||||
func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
return
|
||||
case authTypePresigned, authTypeSigned:
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If Content-Length is unknown or zero, deny the
|
||||
// request. PutBucketPolicy always needs a Content-Length if
|
||||
// incoming request is not chunked.
|
||||
if !contains(r.TransferEncoding, "chunked") {
|
||||
if r.ContentLength == -1 || r.ContentLength == 0 {
|
||||
writeErrorResponse(w, r, ErrMissingContentLength, r.URL.Path)
|
||||
return
|
||||
}
|
||||
// If Content-Length is greater than maximum allowed policy size.
|
||||
if r.ContentLength > maxAccessPolicySize {
|
||||
writeErrorResponse(w, r, ErrEntityTooLarge, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Read access policy up to maxAccessPolicySize.
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
|
||||
// bucket policies are limited to 20KB in size, using a limit reader.
|
||||
bucketPolicyBuf, err := ioutil.ReadAll(io.LimitReader(r.Body, maxAccessPolicySize))
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to read bucket policy.")
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse bucket policy.
|
||||
bucketPolicy, err := parseBucketPolicy(bucketPolicyBuf)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to parse bucket policy.")
|
||||
writeErrorResponse(w, r, ErrInvalidPolicyDocument, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse check bucket policy.
|
||||
if s3Error := checkBucketPolicyResources(bucket, bucketPolicy); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
// Save bucket policy.
|
||||
if err := writeBucketPolicy(bucket, bucketPolicyBuf); err != nil {
|
||||
errorIf(err, "Unable to write bucket policy.")
|
||||
switch err.(type) {
|
||||
case BucketNameInvalid:
|
||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
}
|
||||
return
|
||||
}
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
// DeleteBucketPolicyHandler - DELETE Bucket policy
|
||||
// -----------------
|
||||
// This implementation of the DELETE operation uses the policy
|
||||
// subresource to add to remove a policy on a bucket.
|
||||
func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
return
|
||||
case authTypePresigned, authTypeSigned:
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Delete bucket access policy.
|
||||
if err := removeBucketPolicy(bucket); err != nil {
|
||||
errorIf(err, "Unable to remove bucket policy.")
|
||||
switch err.(type) {
|
||||
case BucketNameInvalid:
|
||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
||||
case BucketPolicyNotFound:
|
||||
writeErrorResponse(w, r, ErrNoSuchBucketPolicy, r.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
}
|
||||
return
|
||||
}
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
// GetBucketPolicyHandler - GET Bucket policy
|
||||
// -----------------
|
||||
// This operation uses the policy
|
||||
// subresource to return the policy of a specified bucket.
|
||||
func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
switch getRequestAuthType(r) {
|
||||
default:
|
||||
// For all unknown auth types return error.
|
||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
||||
return
|
||||
case authTypePresigned, authTypeSigned:
|
||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Read bucket access policy.
|
||||
p, err := readBucketPolicy(bucket)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to read bucket policy.")
|
||||
switch err.(type) {
|
||||
case BucketNameInvalid:
|
||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
||||
case BucketPolicyNotFound:
|
||||
writeErrorResponse(w, r, ErrNoSuchBucketPolicy, r.URL.Path)
|
||||
default:
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
}
|
||||
return
|
||||
}
|
||||
io.Copy(w, bytes.NewReader(p))
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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"
|
||||
"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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// This file implements AWS Access Policy Language parser in
|
||||
// accordance with http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// AWSResourcePrefix - bucket policy resource prefix.
|
||||
AWSResourcePrefix = "arn:aws:s3:::"
|
||||
)
|
||||
|
||||
// supportedActionMap - lists all the actions supported by minio.
|
||||
var supportedActionMap = map[string]struct{}{
|
||||
"s3:GetObject": {},
|
||||
"s3:ListBucket": {},
|
||||
"s3:PutObject": {},
|
||||
"s3:GetBucketLocation": {},
|
||||
"s3:DeleteObject": {},
|
||||
"s3:AbortMultipartUpload": {},
|
||||
"s3:ListBucketMultipartUploads": {},
|
||||
"s3:ListMultipartUploadParts": {},
|
||||
}
|
||||
|
||||
// supported Conditions type.
|
||||
var supportedConditionsType = map[string]struct{}{
|
||||
"StringEquals": {},
|
||||
"StringNotEquals": {},
|
||||
}
|
||||
|
||||
// Validate s3:prefix, s3:max-keys are present if not
|
||||
// supported keys for the conditions.
|
||||
var supportedConditionsKey = map[string]struct{}{
|
||||
"s3:prefix": {},
|
||||
"s3:max-keys": {},
|
||||
}
|
||||
|
||||
// User - canonical users list.
|
||||
type policyUser struct {
|
||||
AWS []string
|
||||
}
|
||||
|
||||
// Statement - minio policy statement
|
||||
type policyStatement struct {
|
||||
Sid string
|
||||
Effect string
|
||||
Principal policyUser `json:"Principal"`
|
||||
Actions []string `json:"Action"`
|
||||
Resources []string `json:"Resource"`
|
||||
Conditions map[string]map[string]string `json:"Condition"`
|
||||
}
|
||||
|
||||
// BucketPolicy - minio policy collection
|
||||
type BucketPolicy struct {
|
||||
Version string // date in 0000-00-00 format
|
||||
Statements []policyStatement `json:"Statement"`
|
||||
}
|
||||
|
||||
// supportedEffectMap - supported effects.
|
||||
var supportedEffectMap = map[string]struct{}{
|
||||
"Allow": {},
|
||||
"Deny": {},
|
||||
}
|
||||
|
||||
// isValidActions - are actions valid.
|
||||
func isValidActions(actions []string) (err error) {
|
||||
// Statement actions cannot be empty.
|
||||
if len(actions) == 0 {
|
||||
err = errors.New("Action list cannot be empty.")
|
||||
return err
|
||||
}
|
||||
for _, action := range actions {
|
||||
if _, ok := supportedActionMap[action]; !ok {
|
||||
err = errors.New("Unsupported action found: ‘" + action + "’, please validate your policy document.")
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidEffect - is effect valid.
|
||||
func isValidEffect(effect string) error {
|
||||
// Statement effect cannot be empty.
|
||||
if len(effect) == 0 {
|
||||
err := errors.New("Policy effect cannot be empty.")
|
||||
return err
|
||||
}
|
||||
_, ok := supportedEffectMap[effect]
|
||||
if !ok {
|
||||
err := errors.New("Unsupported Effect found: ‘" + effect + "’, please validate your policy document.")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidResources - are valid resources.
|
||||
func isValidResources(resources []string) (err error) {
|
||||
// Statement resources cannot be empty.
|
||||
if len(resources) == 0 {
|
||||
err = errors.New("Resource list cannot be empty.")
|
||||
return err
|
||||
}
|
||||
for _, resource := range resources {
|
||||
if !strings.HasPrefix(resource, AWSResourcePrefix) {
|
||||
err = errors.New("Unsupported resource style found: ‘" + resource + "’, please validate your policy document.")
|
||||
return err
|
||||
}
|
||||
resourceSuffix := strings.SplitAfter(resource, AWSResourcePrefix)[1]
|
||||
if len(resourceSuffix) == 0 || strings.HasPrefix(resourceSuffix, "/") {
|
||||
err = errors.New("Invalid resource style found: ‘" + resource + "’, please validate your policy document.")
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidPrincipals - are valid principals.
|
||||
func isValidPrincipals(principals []string) (err error) {
|
||||
// Statement principal should have a value.
|
||||
if len(principals) == 0 {
|
||||
err = errors.New("Principal cannot be empty.")
|
||||
return err
|
||||
}
|
||||
for _, principal := range principals {
|
||||
// Minio does not support or implement IAM, "*" is the only valid value.
|
||||
// Amazon s3 doc on principals: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Principal
|
||||
if principal != "*" {
|
||||
err = fmt.Errorf("Unsupported principal style found: ‘%s’, please validate your policy document.", principal)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidConditions - are valid conditions.
|
||||
func isValidConditions(conditions map[string]map[string]string) (err error) {
|
||||
// Returns true if string 'a' is found in the list.
|
||||
findString := func(a string, list []string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
conditionKeyVal := make(map[string][]string)
|
||||
// Verify conditions should be valid.
|
||||
// Validate if stringEquals, stringNotEquals are present
|
||||
// if not throw an error.
|
||||
for conditionType := range conditions {
|
||||
_, validType := supportedConditionsType[conditionType]
|
||||
if !validType {
|
||||
err = fmt.Errorf("Unsupported condition type '%s', please validate your policy document.", conditionType)
|
||||
return err
|
||||
}
|
||||
for key := range conditions[conditionType] {
|
||||
_, validKey := supportedConditionsKey[key]
|
||||
if !validKey {
|
||||
err = fmt.Errorf("Unsupported condition key '%s', please validate your policy document.", conditionType)
|
||||
return err
|
||||
}
|
||||
conditionArray, ok := conditionKeyVal[key]
|
||||
if ok && findString(conditions[conditionType][key], conditionArray) {
|
||||
err = fmt.Errorf("Ambigious condition values for key '%s', please validate your policy document.", key)
|
||||
return err
|
||||
}
|
||||
conditionKeyVal[key] = append(conditionKeyVal[key], conditions[conditionType][key])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List of actions for which prefixes are not allowed.
|
||||
var invalidPrefixActions = map[string]struct{}{
|
||||
"s3:GetBucketLocation": {},
|
||||
"s3:ListBucket": {},
|
||||
"s3:ListBucketMultipartUploads": {},
|
||||
// Add actions which do not honor prefixes.
|
||||
}
|
||||
|
||||
// resourcePrefix - provides the prefix removing any wildcards.
|
||||
func resourcePrefix(resource string) string {
|
||||
if strings.HasSuffix(resource, "*") {
|
||||
resource = strings.TrimSuffix(resource, "*")
|
||||
}
|
||||
return path.Clean(resource)
|
||||
}
|
||||
|
||||
// checkBucketPolicyResources validates Resources in unmarshalled bucket policy structure.
|
||||
// First valation of Resources done for given set of Actions.
|
||||
// Later its validated for recursive Resources.
|
||||
func checkBucketPolicyResources(bucket string, bucketPolicy BucketPolicy) APIErrorCode {
|
||||
// Validate statements for special actions and collect resources
|
||||
// for others to validate nesting.
|
||||
var resourceMap = make(map[string]struct{})
|
||||
for _, statement := range bucketPolicy.Statements {
|
||||
for _, action := range statement.Actions {
|
||||
for _, resource := range statement.Resources {
|
||||
resourcePrefix := strings.SplitAfter(resource, AWSResourcePrefix)[1]
|
||||
if _, ok := invalidPrefixActions[action]; ok {
|
||||
// Resource prefix is not equal to bucket for
|
||||
// prefix invalid actions, reject them.
|
||||
if resourcePrefix != bucket {
|
||||
return ErrMalformedPolicy
|
||||
}
|
||||
} else {
|
||||
// For all other actions validate if resourcePrefix begins
|
||||
// with bucket name, if not reject them.
|
||||
if strings.Split(resourcePrefix, "/")[0] != bucket {
|
||||
return ErrMalformedPolicy
|
||||
}
|
||||
// All valid resources collect them separately to verify nesting.
|
||||
resourceMap[resourcePrefix] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var resources []string
|
||||
for resource := range resourceMap {
|
||||
resources = append(resources, resourcePrefix(resource))
|
||||
}
|
||||
|
||||
// Sort strings as shorter first.
|
||||
sort.Strings(resources)
|
||||
|
||||
for len(resources) > 1 {
|
||||
var resource string
|
||||
resource, resources = resources[0], resources[1:]
|
||||
// Loop through all resources, if one of them matches with
|
||||
// previous shorter one, it means we have detected
|
||||
// nesting. Reject such rules.
|
||||
for _, otherResource := range resources {
|
||||
// Common prefix reject such rules.
|
||||
if strings.HasPrefix(otherResource, resource) {
|
||||
return ErrMalformedPolicy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No errors found.
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// parseBucketPolicy - parses and validates if bucket policy is of
|
||||
// proper JSON and follows allowed restrictions with policy standards.
|
||||
func parseBucketPolicy(bucketPolicyBuf []byte) (policy BucketPolicy, err error) {
|
||||
if err = json.Unmarshal(bucketPolicyBuf, &policy); err != nil {
|
||||
return BucketPolicy{}, err
|
||||
}
|
||||
|
||||
// Policy version cannot be empty.
|
||||
if len(policy.Version) == 0 {
|
||||
err = errors.New("Policy version cannot be empty.")
|
||||
return BucketPolicy{}, err
|
||||
}
|
||||
|
||||
// Policy statements cannot be empty.
|
||||
if len(policy.Statements) == 0 {
|
||||
err = errors.New("Policy statement cannot be empty.")
|
||||
return BucketPolicy{}, 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
|
||||
}
|
||||
// Statement principal should be supported format.
|
||||
if err := isValidPrincipals(statement.Principal.AWS); err != nil {
|
||||
return BucketPolicy{}, err
|
||||
}
|
||||
// Statement actions should be valid.
|
||||
if err := isValidActions(statement.Actions); err != nil {
|
||||
return BucketPolicy{}, err
|
||||
}
|
||||
// Statement resources should be valid.
|
||||
if err := isValidResources(statement.Resources); err != nil {
|
||||
return BucketPolicy{}, err
|
||||
}
|
||||
// Statement conditions should be valid.
|
||||
if err := isValidConditions(statement.Conditions); err != nil {
|
||||
return BucketPolicy{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Separate deny and allow statements, so that we can apply deny
|
||||
// statements in the beginning followed by Allow statements.
|
||||
var denyStatements []policyStatement
|
||||
var allowStatements []policyStatement
|
||||
for _, statement := range policy.Statements {
|
||||
if statement.Effect == "Deny" {
|
||||
denyStatements = append(denyStatements, statement)
|
||||
continue
|
||||
}
|
||||
// else if statement.Effect == "Allow"
|
||||
allowStatements = append(allowStatements, statement)
|
||||
}
|
||||
|
||||
// Deny statements are enforced first once matched.
|
||||
policy.Statements = append(denyStatements, allowStatements...)
|
||||
|
||||
// Return successfully parsed policy structure.
|
||||
return policy, nil
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"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 statements for read-write BucketPolicy.
|
||||
func setReadWriteStatement(bucketName, objectPrefix string) []policyStatement {
|
||||
bucketResourceStatement := policyStatement{}
|
||||
objectResourceStatement := policyStatement{}
|
||||
statements := []policyStatement{}
|
||||
|
||||
bucketResourceStatement.Effect = "Allow"
|
||||
bucketResourceStatement.Principal.AWS = []string{"*"}
|
||||
bucketResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)}
|
||||
bucketResourceStatement.Actions = readWriteBucketActions
|
||||
objectResourceStatement.Effect = "Allow"
|
||||
objectResourceStatement.Principal.AWS = []string{"*"}
|
||||
objectResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")}
|
||||
objectResourceStatement.Actions = readWriteObjectActions
|
||||
// Save the read write policy.
|
||||
statements = append(statements, bucketResourceStatement, objectResourceStatement)
|
||||
return statements
|
||||
}
|
||||
|
||||
// Obtain statements for read only BucketPolicy.
|
||||
func setReadOnlyStatement(bucketName, objectPrefix string) []policyStatement {
|
||||
bucketResourceStatement := policyStatement{}
|
||||
objectResourceStatement := policyStatement{}
|
||||
statements := []policyStatement{}
|
||||
|
||||
bucketResourceStatement.Effect = "Allow"
|
||||
bucketResourceStatement.Principal.AWS = []string{"*"}
|
||||
bucketResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)}
|
||||
bucketResourceStatement.Actions = readOnlyBucketActions
|
||||
objectResourceStatement.Effect = "Allow"
|
||||
objectResourceStatement.Principal.AWS = []string{"*"}
|
||||
objectResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")}
|
||||
objectResourceStatement.Actions = readOnlyObjectActions
|
||||
// Save the read only policy.
|
||||
statements = append(statements, bucketResourceStatement, objectResourceStatement)
|
||||
return statements
|
||||
}
|
||||
|
||||
// Obtain statements for write only BucketPolicy.
|
||||
func setWriteOnlyStatement(bucketName, objectPrefix string) []policyStatement {
|
||||
bucketResourceStatement := policyStatement{}
|
||||
objectResourceStatement := policyStatement{}
|
||||
statements := []policyStatement{}
|
||||
// Write only policy.
|
||||
bucketResourceStatement.Effect = "Allow"
|
||||
bucketResourceStatement.Principal.AWS = []string{"*"}
|
||||
bucketResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName)}
|
||||
bucketResourceStatement.Actions = writeOnlyBucketActions
|
||||
objectResourceStatement.Effect = "Allow"
|
||||
objectResourceStatement.Principal.AWS = []string{"*"}
|
||||
objectResourceStatement.Resources = []string{fmt.Sprintf("%s%s", AWSResourcePrefix, bucketName+"/"+objectPrefix+"*")}
|
||||
objectResourceStatement.Actions = writeOnlyObjectActions
|
||||
// Save the write only policy.
|
||||
statements = append(statements, bucketResourceStatement, objectResourceStatement)
|
||||
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: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
|
||||
}
|
||||
// contructing 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: setReadOnlyStatement("minio-bucket", "")},
|
||||
// BucketPolicy - 2.
|
||||
// Contains valid read-write only policy statement.
|
||||
{Version: "1.0", Statements: setReadWriteStatement("minio-bucket", "Asia/")},
|
||||
// BucketPolicy - 3.
|
||||
// Contains valid write only policy statement.
|
||||
{Version: "1.0", Statements: setWriteOnlyStatement("minio-bucket", "Asia/India/")},
|
||||
// BucketPolicy - 4.
|
||||
// Contains invalidPrefixActions.
|
||||
// Since resourcePrefix is not to the bucket-name, it return ErrMalformedPolicy.
|
||||
{Version: "1.0", Statements: setReadOnlyStatement("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(setWriteOnlyStatement("minio-bucket-fail", "Asia/India/"))},
|
||||
// BucketPolicy - 6.
|
||||
// contructing policy statement with recursive resources.
|
||||
// should result in ErrMalformedPolicy
|
||||
{Version: "1.0", Statements: setRecurseResource(setValidPrefixActions(setWriteOnlyStatement("minio-bucket", "")))},
|
||||
// BucketPolciy - 7.
|
||||
// constructing policy statement with non recursive but
|
||||
// lexically close resources.
|
||||
// should result in ErrNone.
|
||||
{Version: "1.0", Statements: setResourceLexical(setValidPrefixActions(setWriteOnlyStatement("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.
|
||||
// contructing policy statement with recursive resources.
|
||||
// should result in ErrMalformedPolicy.
|
||||
{bucketAccessPolicies[5], ErrMalformedPolicy, 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: setReadOnlyStatement("minio-bucket", "")},
|
||||
// BucketPolicy - 3.
|
||||
// Read-Write bucket policy.
|
||||
{Version: "1.0", Statements: setReadWriteStatement("minio-bucket", "Asia/")},
|
||||
// BucketPolicy - 4.
|
||||
// Write only bucket policy.
|
||||
{Version: "1.0", Statements: setWriteOnlyStatement("minio-bucket", "Asia/India/")},
|
||||
// BucketPolicy - 5.
|
||||
// BucketPolicy statement contains unsupported action.
|
||||
{Version: "1.0", Statements: setUnsupportedActions(setReadOnlyStatement("minio-bucket", ""))},
|
||||
// BucketPolicy - 6.
|
||||
// BucketPolicy statement contains unsupported Effect.
|
||||
{Version: "1.0", Statements: setUnsupportedEffect(setReadWriteStatement("minio-bucket", "Asia/"))},
|
||||
// BucketPolicy - 7.
|
||||
// BucketPolicy statement contains unsupported Principal.
|
||||
{Version: "1.0", Statements: setUnsupportedPrincipals(setWriteOnlyStatement("minio-bucket", "Asia/India/"))},
|
||||
// BucketPolicy - 8.
|
||||
// BucketPolicy statement contains unsupported Resource.
|
||||
{Version: "1.0", Statements: setUnsupportedResources(setWriteOnlyStatement("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 {
|
||||
inputPolicyBytes, e := json.Marshal(testCase.inputPolicy)
|
||||
if e != nil {
|
||||
t.Fatalf("Test %d: Couldn't Marshal bucket policy", i+1)
|
||||
}
|
||||
|
||||
actualAccessPolicy, err := parseBucketPolicy(inputPolicyBytes)
|
||||
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 !reflect.DeepEqual(testCase.expectedPolicy, actualAccessPolicy) {
|
||||
t.Errorf("Test %d: The expected statements from resource statement generator doesn't match the actual statements", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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 (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// getBucketsConfigPath - get buckets path.
|
||||
func getBucketsConfigPath() (string, error) {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(configPath, "buckets"), nil
|
||||
}
|
||||
|
||||
// getBucketConfigPath - get bucket config path.
|
||||
func getBucketConfigPath(bucket string) (string, error) {
|
||||
bucketsConfigPath, err := getBucketsConfigPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(bucketsConfigPath, bucket), nil
|
||||
}
|
||||
|
||||
// createBucketConfigPath - create bucket config directory.
|
||||
func createBucketConfigPath(bucket string) error {
|
||||
bucketConfigPath, err := getBucketConfigPath(bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.MkdirAll(bucketConfigPath, 0700)
|
||||
}
|
||||
|
||||
// readBucketPolicy - read bucket policy.
|
||||
func readBucketPolicy(bucket string) ([]byte, error) {
|
||||
// Verify bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return nil, BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
|
||||
bucketConfigPath, err := getBucketConfigPath(bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get policy file.
|
||||
bucketPolicyFile := filepath.Join(bucketConfigPath, "access-policy.json")
|
||||
if _, err = os.Stat(bucketPolicyFile); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, BucketPolicyNotFound{Bucket: bucket}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return ioutil.ReadFile(bucketPolicyFile)
|
||||
}
|
||||
|
||||
// removeBucketPolicy - remove bucket policy.
|
||||
func removeBucketPolicy(bucket string) error {
|
||||
// Verify bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
|
||||
bucketConfigPath, err := getBucketConfigPath(bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get policy file.
|
||||
bucketPolicyFile := filepath.Join(bucketConfigPath, "access-policy.json")
|
||||
if _, err = os.Stat(bucketPolicyFile); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return BucketPolicyNotFound{Bucket: bucket}
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(bucketPolicyFile); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeBucketPolicy - save bucket policy.
|
||||
func writeBucketPolicy(bucket string, accessPolicyBytes []byte) error {
|
||||
// Verify if bucket path legal
|
||||
if !IsValidBucketName(bucket) {
|
||||
return BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
|
||||
// Create bucket config path.
|
||||
if err := createBucketConfigPath(bucket); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bucketConfigPath, err := getBucketConfigPath(bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get policy file.
|
||||
bucketPolicyFile := filepath.Join(bucketConfigPath, "access-policy.json")
|
||||
if _, err := os.Stat(bucketPolicyFile); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Write bucket policy.
|
||||
return ioutil.WriteFile(bucketPolicyFile, accessPolicyBytes, 0600)
|
||||
}
|
||||
+13
-4
@@ -16,9 +16,18 @@
|
||||
|
||||
package main
|
||||
|
||||
// DO NOT EDIT THIS FILE DIRECTLY. These are build-time constants
|
||||
// set through ‘buildscripts/gen-ldflags.go’.
|
||||
var (
|
||||
minioVersion = "UNOFFICIAL.GOGET"
|
||||
minioReleaseTag = "UNOFFICIAL.GOGET"
|
||||
minioCommitID = "UNOFFICIAL.GOGET"
|
||||
minioShortCommitID = minioCommitID[:]
|
||||
// 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 minioCommitID.
|
||||
minioShortCommitID = minioCommitID[:12]
|
||||
)
|
||||
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
|
||||
_init() {
|
||||
# Save release LDFLAGS
|
||||
LDFLAGS=$(go run buildscripts/gen-ldflags.go)
|
||||
|
||||
# Extract release tag
|
||||
release_tag=$(echo $LDFLAGS | awk {'print $4'} | cut -f2 -d=)
|
||||
|
||||
# Verify release tag.
|
||||
if [ -z "$release_tag" ]; then
|
||||
echo "Release tag cannot be empty. Please check return value of \`go run buildscripts/gen-ldflags.go\`"
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# Extract release string.
|
||||
release_str=$(echo $MINIO_RELEASE | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Verify release string.
|
||||
if [ -z "$release_str" ]; then
|
||||
echo "Release string cannot be empty. Please set \`MINIO_RELEASE\` env variable."
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# List of supported architectures
|
||||
SUPPORTED_OSARCH='linux/386 linux/amd64 linux/arm windows/386 windows/amd64 darwin/amd64 freebsd/amd64'
|
||||
|
||||
## System binaries
|
||||
CP=`which cp`
|
||||
SHASUM=`which shasum`
|
||||
SED=`which sed`
|
||||
}
|
||||
|
||||
go_build() {
|
||||
local osarch=$1
|
||||
os=$(echo $osarch | cut -f1 -d'/')
|
||||
arch=$(echo $osarch | cut -f2 -d'/')
|
||||
package=$(go list -f '{{.ImportPath}}')
|
||||
echo -n "-->"
|
||||
printf "%15s:%s\n" "${osarch}" "${package}"
|
||||
|
||||
# Release binary name
|
||||
release_bin="$release_str/$os-$arch/$(basename $package).$release_tag"
|
||||
# Release binary downloadable name
|
||||
release_real_bin="$release_str/$os-$arch/$(basename $package)"
|
||||
# Release shasum name
|
||||
release_shasum="$release_str/$os-$arch/$(basename $package).shasum"
|
||||
|
||||
# Go build to build the binary.
|
||||
GOOS=$os GOARCH=$arch go build --ldflags "${LDFLAGS}" -o $release_bin
|
||||
|
||||
# Create copy
|
||||
if [ $os == "windows" ]; then
|
||||
$CP -p $release_bin ${release_real_bin}.exe
|
||||
else
|
||||
$CP -p $release_bin $release_real_bin
|
||||
fi
|
||||
|
||||
# Calculate shasum
|
||||
shasum_str=$(${SHASUM} ${release_bin})
|
||||
echo ${shasum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_shasum
|
||||
}
|
||||
|
||||
main() {
|
||||
# Build releases.
|
||||
echo "Executing $release_str builds for OS: ${SUPPORTED_OSARCH}"
|
||||
echo "Choose an OS Arch from the below"
|
||||
for osarch in ${SUPPORTED_OSARCH}; do
|
||||
echo ${osarch}
|
||||
done
|
||||
|
||||
read -p "If you want to build for all, Just press Enter: " chosen_osarch
|
||||
if [ "$chosen_osarch" = "" ]; then
|
||||
for each_osarch in ${SUPPORTED_OSARCH}; do
|
||||
go_build ${each_osarch}
|
||||
done
|
||||
else
|
||||
go_build ${chosen_osarch}
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
# Run main.
|
||||
_init && main
|
||||
@@ -21,8 +21,7 @@ _init() {
|
||||
|
||||
## Minimum required versions for build dependencies
|
||||
GIT_VERSION="1.0"
|
||||
GPG_VERSION="1.3"
|
||||
GO_VERSION="1.5.1"
|
||||
GO_VERSION="1.6"
|
||||
OSX_VERSION="10.8"
|
||||
UNAME=$(uname -sm)
|
||||
|
||||
@@ -141,6 +140,9 @@ is_supported_os() {
|
||||
"Linux")
|
||||
os="linux"
|
||||
;;
|
||||
"FreeBSD")
|
||||
os="freebsd"
|
||||
;;
|
||||
"Darwin")
|
||||
osx_host_version=$(env sw_vers -productVersion)
|
||||
check_version "${osx_host_version}" "${OSX_VERSION}"
|
||||
@@ -155,7 +157,7 @@ is_supported_os() {
|
||||
is_supported_arch() {
|
||||
local supported
|
||||
case ${UNAME##* } in
|
||||
"x86_64")
|
||||
"x86_64" | "amd64")
|
||||
supported=1
|
||||
;;
|
||||
"arm"*)
|
||||
@@ -181,11 +183,6 @@ check_deps() {
|
||||
if [ $? -ge 2 ]; then
|
||||
MISSING="${MISSING} git"
|
||||
fi
|
||||
|
||||
check_version "$(env gpg --version 2>/dev/null | sed -e 's/^.* \([0-9.\].*\).*$/\1/' -e 's/^\([0-9.\]*\).*/\1/g' | head -1)" "${GPG_VERSION}"
|
||||
if [ $? -ge 2 ]; then
|
||||
MISSING="${MISSING} gpg"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
_init() {
|
||||
|
||||
shopt -s extglob
|
||||
|
||||
PWD=$(pwd -P)
|
||||
GOPATH=$(cd "$(go env GOPATH)" ; env pwd -P)
|
||||
}
|
||||
|
||||
main() {
|
||||
echo "Checking if project is at ${GOPATH}"
|
||||
for minio in $(echo ${GOPATH} | tr ':' ' '); do
|
||||
if [ ! -d ${minio}/src/github.com/minio/minio ]; then
|
||||
echo "Project not found in ${minio}, please follow instructions provided at https://github.com/minio/minio/blob/master/CONTRIBUTING.md#setup-your-minio-github-repository" \
|
||||
&& exit 1
|
||||
fi
|
||||
if [ "x${minio}/src/github.com/minio/minio" != "x${PWD}" ]; then
|
||||
echo "Build outside of ${minio}, two source checkouts found. Exiting." && exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
_init && main
|
||||
|
||||
@@ -28,16 +28,17 @@ 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
|
||||
}
|
||||
|
||||
// genReleaseTag prints release tag to the console for easy git tagging.
|
||||
func releaseTag(version string) string {
|
||||
relPrefix := "UNOFFICIAL"
|
||||
relPrefix := "DEVELOPMENT"
|
||||
if prefix := os.Getenv("MINIO_RELEASE"); prefix != "" {
|
||||
relPrefix = prefix
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
"github.com/minio/go-homedir"
|
||||
)
|
||||
|
||||
// createCertsPath create certs path.
|
||||
func createCertsPath() error {
|
||||
certsPath, err := getCertsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.MkdirAll(certsPath, 0700)
|
||||
}
|
||||
|
||||
// getCertsPath get certs path.
|
||||
func getCertsPath() (string, error) {
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
certsPath := filepath.Join(homeDir, globalMinioCertsDir)
|
||||
return certsPath, nil
|
||||
}
|
||||
|
||||
// mustGetCertsPath must get certs path.
|
||||
func mustGetCertsPath() string {
|
||||
certsPath, err := getCertsPath()
|
||||
fatalIf(err, "Failed to get certificate path.")
|
||||
return certsPath
|
||||
}
|
||||
|
||||
// mustGetCertFile must get cert file.
|
||||
func mustGetCertFile() string {
|
||||
return filepath.Join(mustGetCertsPath(), globalMinioCertFile)
|
||||
}
|
||||
|
||||
// mustGetKeyFile must get key file.
|
||||
func mustGetKeyFile() string {
|
||||
return filepath.Join(mustGetCertsPath(), globalMinioKeyFile)
|
||||
}
|
||||
|
||||
// isCertFileExists verifies if cert file exists, returns true if
|
||||
// found, false otherwise.
|
||||
func isCertFileExists() bool {
|
||||
st, e := os.Stat(filepath.Join(mustGetCertsPath(), globalMinioCertFile))
|
||||
// If file exists and is regular return true.
|
||||
if e == nil && st.Mode().IsRegular() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isKeyFileExists verifies if key file exists, returns true if found,
|
||||
// false otherwise.
|
||||
func isKeyFileExists() bool {
|
||||
st, e := os.Stat(filepath.Join(mustGetCertsPath(), globalMinioKeyFile))
|
||||
// If file exists and is regular return true.
|
||||
if e == nil && st.Mode().IsRegular() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isSSL - returns true with both cert and key exists.
|
||||
func isSSL() bool {
|
||||
if isCertFileExists() && isKeyFileExists() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
+16
-3
@@ -18,13 +18,26 @@ package main
|
||||
|
||||
import "github.com/minio/cli"
|
||||
|
||||
// Collection of minio commands currently supported are
|
||||
// Collection of minio commands currently supported are.
|
||||
var commands = []cli.Command{}
|
||||
|
||||
// Collection of minio commands currently supported in a trie tree
|
||||
// Collection of minio commands currently supported in a trie tree.
|
||||
var commandsTree = newTrie()
|
||||
|
||||
// registerCommand registers a cli command
|
||||
// Collection of minio flags currently supported.
|
||||
var globalFlags = []cli.Flag{
|
||||
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.
|
||||
func registerCommand(command cli.Command) {
|
||||
commands = append(commands, command)
|
||||
commandsTree.Insert(command.Name)
|
||||
|
||||
@@ -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 (
|
||||
"runtime"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio-xl/pkg/probe"
|
||||
)
|
||||
|
||||
// Configure logger
|
||||
var configLoggerCmd = cli.Command{
|
||||
Name: "logger",
|
||||
Usage: "Configure logger.",
|
||||
Action: mainConfigLogger,
|
||||
CustomHelpTemplate: `NAME:
|
||||
minio config {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
minio config {{.Name}} OPERATION [ARGS...]
|
||||
|
||||
OPERATION = add | list | remove
|
||||
|
||||
EXAMPLES:
|
||||
1. Configure new mongo logger.
|
||||
$ minio config {{.Name}} add mongo localhost:28710 mydb mylogger
|
||||
|
||||
2. Configure new syslog logger. NOTE: syslog logger is not supported on windows.
|
||||
$ minio config {{.Name}} add syslog localhost:554 udp
|
||||
|
||||
3. Configure new file logger. "/var/log" should be writable by user.
|
||||
$ minio config {{.Name}} add file /var/log/minio.log
|
||||
|
||||
4. List currently configured logger.
|
||||
$ minio config {{.Name}} list
|
||||
|
||||
5. Remove/Reset a configured logger.
|
||||
$ minio config {{.Name}} remove mongo
|
||||
`,
|
||||
}
|
||||
|
||||
// Inherit at one place
|
||||
type config struct {
|
||||
*configV2
|
||||
}
|
||||
|
||||
func mainConfigLogger(ctx *cli.Context) {
|
||||
if !ctx.Args().Present() || ctx.Args().First() == "help" {
|
||||
cli.ShowCommandHelpAndExit(ctx, "logger", 1) // last argument is exit code
|
||||
}
|
||||
conf, err := loadConfigV2()
|
||||
fatalIf(err.Trace(), "Unable to load config", nil)
|
||||
|
||||
if ctx.Args().Get(0) == "add" {
|
||||
args := ctx.Args().Tail()
|
||||
if args.Get(0) == "mongo" {
|
||||
enableLog2Mongo(&config{conf}, args.Tail())
|
||||
}
|
||||
if args.Get(0) == "syslog" {
|
||||
if runtime.GOOS == "windows" {
|
||||
fatalIf(probe.NewError(errInvalidArgument), "Syslog is not supported on windows.", nil)
|
||||
}
|
||||
enableLog2Syslog(&config{conf}, args.Tail())
|
||||
}
|
||||
if args.Get(0) == "file" {
|
||||
enableLog2File(&config{conf}, args.Tail())
|
||||
}
|
||||
}
|
||||
if ctx.Args().Get(0) == "remove" {
|
||||
args := ctx.Args().Tail()
|
||||
if args.Get(0) == "mongo" {
|
||||
conf.MongoLogger.Addr = ""
|
||||
conf.MongoLogger.DB = ""
|
||||
conf.MongoLogger.Collection = ""
|
||||
err := saveConfig(conf)
|
||||
fatalIf(err.Trace(), "Unable to save config.", nil)
|
||||
}
|
||||
if args.Get(0) == "syslog" {
|
||||
if runtime.GOOS == "windows" {
|
||||
fatalIf(probe.NewError(errInvalidArgument), "Syslog is not supported on windows.", nil)
|
||||
}
|
||||
conf.SyslogLogger.Network = ""
|
||||
conf.SyslogLogger.Addr = ""
|
||||
err := saveConfig(conf)
|
||||
fatalIf(err.Trace(), "Unable to save config.", nil)
|
||||
}
|
||||
if args.Get(0) == "file" {
|
||||
conf.FileLogger.Filename = ""
|
||||
err := saveConfig(conf)
|
||||
fatalIf(err.Trace(), "Unable to save config.", nil)
|
||||
}
|
||||
}
|
||||
if ctx.Args().Get(0) == "list" {
|
||||
Println(conf)
|
||||
}
|
||||
}
|
||||
|
||||
func enableLog2Mongo(conf *config, args cli.Args) {
|
||||
if conf.IsFileLoggingEnabled() {
|
||||
Infoln("File logging already enabled. Removing automatically by enabling mongo.")
|
||||
conf.FileLogger.Filename = ""
|
||||
}
|
||||
if conf.IsSysloggingEnabled() {
|
||||
Infoln("Syslog logging already enabled. Removing automatically by enabling mongo.")
|
||||
conf.SyslogLogger.Addr = ""
|
||||
conf.SyslogLogger.Network = ""
|
||||
}
|
||||
conf.MongoLogger.Addr = args.Get(0)
|
||||
conf.MongoLogger.DB = args.Get(1)
|
||||
conf.MongoLogger.Collection = args.Get(2)
|
||||
|
||||
err := saveConfig(conf.configV2)
|
||||
fatalIf(err.Trace(), "Unable to save mongo logging config.", nil)
|
||||
}
|
||||
|
||||
func enableLog2Syslog(conf *config, args cli.Args) {
|
||||
if conf.IsFileLoggingEnabled() {
|
||||
Infoln("File logging already enabled. Removing automatically by enabling syslog.")
|
||||
conf.FileLogger.Filename = ""
|
||||
}
|
||||
if conf.IsMongoLoggingEnabled() {
|
||||
Infoln("Mongo logging already enabled. Removing automatically by enabling syslog.")
|
||||
conf.MongoLogger.Addr = ""
|
||||
conf.MongoLogger.DB = ""
|
||||
conf.MongoLogger.Collection = ""
|
||||
}
|
||||
conf.SyslogLogger.Addr = args.Get(0)
|
||||
conf.SyslogLogger.Network = args.Get(1)
|
||||
err := saveConfig(conf.configV2)
|
||||
fatalIf(err.Trace(), "Unable to save syslog config.", nil)
|
||||
}
|
||||
|
||||
func enableLog2File(conf *config, args cli.Args) {
|
||||
if conf.IsSysloggingEnabled() {
|
||||
Infoln("Syslog logging already enabled. Removing automatically by enabling file logging.")
|
||||
conf.SyslogLogger.Addr = ""
|
||||
conf.SyslogLogger.Network = ""
|
||||
}
|
||||
if conf.IsMongoLoggingEnabled() {
|
||||
Infoln("Mongo logging already enabled. Removing automatically by enabling file logging.")
|
||||
conf.MongoLogger.Addr = ""
|
||||
conf.MongoLogger.DB = ""
|
||||
conf.MongoLogger.Collection = ""
|
||||
}
|
||||
conf.FileLogger.Filename = args.Get(0)
|
||||
err := saveConfig(conf.configV2)
|
||||
fatalIf(err.Trace(), "Unable to save file logging config.", nil)
|
||||
}
|
||||
@@ -1,53 +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 "github.com/minio/cli"
|
||||
|
||||
// Configure minio server
|
||||
//
|
||||
// ----
|
||||
// NOTE: that the configure command only writes values to the config file.
|
||||
// It does not use any configuration values from the environment variables.
|
||||
// ----
|
||||
//
|
||||
var configCmd = cli.Command{
|
||||
Name: "config",
|
||||
Usage: "Collection of config management commands.",
|
||||
Action: mainConfig,
|
||||
Subcommands: []cli.Command{
|
||||
configLoggerCmd,
|
||||
configVersionCmd,
|
||||
},
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.Name}} {{if .Flags}}[global flags] {{end}}command{{if .Flags}} [command flags]{{end}} [arguments...]
|
||||
|
||||
COMMANDS:
|
||||
{{range .Commands}}{{ .Name }}{{ "\t" }}{{.Usage}}
|
||||
{{end}}
|
||||
`,
|
||||
}
|
||||
|
||||
// mainConfig is the handle for "minio config" command. provides sub-commands which write configuration data in json format to config file.
|
||||
func mainConfig(ctx *cli.Context) {
|
||||
if !ctx.Args().Present() || ctx.Args().First() == "help" {
|
||||
cli.ShowAppHelp(ctx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
func migrateConfig() {
|
||||
// Purge all configs with version '1'.
|
||||
purgeV1()
|
||||
// Migrate version '2' to '3'.
|
||||
migrateV2ToV3()
|
||||
// Migrate version '3' to '4'.
|
||||
migrateV3ToV4()
|
||||
}
|
||||
|
||||
// Version '1' is not supported anymore and deprecated, safe to delete.
|
||||
func purgeV1() {
|
||||
cv1, err := loadConfigV1()
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
fatalIf(err, "Unable to load config version ‘1’.")
|
||||
|
||||
if cv1.Version == "1" {
|
||||
console.Println("Removed unsupported config version ‘1’.")
|
||||
/// Purge old fsUsers.json file
|
||||
configPath, err := getConfigPath()
|
||||
fatalIf(err, "Unable to retrieve config path.")
|
||||
|
||||
configFile := filepath.Join(configPath, "fsUsers.json")
|
||||
os.RemoveAll(configFile)
|
||||
}
|
||||
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 && os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
fatalIf(err, "Unable to load config version ‘2’.")
|
||||
if cv2.Version != "2" {
|
||||
return
|
||||
}
|
||||
srvConfig := &configV3{}
|
||||
srvConfig.Version = "3"
|
||||
srvConfig.Addr = ":9000"
|
||||
srvConfig.Credential = credential{
|
||||
AccessKeyID: cv2.Credentials.AccessKeyID,
|
||||
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",
|
||||
}
|
||||
flogger := fileLogger{}
|
||||
flogger.Level = "error"
|
||||
if cv2.FileLogger.Filename != "" {
|
||||
flogger.Enable = true
|
||||
flogger.Filename = cv2.FileLogger.Filename
|
||||
}
|
||||
srvConfig.Logger.File = flogger
|
||||
|
||||
slogger := syslogLogger{}
|
||||
slogger.Level = "debug"
|
||||
if cv2.SyslogLogger.Addr != "" {
|
||||
slogger.Enable = true
|
||||
slogger.Addr = cv2.SyslogLogger.Addr
|
||||
}
|
||||
srvConfig.Logger.Syslog = slogger
|
||||
|
||||
qc, err := quick.New(srvConfig)
|
||||
fatalIf(err, "Unable to initialize config.")
|
||||
|
||||
configFile, err := getConfigFile()
|
||||
fatalIf(err, "Unable to get config file.")
|
||||
|
||||
// Migrate the config.
|
||||
err = qc.Save(configFile)
|
||||
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.")
|
||||
}
|
||||
|
||||
// Version '3' to '4' migrates config, removes previous fields related
|
||||
// to backend types and server address. This change further simplifies
|
||||
// the config for future additions.
|
||||
func migrateV3ToV4() {
|
||||
cv3, err := loadConfigV3()
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
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.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, "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 ‘"+cv3.Version+"’ to ‘"+srvConfig.Version+"’ failed.")
|
||||
|
||||
console.Println("Migration from version ‘" + cv3.Version + "’ to ‘" + srvConfig.Version + "’ completed successfully.")
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
/////////////////// Config V1 ///////////////////
|
||||
type configV1 struct {
|
||||
Version string `json:"version"`
|
||||
AccessKeyID string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
}
|
||||
|
||||
// loadConfigV1 load config
|
||||
func loadConfigV1() (*configV1, error) {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configFile := filepath.Join(configPath, "fsUsers.json")
|
||||
if _, err = os.Stat(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &configV1{}
|
||||
c.Version = "1"
|
||||
qc, err := quick.New(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := qc.Load(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
/////////////////// Config V2 ///////////////////
|
||||
type configV2 struct {
|
||||
Version string `json:"version"`
|
||||
Credentials struct {
|
||||
AccessKeyID string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
Region string `json:"region"`
|
||||
} `json:"credentials"`
|
||||
MongoLogger struct {
|
||||
Addr string `json:"addr"`
|
||||
DB string `json:"db"`
|
||||
Collection string `json:"collection"`
|
||||
} `json:"mongoLogger"`
|
||||
SyslogLogger struct {
|
||||
Network string `json:"network"`
|
||||
Addr string `json:"addr"`
|
||||
} `json:"syslogLogger"`
|
||||
FileLogger struct {
|
||||
Filename string `json:"filename"`
|
||||
} `json:"fileLogger"`
|
||||
}
|
||||
|
||||
// loadConfigV2 load config version '2'.
|
||||
func loadConfigV2() (*configV2, error) {
|
||||
configFile, err := getConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = os.Stat(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &configV2{}
|
||||
c.Version = "2"
|
||||
qc, err := quick.New(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := qc.Load(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
/////////////////// Config V3 ///////////////////
|
||||
|
||||
// backendV3 type.
|
||||
type backendV3 struct {
|
||||
Type string `json:"type"`
|
||||
Disk string `json:"disk,omitempty"`
|
||||
Disks []string `json:"disks,omitempty"`
|
||||
}
|
||||
|
||||
// loggerV3 type.
|
||||
type loggerV3 struct {
|
||||
Console struct {
|
||||
Enable bool `json:"enable"`
|
||||
Level string `json:"level"`
|
||||
}
|
||||
File struct {
|
||||
Enable bool `json:"enable"`
|
||||
Filename string `json:"fileName"`
|
||||
Level string `json:"level"`
|
||||
}
|
||||
Syslog struct {
|
||||
Enable bool `json:"enable"`
|
||||
Addr string `json:"address"`
|
||||
Level string `json:"level"`
|
||||
} `json:"syslog"`
|
||||
// Add new loggers here.
|
||||
}
|
||||
|
||||
// configV3 server configuration version '3'.
|
||||
type configV3 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// Backend configuration.
|
||||
Backend backendV3 `json:"backend"`
|
||||
|
||||
// http Server configuration.
|
||||
Addr string `json:"address"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger loggerV3 `json:"logger"`
|
||||
}
|
||||
|
||||
// loadConfigV3 load config version '3'.
|
||||
func loadConfigV3() (*configV3, error) {
|
||||
configFile, err := getConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = os.Stat(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &configV3{}
|
||||
c.Version = "3"
|
||||
qc, err := quick.New(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := qc.Load(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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"
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
// serverConfigV4 server configuration version '4'.
|
||||
type serverConfigV4 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger logger `json:"logger"`
|
||||
|
||||
// Read Write mutex.
|
||||
rwMutex *sync.RWMutex
|
||||
}
|
||||
|
||||
// initConfig - initialize server config. config version (called only once).
|
||||
func initConfig() error {
|
||||
if !isConfigFileExists() {
|
||||
srvCfg := &serverConfigV4{}
|
||||
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
|
||||
}
|
||||
|
||||
// Create certs path.
|
||||
err = createCertsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the new config globally.
|
||||
serverConfig = srvCfg
|
||||
|
||||
// Save config into file.
|
||||
return serverConfig.Save()
|
||||
}
|
||||
configFile, err := getConfigFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = os.Stat(configFile); err != nil {
|
||||
return err
|
||||
}
|
||||
srvCfg := &serverConfigV4{}
|
||||
srvCfg.Version = globalMinioConfigVersion
|
||||
srvCfg.rwMutex = &sync.RWMutex{}
|
||||
qc, err := quick.New(srvCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := qc.Load(configFile); err != nil {
|
||||
return err
|
||||
}
|
||||
// Save the loaded config globally.
|
||||
serverConfig = srvCfg
|
||||
// Set the version properly after the unmarshalled json is loaded.
|
||||
serverConfig.Version = globalMinioConfigVersion
|
||||
return nil
|
||||
}
|
||||
|
||||
// serverConfig server config.
|
||||
var serverConfig *serverConfigV4
|
||||
|
||||
// GetVersion get current config version.
|
||||
func (s serverConfigV4) GetVersion() string {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Version
|
||||
}
|
||||
|
||||
/// Logger related.
|
||||
|
||||
// SetFileLogger set new file logger.
|
||||
func (s *serverConfigV4) SetFileLogger(flogger fileLogger) {
|
||||
s.rwMutex.Lock()
|
||||
defer s.rwMutex.Unlock()
|
||||
s.Logger.File = flogger
|
||||
}
|
||||
|
||||
// GetFileLogger get current file logger.
|
||||
func (s serverConfigV4) GetFileLogger() fileLogger {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Logger.File
|
||||
}
|
||||
|
||||
// SetConsoleLogger set new console logger.
|
||||
func (s *serverConfigV4) SetConsoleLogger(clogger consoleLogger) {
|
||||
s.rwMutex.Lock()
|
||||
defer s.rwMutex.Unlock()
|
||||
s.Logger.Console = clogger
|
||||
}
|
||||
|
||||
// GetConsoleLogger get current console logger.
|
||||
func (s serverConfigV4) GetConsoleLogger() consoleLogger {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Logger.Console
|
||||
}
|
||||
|
||||
// SetSyslogLogger set new syslog logger.
|
||||
func (s *serverConfigV4) SetSyslogLogger(slogger syslogLogger) {
|
||||
s.rwMutex.Lock()
|
||||
defer s.rwMutex.Unlock()
|
||||
s.Logger.Syslog = slogger
|
||||
}
|
||||
|
||||
// GetSyslogLogger get current syslog logger.
|
||||
func (s *serverConfigV4) GetSyslogLogger() syslogLogger {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Logger.Syslog
|
||||
}
|
||||
|
||||
// SetRegion set new region.
|
||||
func (s *serverConfigV4) SetRegion(region string) {
|
||||
s.rwMutex.Lock()
|
||||
defer s.rwMutex.Unlock()
|
||||
s.Region = region
|
||||
}
|
||||
|
||||
// GetRegion get current region.
|
||||
func (s serverConfigV4) GetRegion() string {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Region
|
||||
}
|
||||
|
||||
// SetCredentials set new credentials.
|
||||
func (s *serverConfigV4) SetCredential(creds credential) {
|
||||
s.rwMutex.Lock()
|
||||
defer s.rwMutex.Unlock()
|
||||
s.Credential = creds
|
||||
}
|
||||
|
||||
// GetCredentials get current credentials.
|
||||
func (s serverConfigV4) GetCredential() credential {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
return s.Credential
|
||||
}
|
||||
|
||||
// Save config.
|
||||
func (s serverConfigV4) Save() error {
|
||||
s.rwMutex.RLock()
|
||||
defer s.rwMutex.RUnlock()
|
||||
|
||||
// get config file.
|
||||
configFile, err := getConfigFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// initialize quick.
|
||||
qc, err := quick.New(&s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save config file.
|
||||
return qc.Save(configFile)
|
||||
}
|
||||
@@ -1,47 +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 "github.com/minio/cli"
|
||||
|
||||
// Print config version.
|
||||
var configVersionCmd = cli.Command{
|
||||
Name: "version",
|
||||
Usage: "Print config version.",
|
||||
Action: mainConfigVersion,
|
||||
CustomHelpTemplate: `NAME:
|
||||
minio config {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
minio config {{.Name}}
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
func mainConfigVersion(ctx *cli.Context) {
|
||||
if ctx.Args().First() == "help" {
|
||||
cli.ShowCommandHelpAndExit(ctx, "version", 1) // last argument is exit code
|
||||
}
|
||||
|
||||
config, err := loadConfigV2()
|
||||
fatalIf(err.Trace(), "Unable to load config", nil)
|
||||
|
||||
// convert interface{} back to its original struct
|
||||
newConf := config
|
||||
type Version string
|
||||
Println(newConf.Version)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
"github.com/minio/go-homedir"
|
||||
)
|
||||
|
||||
// configPath for custom config path only for testing purposes
|
||||
var customConfigPath string
|
||||
|
||||
// Sets a new config path.
|
||||
func setGlobalConfigPath(configPath string) {
|
||||
customConfigPath = configPath
|
||||
}
|
||||
|
||||
// getConfigPath get server config path
|
||||
func getConfigPath() (string, error) {
|
||||
if customConfigPath != "" {
|
||||
return customConfigPath, nil
|
||||
}
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
configPath := filepath.Join(homeDir, globalMinioConfigDir)
|
||||
return configPath, nil
|
||||
}
|
||||
|
||||
// mustGetConfigPath must get server config path.
|
||||
func mustGetConfigPath() string {
|
||||
configPath, err := getConfigPath()
|
||||
fatalIf(err, "Unable to get config path.")
|
||||
return configPath
|
||||
}
|
||||
|
||||
// createConfigPath create server config path.
|
||||
func createConfigPath() error {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.MkdirAll(configPath, 0700)
|
||||
}
|
||||
|
||||
// isConfigFileExists - returns true if config file exists.
|
||||
func isConfigFileExists() bool {
|
||||
st, err := os.Stat(mustGetConfigFile())
|
||||
// If file exists and is regular return true.
|
||||
if err == nil && st.Mode().IsRegular() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// mustGetConfigFile must get server config file.
|
||||
func mustGetConfigFile() string {
|
||||
configFile, err := getConfigFile()
|
||||
fatalIf(err, "Unable to get config file.")
|
||||
|
||||
return configFile
|
||||
}
|
||||
|
||||
// getConfigFile get server config file.
|
||||
func getConfigFile() (string, error) {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(configPath, globalMinioConfigFile), nil
|
||||
}
|
||||
-170
@@ -1,170 +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"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type logLevel int
|
||||
|
||||
const (
|
||||
levelPrint logLevel = iota + 1
|
||||
levelDebug
|
||||
levelInfo
|
||||
levelError
|
||||
levelFatal
|
||||
)
|
||||
|
||||
var (
|
||||
mutex = &sync.RWMutex{}
|
||||
|
||||
// Fatal prints a error message and exits
|
||||
Fatal = func(a ...interface{}) { privatePrint(levelFatal, a...) }
|
||||
// Fatalln prints a error message with a new line and exits
|
||||
Fatalln = func(a ...interface{}) { privatePrintln(levelFatal, a...) }
|
||||
// Fatalf prints a error message with formatting and exits
|
||||
Fatalf = func(f string, a ...interface{}) { privatePrintf(levelFatal, f, a...) }
|
||||
|
||||
// Error prints a error message
|
||||
Error = func(a ...interface{}) { privatePrint(levelError, a...) }
|
||||
// Errorln prints a error message with a new line
|
||||
Errorln = func(a ...interface{}) { privatePrintln(levelError, a...) }
|
||||
// Errorf prints a error message with formatting
|
||||
Errorf = func(f string, a ...interface{}) { privatePrintf(levelError, f, a...) }
|
||||
|
||||
// Info prints a informational message
|
||||
Info = func(a ...interface{}) { privatePrint(levelInfo, a...) }
|
||||
// Infoln prints a informational message with a new line
|
||||
Infoln = func(a ...interface{}) { privatePrintln(levelInfo, a...) }
|
||||
// Infof prints a informational message with formatting
|
||||
Infof = func(f string, a ...interface{}) { privatePrintf(levelInfo, f, a...) }
|
||||
|
||||
// Debug prints a debug message
|
||||
Debug = func(a ...interface{}) { privatePrint(levelDebug, a...) }
|
||||
// Debugln prints a debug message with a new line
|
||||
Debugln = func(a ...interface{}) { privatePrintln(levelDebug, a...) }
|
||||
// Debugf prints a debug message with formatting
|
||||
Debugf = func(f string, a ...interface{}) { privatePrintf(levelDebug, f, a...) }
|
||||
|
||||
// Print prints a debug message
|
||||
Print = func(a ...interface{}) { privatePrint(levelPrint, a...) }
|
||||
// Println prints a debug message with a new line
|
||||
Println = func(a ...interface{}) { privatePrintln(levelPrint, a...) }
|
||||
// Printf prints a debug message with formatting
|
||||
Printf = func(f string, a ...interface{}) { privatePrintf(levelPrint, f, a...) }
|
||||
)
|
||||
|
||||
var (
|
||||
// print prints a message prefixed with message type and program name
|
||||
privatePrint = func(l logLevel, a ...interface{}) {
|
||||
switch l {
|
||||
case levelDebug:
|
||||
mutex.Lock()
|
||||
fmt.Fprint(os.Stderr, ProgramName()+": <DEBUG> ")
|
||||
fmt.Fprint(os.Stderr, a...)
|
||||
mutex.Unlock()
|
||||
case levelInfo:
|
||||
mutex.Lock()
|
||||
fmt.Print(ProgramName() + ": <INFO> ")
|
||||
fmt.Print(a...)
|
||||
mutex.Unlock()
|
||||
case levelError:
|
||||
mutex.Lock()
|
||||
fmt.Fprint(os.Stderr, ProgramName()+": <ERROR> ")
|
||||
fmt.Fprint(os.Stderr, a...)
|
||||
mutex.Unlock()
|
||||
case levelFatal:
|
||||
mutex.Lock()
|
||||
fmt.Fprint(os.Stderr, ProgramName()+": <FATAL> ")
|
||||
fmt.Fprint(os.Stderr, a...)
|
||||
mutex.Unlock()
|
||||
os.Exit(1)
|
||||
default:
|
||||
fmt.Print(a...)
|
||||
}
|
||||
}
|
||||
|
||||
// println - same as print with a new line
|
||||
privatePrintln = func(l logLevel, a ...interface{}) {
|
||||
switch l {
|
||||
case levelDebug:
|
||||
mutex.Lock()
|
||||
fmt.Fprint(os.Stderr, ProgramName()+": <DEBUG> ")
|
||||
fmt.Fprintln(os.Stderr, a...)
|
||||
mutex.Unlock()
|
||||
case levelInfo:
|
||||
mutex.Lock()
|
||||
fmt.Print(ProgramName() + ": <INFO> ")
|
||||
fmt.Println(a...)
|
||||
mutex.Unlock()
|
||||
case levelError:
|
||||
mutex.Lock()
|
||||
fmt.Fprint(os.Stderr, ProgramName()+": <ERROR> ")
|
||||
fmt.Fprintln(os.Stderr, a...)
|
||||
mutex.Unlock()
|
||||
case levelFatal:
|
||||
mutex.Lock()
|
||||
fmt.Fprint(os.Stderr, ProgramName()+": <FATAL> ")
|
||||
fmt.Fprintln(os.Stderr, a...)
|
||||
mutex.Unlock()
|
||||
os.Exit(1)
|
||||
default:
|
||||
fmt.Println(a...)
|
||||
}
|
||||
}
|
||||
|
||||
// printf - same as print, but takes a format specifier
|
||||
privatePrintf = func(l logLevel, f string, a ...interface{}) {
|
||||
switch l {
|
||||
case levelDebug:
|
||||
mutex.Lock()
|
||||
fmt.Fprint(os.Stderr, ProgramName()+": <DEBUG> ")
|
||||
fmt.Fprintf(os.Stderr, f, a...)
|
||||
mutex.Unlock()
|
||||
case levelInfo:
|
||||
mutex.Lock()
|
||||
fmt.Print(ProgramName() + ": <INFO> ")
|
||||
fmt.Printf(f, a...)
|
||||
mutex.Unlock()
|
||||
case levelError:
|
||||
mutex.Lock()
|
||||
fmt.Fprint(os.Stderr, ProgramName()+": <ERROR> ")
|
||||
fmt.Fprintf(os.Stderr, f, a...)
|
||||
mutex.Unlock()
|
||||
|
||||
case levelFatal:
|
||||
mutex.Lock()
|
||||
fmt.Fprint(os.Stderr, ProgramName()+": <FATAL> ")
|
||||
fmt.Fprintf(os.Stderr, f, a...)
|
||||
mutex.Unlock()
|
||||
os.Exit(1)
|
||||
default:
|
||||
fmt.Printf(f, a...)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ProgramName - return the name of the executable program
|
||||
func ProgramName() string {
|
||||
_, progName := filepath.Split(os.Args[0])
|
||||
return progName
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Minio Client (C) 2014-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"
|
||||
)
|
||||
|
||||
// Returns the minimum value of a slice of integers
|
||||
func minimum(integers []int) (minVal int) {
|
||||
minVal = math.MaxInt32
|
||||
for _, v := range integers {
|
||||
if v < minVal {
|
||||
minVal = v
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DamerauLevenshteinDistance calculates distance between two strings using an algorithm
|
||||
// described in https://en.wikipedia.org/wiki/Damerau-Levenshtein_distance
|
||||
func DamerauLevenshteinDistance(a string, b string) int {
|
||||
d := make([][]int, len(a)+1)
|
||||
for i := 1; i <= len(a)+1; i++ {
|
||||
d[i-1] = make([]int, len(b)+1)
|
||||
}
|
||||
for i := 0; i <= len(a); i++ {
|
||||
d[i][0] = i
|
||||
}
|
||||
for j := 0; j <= len(b); j++ {
|
||||
d[0][j] = j
|
||||
}
|
||||
for i := 1; i <= len(a); i++ {
|
||||
for j := 1; j <= len(b); j++ {
|
||||
cost := 0
|
||||
if a[i-1] == b[j-1] {
|
||||
cost = 0
|
||||
} else {
|
||||
cost = 1
|
||||
}
|
||||
d[i][j] = minimum([]int{
|
||||
d[i-1][j] + 1,
|
||||
d[i][j-1] + 1,
|
||||
d[i-1][j-1] + cost,
|
||||
})
|
||||
if i > 1 && j > 1 && a[i-1] == b[j-2] && a[i-2] == b[j-1] {
|
||||
d[i][j] = minimum([]int{d[i][j], d[i-2][j-2] + cost}) // transposition
|
||||
}
|
||||
}
|
||||
}
|
||||
return d[len(a)][len(b)]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# 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
|
||||
ENV ALLOW_CONTAINER_ROOT=1
|
||||
ENTRYPOINT ["/start.sh"]
|
||||
CMD ["/export"]
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/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
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# Server command line SPEC - Fri Apr 1 19:49:56 PDT 2016
|
||||
|
||||
## Single disk
|
||||
Regular single server, single disk mode.
|
||||
```
|
||||
$ minio server <disk>
|
||||
```
|
||||
|
||||
## Multi disk
|
||||
```
|
||||
$ minio controller start - Start minio controller.
|
||||
```
|
||||
Prints a secret token to be used by all parties.
|
||||
|
||||
Start all servers without disk with `MINIO_CONTROLLER` env set, start in cluster mode.
|
||||
```
|
||||
$ MINIO_CONTROLLER=<host>:<token> minio server
|
||||
```
|
||||
|
||||
## Minio Controller cli.
|
||||
|
||||
Set controller host and token.
|
||||
```
|
||||
$ export MINIO_CONTROLLER=<host>:<token>
|
||||
```
|
||||
|
||||
Create a cluster from the pool of nodes.
|
||||
```
|
||||
$ minioctl new <clustername> <ip1>:/mnt/disk1 .. <ip16>:/mnt/disk1
|
||||
```
|
||||
|
||||
Start the cluster.
|
||||
```
|
||||
$ minioctl start <clustername>
|
||||
```
|
||||
|
||||
Stop the cluster
|
||||
```
|
||||
$ minioctl stop <clustername>
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
## Access Policy
|
||||
|
||||
This package implements parsing and validating bucket access policies based on Access Policy Language specification - http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
|
||||
|
||||
### Supports following effects.
|
||||
|
||||
Allow
|
||||
Deny
|
||||
|
||||
### Supports following set of operations.
|
||||
|
||||
s3:GetObject
|
||||
s3:ListBucket
|
||||
s3:PutObject
|
||||
s3:GetBucketLocation
|
||||
s3:DeleteObject
|
||||
s3:AbortMultipartUpload
|
||||
s3:ListBucketMultipartUploads
|
||||
s3:ListMultipartUploadParts
|
||||
|
||||
### Supports following conditions.
|
||||
|
||||
StringEquals
|
||||
StringNotEquals
|
||||
|
||||
Supported applicable condition keys for each conditions.
|
||||
|
||||
s3:prefix
|
||||
s3:max-keys
|
||||
|
||||
### Nested policy support.
|
||||
|
||||
Nested policies are not allowed.
|
||||
@@ -0,0 +1,33 @@
|
||||
## Multipart
|
||||
|
||||
Each incoming part is uploaded in the following format.
|
||||
|
||||
Placeholder uploads.json to indicate a leaf.
|
||||
|
||||
```EXPORT_DIR/.minio/multipart/BUCKET/PATH/TO/OBJECT/uploads.json```
|
||||
|
||||
Incomplete file
|
||||
|
||||
```EXPORT_DIR/.minio/multipart/BUCKET/PATH/TO/OBJECT/UPLOADID/00000.incomplete```
|
||||
|
||||
Actual parts
|
||||
|
||||
```EXPORT_DIR/.minio/multipart/BUCKET/PATH/TO/OBJECT/UPLOADID/PART_NUMBER.MD5SUM_STRING```
|
||||
|
||||
## FS Format.
|
||||
|
||||
Each of these parts are concatenated back to a single contigous file.
|
||||
|
||||
```EXPORT_DIR/BUCKET/PATH/TO/OBJECT```
|
||||
|
||||
## XL Format.
|
||||
|
||||
Each of these parts are kept as is in the following format.
|
||||
|
||||
Special json file indicate the metata multipart information, essentially list of parts etc.
|
||||
|
||||
```EXPORT_DIR/BUCKET/PATH/TO/OBJECT/00000.minio.multipart```
|
||||
|
||||
All the parts that were uploaded.
|
||||
|
||||
```EXPORT_DIR/BUCKET/PATH/TO/OBJECT/PART_NUMBER.minio.multipart```
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"format": "fs",
|
||||
"version": "1"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": "1",
|
||||
"format": "fs",
|
||||
"minio": {
|
||||
"release": "DEVELOPMENT.GOGET"
|
||||
},
|
||||
"parts": [
|
||||
{
|
||||
"number": 1,
|
||||
"name": "object1",
|
||||
"size": 29,
|
||||
"eTag": "",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": "1",
|
||||
"format": "fs",
|
||||
"uploadIds": [
|
||||
{
|
||||
"uploadID": "id",
|
||||
"startTime": "time",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
"format": "xl",
|
||||
"uploadIds": [
|
||||
{
|
||||
"uploadID": "id",
|
||||
"startTime": "time",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"parts": [
|
||||
{
|
||||
"number": 1,
|
||||
"size": 5242880,
|
||||
"etag": "3565c6e741e69a007a5ac7db893a62b5",
|
||||
"name": "object1"
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"size": 5242880,
|
||||
"etag": "d416712335c280ab1e39498552937764",
|
||||
"name": "object2"
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
"size": 4338324,
|
||||
"etag": "8a98c5c54d81c6c95ed9bdcaeb941aaf",
|
||||
"name": "object3"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"md5Sum": "97586a5290d4f5a41328062d6a7da593-3",
|
||||
"content-type": "application\/octet-stream",
|
||||
"content-encoding": ""
|
||||
},
|
||||
"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": "object1",
|
||||
"algorithm": "sha512",
|
||||
"hash": "d9910e1492446389cfae6fe979db0245f96ca97ca2c7a25cab45805882004479320d866a47ea1f7be6a62625dd4de6caf7816009ef9d62779346d01a221b335c",
|
||||
},
|
||||
{
|
||||
"name": "object2",
|
||||
"algorithm": "sha512",
|
||||
"hash": "d9910e1492446389cfae6fe979db0245f96ca97ca2c7a25cab45805882004479320d866a47ea1f7be6a62625dd4de6caf7816009ef9d62779346d01a221b335c",
|
||||
},
|
||||
],
|
||||
},
|
||||
"stat": {
|
||||
"version": 0,
|
||||
"modTime": "2016-05-24T00:09:40.122390255Z",
|
||||
"size": 14824084
|
||||
},
|
||||
"format": "xl",
|
||||
"version": "1"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
### Logging.
|
||||
|
||||
- `fatalIf` - wrapper function which takes error and prints jsonic error messages.
|
||||
- `errorIf` - similar to fatalIf but doesn't exit on err != nil.
|
||||
|
||||
Supported logging targets.
|
||||
|
||||
- console
|
||||
- file
|
||||
- syslog
|
||||
|
||||
Sample logger section from `~/.minio/config.json`
|
||||
```
|
||||
"console": {
|
||||
"enable": true,
|
||||
"level": "error"
|
||||
},
|
||||
"file": {
|
||||
"enable": false,
|
||||
"fileName": "",
|
||||
"level": "error"
|
||||
},
|
||||
"syslog": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"level": "error"
|
||||
}
|
||||
```
|
||||
@@ -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,41 @@
|
||||
### file.json
|
||||
|
||||
``file.json`` is a special file captured and written by XL storage API layer
|
||||
to interpret, manage and extract erasured data from multiple disks.
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stat": {
|
||||
"size": 24256,
|
||||
"modTime": "2016-04-28T00:11:37.843Z",
|
||||
"delete": false,
|
||||
"version": 0
|
||||
},
|
||||
"erasure": {
|
||||
"data": 5,
|
||||
"parity": 5,
|
||||
"blockSize": 4194304
|
||||
],
|
||||
"minio": {
|
||||
"release": "RELEASE.2016-04-28T00-09-47Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### JSON meaning.
|
||||
|
||||
- "version" // Version of the meta json file.
|
||||
|
||||
- "stat" // Stat value of written file.
|
||||
|
||||
- "size" // Size of the file.
|
||||
- "modTime" // Modified time of the file.
|
||||
- "deleted" // Field to track if the file is deleted when disks are down.
|
||||
- "version" // File version tracked when disks are down.
|
||||
|
||||
- "erasure" // Erasure metadata for the written file.
|
||||
|
||||
- "data" // Data blocks parts of the file.
|
||||
- "parity" // Parity blocks parts of the file.
|
||||
- "blockSize" // BlockSize read/write chunk size.
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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 string, path string, partName string, data io.Reader, eInfos []erasureInfo) (newEInfos []erasureInfo, size int64, err error) {
|
||||
// Allocated blockSized buffer for reading.
|
||||
buf := make([]byte, blockSizeV1)
|
||||
hashWriters := newHashWriters(len(disks))
|
||||
|
||||
// Just pick one eInfo.
|
||||
eInfo := pickValidErasureInfo(eInfos)
|
||||
|
||||
// Read until io.EOF, erasure codes data and writes to all disks.
|
||||
for {
|
||||
var n int
|
||||
n, err = io.ReadFull(data, buf)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil && err != io.ErrUnexpectedEOF {
|
||||
return nil, 0, err
|
||||
}
|
||||
size += int64(n)
|
||||
var blocks [][]byte
|
||||
// Returns encoded blocks.
|
||||
blocks, err = encodeData(buf[:n], eInfo.DataBlocks, eInfo.ParityBlocks)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
err = appendFile(disks, volume, path, blocks, eInfo.Distribution, hashWriters)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the checksums.
|
||||
checkSums := make([]checkSumInfo, len(disks))
|
||||
for index := range disks {
|
||||
blockIndex := eInfo.Distribution[index] - 1
|
||||
checkSums[blockIndex] = checkSumInfo{
|
||||
Name: partName,
|
||||
Algorithm: "sha512",
|
||||
Hash: hex.EncodeToString(hashWriters[blockIndex].Sum(nil)),
|
||||
}
|
||||
}
|
||||
|
||||
// Erasure info update for checksum for each disks.
|
||||
newEInfos = make([]erasureInfo, len(disks))
|
||||
for index, eInfo := range eInfos {
|
||||
if eInfo.IsValid() {
|
||||
blockIndex := eInfo.Distribution[index] - 1
|
||||
newEInfos[index] = eInfo
|
||||
newEInfos[index].Checksum = append(newEInfos[index].Checksum, checkSums[blockIndex])
|
||||
}
|
||||
}
|
||||
|
||||
// Return newEInfos.
|
||||
return newEInfos, size, 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, distribution []int, hashWriters []hash.Hash) (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()
|
||||
// Pick the block from the distribution.
|
||||
blockIndex := distribution[index] - 1
|
||||
n, wErr := disk.AppendFile(volume, path, enBlocks[blockIndex])
|
||||
if wErr != nil {
|
||||
wErrs[index] = wErr
|
||||
return
|
||||
}
|
||||
if n != int64(len(enBlocks[blockIndex])) {
|
||||
wErrs[index] = errUnexpected
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate hash for each blocks.
|
||||
hashWriters[blockIndex].Write(enBlocks[blockIndex])
|
||||
|
||||
// Successfully wrote.
|
||||
wErrs[index] = nil
|
||||
}(index, disk)
|
||||
}
|
||||
|
||||
// Wait for all the appends to finish.
|
||||
wg.Wait()
|
||||
|
||||
// Return success.
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
)
|
||||
|
||||
// erasureReadFile - read an entire erasure coded file at into a byte
|
||||
// array. Erasure coded parts are often few mega bytes in size and it
|
||||
// is convenient to return them as byte slice. This function also
|
||||
// supports bit-rot detection by verifying checksum of individual
|
||||
// block's checksum.
|
||||
func erasureReadFile(disks []StorageAPI, volume string, path string, partName string, size int64, eInfos []erasureInfo) ([]byte, error) {
|
||||
// Return data buffer.
|
||||
var buffer []byte
|
||||
|
||||
// Total size left
|
||||
totalSizeLeft := size
|
||||
|
||||
// Starting offset for reading.
|
||||
startOffset := int64(0)
|
||||
|
||||
// Gather previously calculated block checksums.
|
||||
blockCheckSums := metaPartBlockChecksums(disks, eInfos, partName)
|
||||
|
||||
// Pick one erasure info.
|
||||
eInfo := pickValidErasureInfo(eInfos)
|
||||
|
||||
// Write until each parts are read and exhausted.
|
||||
for totalSizeLeft > 0 {
|
||||
// Calculate the proper block size.
|
||||
var curBlockSize int64
|
||||
if eInfo.BlockSize < totalSizeLeft {
|
||||
curBlockSize = eInfo.BlockSize
|
||||
} else {
|
||||
curBlockSize = totalSizeLeft
|
||||
}
|
||||
|
||||
// Calculate the current encoded block size.
|
||||
curEncBlockSize := getEncodedBlockLen(curBlockSize, eInfo.DataBlocks)
|
||||
offsetEncOffset := getEncodedBlockLen(startOffset, eInfo.DataBlocks)
|
||||
|
||||
// Allocate encoded blocks up to storage disks.
|
||||
enBlocks := make([][]byte, len(disks))
|
||||
|
||||
// Counter to keep success data blocks.
|
||||
var successDataBlocksCount = 0
|
||||
var noReconstruct bool // Set for no reconstruction.
|
||||
|
||||
// Read from all the disks.
|
||||
for index, disk := range disks {
|
||||
blockIndex := eInfo.Distribution[index] - 1
|
||||
if !isValidBlock(disks, volume, path, toDiskIndex(blockIndex, eInfo.Distribution), blockCheckSums) {
|
||||
continue
|
||||
}
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
// Initialize shard slice and fill the data from each parts.
|
||||
enBlocks[blockIndex] = make([]byte, curEncBlockSize)
|
||||
// Read the necessary blocks.
|
||||
_, err := disk.ReadFile(volume, path, offsetEncOffset, enBlocks[blockIndex])
|
||||
if err != nil {
|
||||
enBlocks[blockIndex] = nil
|
||||
}
|
||||
// Verify if we have successfully read all the data blocks.
|
||||
if blockIndex < eInfo.DataBlocks && enBlocks[blockIndex] != nil {
|
||||
successDataBlocksCount++
|
||||
// Set when we have all the data blocks and no
|
||||
// reconstruction is needed, so that we can avoid
|
||||
// erasure reconstruction.
|
||||
noReconstruct = successDataBlocksCount == eInfo.DataBlocks
|
||||
if noReconstruct {
|
||||
// Break out we have read all the data blocks.
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check blocks if they are all zero in length, we have corruption return error.
|
||||
if checkBlockSize(enBlocks) == 0 {
|
||||
return nil, errXLDataCorrupt
|
||||
}
|
||||
|
||||
// Verify if reconstruction is needed, proceed with reconstruction.
|
||||
if !noReconstruct {
|
||||
err := decodeData(enBlocks, eInfo.DataBlocks, eInfo.ParityBlocks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Get data blocks from encoded blocks.
|
||||
dataBlocks, err := getDataBlocks(enBlocks, eInfo.DataBlocks, int(curBlockSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Copy data blocks.
|
||||
buffer = append(buffer, dataBlocks...)
|
||||
|
||||
// Negate the 'n' size written to client.
|
||||
totalSizeLeft -= int64(len(dataBlocks))
|
||||
|
||||
// Increase the offset to move forward.
|
||||
startOffset += int64(len(dataBlocks))
|
||||
|
||||
// Relenquish memory.
|
||||
dataBlocks = nil
|
||||
}
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
// PartObjectChecksum - returns the checksum for the part name from the checksum slice.
|
||||
func (e erasureInfo) PartObjectChecksum(partName string) checkSumInfo {
|
||||
for _, checksum := range e.Checksum {
|
||||
if checksum.Name == partName {
|
||||
return checksum
|
||||
}
|
||||
}
|
||||
return checkSumInfo{}
|
||||
}
|
||||
|
||||
// xlMetaPartBlockChecksums - get block checksums for a given part.
|
||||
func metaPartBlockChecksums(disks []StorageAPI, eInfos []erasureInfo, partName string) (blockCheckSums []checkSumInfo) {
|
||||
for index := range disks {
|
||||
if eInfos[index].IsValid() {
|
||||
// Save the read checksums for a given part.
|
||||
blockCheckSums = append(blockCheckSums, eInfos[index].PartObjectChecksum(partName))
|
||||
} else {
|
||||
blockCheckSums = append(blockCheckSums, checkSumInfo{})
|
||||
}
|
||||
}
|
||||
return blockCheckSums
|
||||
}
|
||||
|
||||
// Takes block index and block distribution to get the disk index.
|
||||
func toDiskIndex(blockIdx int, distribution []int) (diskIndex int) {
|
||||
diskIndex = -1
|
||||
// Find out the right disk index for the input block index.
|
||||
for index, blockIndex := range distribution {
|
||||
if blockIndex == blockIdx {
|
||||
diskIndex = index
|
||||
}
|
||||
}
|
||||
return diskIndex
|
||||
}
|
||||
|
||||
// isValidBlock - calculates the checksum hash for the block and
|
||||
// validates if its correct returns true for valid cases, false otherwise.
|
||||
func isValidBlock(disks []StorageAPI, volume, path string, diskIndex int, blockCheckSums []checkSumInfo) bool {
|
||||
// Unknown block index requested, treat it as error.
|
||||
if diskIndex == -1 {
|
||||
return false
|
||||
}
|
||||
// Disk is not present, treat entire block to be non existent.
|
||||
if disks[diskIndex] == nil {
|
||||
return false
|
||||
}
|
||||
// Read everything for a given block and calculate hash.
|
||||
hashWriter := newHash(blockCheckSums[diskIndex].Algorithm)
|
||||
hashBytes, err := hashSum(disks[diskIndex], volume, path, hashWriter)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return hex.EncodeToString(hashBytes) == blockCheckSums[diskIndex].Hash
|
||||
}
|
||||
|
||||
// decodeData - decode encoded blocks.
|
||||
func decodeData(enBlocks [][]byte, dataBlocks, parityBlocks int) error {
|
||||
rs, err := reedsolomon.New(dataBlocks, parityBlocks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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 (
|
||||
"crypto/sha512"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
)
|
||||
|
||||
// newHashWriters - inititialize a slice of hashes for the disk count.
|
||||
func newHashWriters(diskCount int) []hash.Hash {
|
||||
hashWriters := make([]hash.Hash, diskCount)
|
||||
for index := range hashWriters {
|
||||
hashWriters[index] = newHash("sha512")
|
||||
}
|
||||
return hashWriters
|
||||
}
|
||||
|
||||
// newHash - gives you a newly allocated hash depending on the input algorithm.
|
||||
func newHash(algo string) hash.Hash {
|
||||
switch algo {
|
||||
case "sha512":
|
||||
return sha512.New()
|
||||
// Add new hashes here.
|
||||
default:
|
||||
return sha512.New()
|
||||
}
|
||||
}
|
||||
|
||||
func hashSum(disk StorageAPI, volume, path string, writer hash.Hash) ([]byte, error) {
|
||||
startOffset := int64(0)
|
||||
// Read until io.EOF.
|
||||
for {
|
||||
buf := make([]byte, blockSizeV1)
|
||||
n, err := disk.ReadFile(volume, path, startOffset, buf)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
writer.Write(buf[:n])
|
||||
startOffset += n
|
||||
}
|
||||
return writer.Sum(nil), nil
|
||||
}
|
||||
|
||||
// getDataBlocks - fetches the data block only part of the input encoded blocks.
|
||||
func getDataBlocks(enBlocks [][]byte, dataBlocks int, curBlockSize int) (data []byte, err error) {
|
||||
if len(enBlocks) < dataBlocks {
|
||||
return nil, reedsolomon.ErrTooFewShards
|
||||
}
|
||||
size := 0
|
||||
blocks := enBlocks[:dataBlocks]
|
||||
for _, block := range blocks {
|
||||
size += len(block)
|
||||
}
|
||||
if size < curBlockSize {
|
||||
return nil, reedsolomon.ErrShortData
|
||||
}
|
||||
|
||||
write := curBlockSize
|
||||
for _, block := range blocks {
|
||||
if write < len(block) {
|
||||
data = append(data, block[:write]...)
|
||||
return data, nil
|
||||
}
|
||||
data = append(data, block...)
|
||||
write -= len(block)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// checkBlockSize return the size of a single block.
|
||||
// The first non-zero size is returned,
|
||||
// or 0 if all blocks are size 0.
|
||||
func checkBlockSize(blocks [][]byte) int {
|
||||
for _, block := range blocks {
|
||||
if len(block) != 0 {
|
||||
return len(block)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// calculate the blockSize based on input length and total number of
|
||||
// data blocks.
|
||||
func getEncodedBlockLen(inputLen int64, dataBlocks int) (curEncBlockSize int64) {
|
||||
curEncBlockSize = (inputLen + int64(dataBlocks) - 1) / int64(dataBlocks)
|
||||
return curEncBlockSize
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// waitCloser implements a Closer that blocks until released, this
|
||||
// prevents the writer closing one end of a pipe while making sure
|
||||
// that all data is written and committed to disk on the end.
|
||||
// Additionally this also implements Write().
|
||||
type waitCloser struct {
|
||||
wg *sync.WaitGroup // Waitgroup for atomicity.
|
||||
writer io.WriteCloser // Embedded writer.
|
||||
}
|
||||
|
||||
// Write to the underlying writer.
|
||||
func (b *waitCloser) Write(data []byte) (int, error) {
|
||||
return b.writer.Write(data)
|
||||
}
|
||||
|
||||
// Close blocks until another goroutine calls Release(error). Returns
|
||||
// error code if either writer fails or Release is called with an error.
|
||||
func (b *waitCloser) Close() error {
|
||||
err := b.writer.Close()
|
||||
b.wg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
// CloseWithError closes the writer; subsequent read to the read
|
||||
// half of the pipe will return the error err.
|
||||
func (b *waitCloser) CloseWithError(err error) error {
|
||||
w, ok := b.writer.(*io.PipeWriter)
|
||||
if ok {
|
||||
return w.CloseWithError(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// release the Close, causing it to unblock. Only call this
|
||||
// once. Calling it multiple times results in a panic.
|
||||
func (b *waitCloser) release() {
|
||||
b.wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
// newWaitCloser creates a new write closer that must be
|
||||
// released by the read consumer.
|
||||
func newWaitCloser(writer io.WriteCloser) *waitCloser {
|
||||
// Wait group for the go-routine.
|
||||
wg := &sync.WaitGroup{}
|
||||
// Add to the wait group to wait for.
|
||||
wg.Add(1)
|
||||
return &waitCloser{
|
||||
wg: wg,
|
||||
writer: writer,
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -1,57 +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 "github.com/minio/cli"
|
||||
|
||||
// Collection of minio flags currently supported
|
||||
var flags = []cli.Flag{}
|
||||
|
||||
var (
|
||||
configFolderFlag = cli.StringFlag{
|
||||
Name: "config-folder, C",
|
||||
Value: mustGetConfigPath(),
|
||||
Usage: "Path to configuration folder.",
|
||||
}
|
||||
|
||||
addressFlag = cli.StringFlag{
|
||||
Name: "address",
|
||||
Value: ":9000",
|
||||
Usage: "ADDRESS:PORT for cloud storage access.",
|
||||
}
|
||||
|
||||
accessLogFlag = cli.BoolFlag{
|
||||
Name: "enable-accesslog",
|
||||
Hide: true,
|
||||
Usage: "Enable access logs for all incoming HTTP request.",
|
||||
}
|
||||
|
||||
certFlag = cli.StringFlag{
|
||||
Name: "cert",
|
||||
Usage: "Provide your domain certificate.",
|
||||
}
|
||||
|
||||
keyFlag = cli.StringFlag{
|
||||
Name: "key",
|
||||
Usage: "Provide your domain private key.",
|
||||
}
|
||||
)
|
||||
|
||||
// registerFlag registers a cli flag
|
||||
func registerFlag(flag cli.Flag) {
|
||||
flags = append(flags, flag)
|
||||
}
|
||||
@@ -0,0 +1,588 @@
|
||||
/*
|
||||
* 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"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/skyrings/skyring-common/tools/uuid"
|
||||
)
|
||||
|
||||
// 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
|
||||
*/
|
||||
|
||||
var errSomeDiskUnformatted = errors.New("some disks are found to be unformatted")
|
||||
var errSomeDiskOffline = errors.New("some disks are offline")
|
||||
|
||||
// Returns error slice into understandable errors.
|
||||
func reduceFormatErrs(errs []error, diskCount int) error {
|
||||
var errUnformattedDiskCount = 0
|
||||
var errDiskNotFoundCount = 0
|
||||
for _, err := range errs {
|
||||
if err == errUnformattedDisk {
|
||||
errUnformattedDiskCount++
|
||||
} else if err == errDiskNotFound {
|
||||
errDiskNotFoundCount++
|
||||
}
|
||||
}
|
||||
// Returns errUnformattedDisk if all disks report unFormattedDisk.
|
||||
if errUnformattedDiskCount == diskCount {
|
||||
return errUnformattedDisk
|
||||
} else if errUnformattedDiskCount < diskCount && errDiskNotFoundCount == 0 {
|
||||
// Only some disks return unFormattedDisk and all disks are online.
|
||||
return errSomeDiskUnformatted
|
||||
} else if errUnformattedDiskCount < diskCount && errDiskNotFoundCount > 0 {
|
||||
// Only some disks return unFormattedDisk and some disks are
|
||||
// offline as well.
|
||||
return errSomeDiskOffline
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
// One of the disk has corrupt format, return error.
|
||||
if errCorruptFormatCount > 0 {
|
||||
return errCorruptedFormat
|
||||
}
|
||||
|
||||
// Validates if format and JBOD are consistent across all disks.
|
||||
if err = checkFormatXL(formatConfigs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Success..
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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))
|
||||
var disksFound = make(map[string]bool)
|
||||
// Collect currently available disk uuids.
|
||||
for index, formatConfig := range formatConfigs {
|
||||
if formatConfig == nil {
|
||||
continue
|
||||
}
|
||||
disks[index] = formatConfig.XL.Disk
|
||||
}
|
||||
// Validate collected uuids and verify JBOD.
|
||||
for index, uuid := range disks {
|
||||
if uuid == "" {
|
||||
continue
|
||||
}
|
||||
var formatConfig = formatConfigs[index]
|
||||
for _, savedUUID := range formatConfig.XL.JBOD {
|
||||
if savedUUID == uuid {
|
||||
disksFound[uuid] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if all disks are found.
|
||||
for _, value := range disksFound {
|
||||
if !value {
|
||||
return errors.New("Some disks not found in JBOD.")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkJBODConsistency - validate xl jbod order if they are consistent.
|
||||
func checkJBODConsistency(formatConfigs []*formatConfigV1) error {
|
||||
var firstJBOD []string
|
||||
// Extract first valid JBOD.
|
||||
for _, format := range formatConfigs {
|
||||
if format == nil {
|
||||
continue
|
||||
}
|
||||
firstJBOD = format.XL.JBOD
|
||||
break
|
||||
}
|
||||
jbodStr := strings.Join(firstJBOD, ".")
|
||||
for _, format := range formatConfigs {
|
||||
if format == nil {
|
||||
continue
|
||||
}
|
||||
savedJBODStr := strings.Join(format.XL.JBOD, ".")
|
||||
if jbodStr != savedJBODStr {
|
||||
return errors.New("Inconsistent JBOD found.")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findIndex(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 := findIndex(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 - load format from disk.
|
||||
func loadFormat(disk StorageAPI) (format *formatConfigV1, err error) {
|
||||
var buffer []byte
|
||||
buffer, err = readAll(disk, 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
|
||||
}
|
||||
format = &formatConfigV1{}
|
||||
err = json.Unmarshal(buffer, format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return format, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func healFormatXL(bootstrapDisks []StorageAPI) error {
|
||||
needHeal := make([]bool, len(bootstrapDisks)) // Slice indicating which drives needs healing.
|
||||
|
||||
formatConfigs := make([]*formatConfigV1, len(bootstrapDisks))
|
||||
var referenceConfig *formatConfigV1
|
||||
successCount := 0 // Tracks if we have successfully loaded all `format.json` from all disks.
|
||||
formatNotFoundCount := 0 // Tracks if we `format.json` is not found on all disks.
|
||||
// Loads `format.json` from all disks.
|
||||
for index, disk := range bootstrapDisks {
|
||||
formatXL, err := loadFormat(disk)
|
||||
if err != nil {
|
||||
if err == errUnformattedDisk {
|
||||
// format.json is missing, should be healed.
|
||||
needHeal[index] = true
|
||||
formatNotFoundCount++
|
||||
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
|
||||
successCount++
|
||||
}
|
||||
// All `format.json` has been read successfully, previously completed.
|
||||
if successCount == len(bootstrapDisks) {
|
||||
// Return success.
|
||||
return nil
|
||||
}
|
||||
// All disks are fresh, format.json will be written by initFormatXL()
|
||||
if formatNotFoundCount == len(bootstrapDisks) {
|
||||
return initFormatXL(bootstrapDisks)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
uuidUsage := make([]struct {
|
||||
uuid string // Disk uuid
|
||||
inUse bool // indicates if the uuid is used by
|
||||
// any disk
|
||||
}, len(bootstrapDisks))
|
||||
|
||||
// Returns any unused drive UUID.
|
||||
getUnusedUUID := func() string {
|
||||
for index := range uuidUsage {
|
||||
if !uuidUsage[index].inUse {
|
||||
uuidUsage[index].inUse = true
|
||||
return uuidUsage[index].uuid
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// From reference config update UUID's not be in use.
|
||||
for index, diskUUID := range referenceConfig.XL.JBOD {
|
||||
uuidUsage[index].uuid = diskUUID
|
||||
uuidUsage[index].inUse = false
|
||||
}
|
||||
|
||||
// For all config formats validate if they are in use and
|
||||
// update the uuidUsage values.
|
||||
for _, config := range formatConfigs {
|
||||
if config == nil {
|
||||
continue
|
||||
}
|
||||
for index := range uuidUsage {
|
||||
if config.XL.Disk == uuidUsage[index].uuid {
|
||||
uuidUsage[index].inUse = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// This section heals the format.json and updates the fresh disks
|
||||
// by apply a new UUID for all the fresh disks.
|
||||
for index, heal := range needHeal {
|
||||
if !heal {
|
||||
continue
|
||||
}
|
||||
config := &formatConfigV1{}
|
||||
*config = *referenceConfig
|
||||
config.XL.Disk = getUnusedUUID()
|
||||
if config.XL.Disk == "" {
|
||||
// getUnusedUUID() should have
|
||||
// returned an unused uuid, it
|
||||
// is an unexpected error.
|
||||
return errUnexpected
|
||||
}
|
||||
|
||||
formatBytes, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Fresh disk without format.json
|
||||
_, _ = bootstrapDisks[index].AppendFile(minioMetaBucket, formatConfigFile, formatBytes)
|
||||
// Ignore any error from AppendFile() as
|
||||
// quorum might still be there to be operational.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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) {
|
||||
return nil, errUnformattedDisk
|
||||
} else if diskNotFoundCount == len(bootstrapDisks) {
|
||||
return nil, errDiskNotFound
|
||||
} else if diskNotFoundCount > len(bootstrapDisks)-(len(bootstrapDisks)/2+1) {
|
||||
return nil, errXLReadQuorum
|
||||
} else if unformattedDisksFoundCnt > 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)
|
||||
}
|
||||
|
||||
// initFormatXL - save XL format configuration on all disks.
|
||||
func initFormatXL(storageDisks []StorageAPI) (err error) {
|
||||
var (
|
||||
jbod = make([]string, len(storageDisks))
|
||||
formats = make([]*formatConfigV1, len(storageDisks))
|
||||
saveFormatErrCnt = 0
|
||||
)
|
||||
for index, disk := range storageDisks {
|
||||
if err = disk.MakeVol(minioMetaBucket); err != nil {
|
||||
if err != errVolumeExists {
|
||||
saveFormatErrCnt++
|
||||
// Check for write quorum.
|
||||
if saveFormatErrCnt <= len(storageDisks)-(len(storageDisks)/2+3) {
|
||||
continue
|
||||
}
|
||||
return errXLWriteQuorum
|
||||
}
|
||||
}
|
||||
var u *uuid.UUID
|
||||
u, err = uuid.New()
|
||||
if err != nil {
|
||||
saveFormatErrCnt++
|
||||
// Check for write quorum.
|
||||
if saveFormatErrCnt <= len(storageDisks)-(len(storageDisks)/2+3) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
formats[index] = &formatConfigV1{
|
||||
Version: "1",
|
||||
Format: "xl",
|
||||
XL: &xlFormat{
|
||||
Version: "1",
|
||||
Disk: u.String(),
|
||||
},
|
||||
}
|
||||
jbod[index] = formats[index].XL.Disk
|
||||
}
|
||||
for index, disk := range storageDisks {
|
||||
formats[index].XL.JBOD = jbod
|
||||
formatBytes, err := json.Marshal(formats[index])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := disk.AppendFile(minioMetaBucket, formatConfigFile, formatBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != int64(len(formatBytes)) {
|
||||
return errUnexpected
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const (
|
||||
fsMetaJSONFile = "fs.json"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
Parts []objectPartInfo `json:"parts,omitempty"`
|
||||
}
|
||||
|
||||
// ObjectPartIndex - returns the index of matching object part number.
|
||||
func (m fsMetaV1) ObjectPartIndex(partNumber int) (partIndex int) {
|
||||
for i, part := range m.Parts {
|
||||
if partNumber == part.Number {
|
||||
partIndex = i
|
||||
return partIndex
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// AddObjectPart - add a new object part in order.
|
||||
func (m *fsMetaV1) AddObjectPart(partNumber int, partName string, partETag string, partSize int64) {
|
||||
partInfo := objectPartInfo{
|
||||
Number: partNumber,
|
||||
Name: partName,
|
||||
ETag: partETag,
|
||||
Size: partSize,
|
||||
}
|
||||
|
||||
// Update part info if it already exists.
|
||||
for i, part := range m.Parts {
|
||||
if partNumber == part.Number {
|
||||
m.Parts[i] = partInfo
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed to include new part info.
|
||||
m.Parts = append(m.Parts, partInfo)
|
||||
|
||||
// Parts in fsMeta should be in sorted order by part number.
|
||||
sort.Sort(byObjectPartNumber(m.Parts))
|
||||
}
|
||||
|
||||
// readFSMetadata - returns the object metadata `fs.json` content.
|
||||
func (fs fsObjects) readFSMetadata(bucket, object string) (fsMeta fsMetaV1, err error) {
|
||||
var buffer []byte
|
||||
buffer, err = readAll(fs.storage, bucket, path.Join(object, fsMetaJSONFile))
|
||||
if err != nil {
|
||||
return fsMetaV1{}, err
|
||||
}
|
||||
err = json.Unmarshal(buffer, &fsMeta)
|
||||
if err != nil {
|
||||
return fsMetaV1{}, err
|
||||
}
|
||||
return fsMeta, nil
|
||||
}
|
||||
|
||||
// newFSMetaV1 - initializes new fsMetaV1.
|
||||
func newFSMetaV1() (fsMeta fsMetaV1) {
|
||||
fsMeta = fsMetaV1{}
|
||||
fsMeta.Version = "1"
|
||||
fsMeta.Format = "fs"
|
||||
fsMeta.Minio.Release = minioReleaseTag
|
||||
return fsMeta
|
||||
}
|
||||
|
||||
// writeFSMetadata - writes `fs.json` metadata.
|
||||
func (fs fsObjects) writeFSMetadata(bucket, prefix string, fsMeta fsMetaV1) error {
|
||||
metadataBytes, err := json.Marshal(fsMeta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := fs.storage.AppendFile(bucket, path.Join(prefix, fsMetaJSONFile), metadataBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != int64(len(metadataBytes)) {
|
||||
return errUnexpected
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 (
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Returns if the prefix is a multipart upload.
|
||||
func (fs fsObjects) isMultipartUpload(bucket, prefix string) bool {
|
||||
_, err := fs.storage.StatFile(bucket, pathJoin(prefix, uploadsJSONFile))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// listUploadsInfo - list all uploads info.
|
||||
func (fs fsObjects) listUploadsInfo(prefixPath string) (uploads []uploadInfo, err error) {
|
||||
splitPrefixes := strings.SplitN(prefixPath, "/", 3)
|
||||
uploadIDs, err := readUploadsJSON(splitPrefixes[1], splitPrefixes[2], fs.storage)
|
||||
if err != nil {
|
||||
if err == errFileNotFound {
|
||||
return []uploadInfo{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
uploads = uploadIDs.Uploads
|
||||
return uploads, nil
|
||||
}
|
||||
|
||||
// Checks whether bucket exists.
|
||||
func (fs fsObjects) isBucketExist(bucket string) bool {
|
||||
// Check whether bucket exists.
|
||||
_, err := fs.storage.StatVol(bucket)
|
||||
if err != nil {
|
||||
if err == errVolumeNotFound {
|
||||
return false
|
||||
}
|
||||
errorIf(err, "Stat failed on bucket "+bucket+".")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isUploadIDExists - verify if a given uploadID exists and is valid.
|
||||
func (fs fsObjects) isUploadIDExists(bucket, object, uploadID string) bool {
|
||||
uploadIDPath := path.Join(mpartMetaPrefix, bucket, object, uploadID)
|
||||
_, err := fs.storage.StatFile(minioMetaBucket, path.Join(uploadIDPath, fsMetaJSONFile))
|
||||
if err != nil {
|
||||
if err == errFileNotFound {
|
||||
return false
|
||||
}
|
||||
errorIf(err, "Unable to access upload id"+uploadIDPath)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
/*
|
||||
* 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 (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/skyrings/skyring-common/tools/uuid"
|
||||
)
|
||||
|
||||
// listMultipartUploads - lists all multipart uploads.
|
||||
func (fs fsObjects) listMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (ListMultipartsInfo, error) {
|
||||
result := ListMultipartsInfo{}
|
||||
recursive := true
|
||||
if delimiter == slashSeparator {
|
||||
recursive = false
|
||||
}
|
||||
|
||||
result.IsTruncated = true
|
||||
result.MaxUploads = maxUploads
|
||||
result.KeyMarker = keyMarker
|
||||
result.Prefix = prefix
|
||||
result.Delimiter = delimiter
|
||||
|
||||
// Not using path.Join() as it strips off the trailing '/'.
|
||||
multipartPrefixPath := pathJoin(mpartMetaPrefix, bucket, prefix)
|
||||
if prefix == "" {
|
||||
// Should have a trailing "/" if prefix is ""
|
||||
// For ex. multipartPrefixPath should be "multipart/bucket/" if prefix is ""
|
||||
multipartPrefixPath += slashSeparator
|
||||
}
|
||||
multipartMarkerPath := ""
|
||||
if keyMarker != "" {
|
||||
multipartMarkerPath = pathJoin(mpartMetaPrefix, bucket, keyMarker)
|
||||
}
|
||||
var uploads []uploadMetadata
|
||||
var err error
|
||||
var eof bool
|
||||
if uploadIDMarker != "" {
|
||||
nsMutex.RLock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, keyMarker))
|
||||
uploads, _, err = listMultipartUploadIDs(bucket, keyMarker, uploadIDMarker, maxUploads, fs.storage)
|
||||
nsMutex.RUnlock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, keyMarker))
|
||||
if err != nil {
|
||||
return ListMultipartsInfo{}, err
|
||||
}
|
||||
maxUploads = maxUploads - len(uploads)
|
||||
}
|
||||
if maxUploads > 0 {
|
||||
walker := fs.lookupTreeWalk(listParams{minioMetaBucket, recursive, multipartMarkerPath, multipartPrefixPath})
|
||||
if walker == nil {
|
||||
walker = fs.startTreeWalk(minioMetaBucket, multipartPrefixPath, multipartMarkerPath, recursive, func(bucket, object string) bool {
|
||||
return fs.isMultipartUpload(bucket, object)
|
||||
})
|
||||
}
|
||||
for maxUploads > 0 {
|
||||
walkResult, ok := <-walker.ch
|
||||
if !ok {
|
||||
// Closed channel.
|
||||
eof = true
|
||||
break
|
||||
}
|
||||
// For any walk error return right away.
|
||||
if walkResult.err != nil {
|
||||
// File not found or Disk not found is a valid case.
|
||||
if walkResult.err == errFileNotFound || walkResult.err == errDiskNotFound {
|
||||
eof = true
|
||||
break
|
||||
}
|
||||
return ListMultipartsInfo{}, err
|
||||
}
|
||||
entry := strings.TrimPrefix(walkResult.entry, retainSlash(pathJoin(mpartMetaPrefix, bucket)))
|
||||
if strings.HasSuffix(walkResult.entry, slashSeparator) {
|
||||
uploads = append(uploads, uploadMetadata{
|
||||
Object: entry,
|
||||
})
|
||||
maxUploads--
|
||||
if maxUploads == 0 {
|
||||
if walkResult.end {
|
||||
eof = true
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
var tmpUploads []uploadMetadata
|
||||
var end bool
|
||||
uploadIDMarker = ""
|
||||
nsMutex.RLock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, entry))
|
||||
tmpUploads, end, err = listMultipartUploadIDs(bucket, entry, uploadIDMarker, maxUploads, fs.storage)
|
||||
nsMutex.RUnlock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, entry))
|
||||
if err != nil {
|
||||
return ListMultipartsInfo{}, err
|
||||
}
|
||||
uploads = append(uploads, tmpUploads...)
|
||||
maxUploads -= len(tmpUploads)
|
||||
if walkResult.end && end {
|
||||
eof = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Loop through all the received uploads fill in the multiparts result.
|
||||
for _, upload := range uploads {
|
||||
var objectName string
|
||||
var uploadID string
|
||||
if strings.HasSuffix(upload.Object, slashSeparator) {
|
||||
// All directory entries are common prefixes.
|
||||
uploadID = "" // Upload ids are empty for CommonPrefixes.
|
||||
objectName = upload.Object
|
||||
result.CommonPrefixes = append(result.CommonPrefixes, objectName)
|
||||
} else {
|
||||
uploadID = upload.UploadID
|
||||
objectName = upload.Object
|
||||
result.Uploads = append(result.Uploads, upload)
|
||||
}
|
||||
result.NextKeyMarker = objectName
|
||||
result.NextUploadIDMarker = uploadID
|
||||
}
|
||||
result.IsTruncated = !eof
|
||||
if !result.IsTruncated {
|
||||
result.NextKeyMarker = ""
|
||||
result.NextUploadIDMarker = ""
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListMultipartUploads - lists all the pending multipart uploads on a
|
||||
// bucket. Additionally takes 'prefix, keyMarker, uploadIDmarker and a
|
||||
// delimiter' which allows us to list uploads match a particular
|
||||
// prefix or lexically starting from 'keyMarker' or delimiting the
|
||||
// output to get a directory like listing.
|
||||
//
|
||||
// Implements S3 compatible ListMultipartUploads API. The resulting
|
||||
// ListMultipartsInfo structure is unmarshalled directly into XML and
|
||||
// replied back to the client.
|
||||
func (fs fsObjects) ListMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (ListMultipartsInfo, error) {
|
||||
// Validate input arguments.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return ListMultipartsInfo{}, BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
if !fs.isBucketExist(bucket) {
|
||||
return ListMultipartsInfo{}, BucketNotFound{Bucket: bucket}
|
||||
}
|
||||
if !IsValidObjectPrefix(prefix) {
|
||||
return ListMultipartsInfo{}, ObjectNameInvalid{Bucket: bucket, Object: prefix}
|
||||
}
|
||||
// Verify if delimiter is anything other than '/', which we do not support.
|
||||
if delimiter != "" && delimiter != slashSeparator {
|
||||
return ListMultipartsInfo{}, UnsupportedDelimiter{
|
||||
Delimiter: delimiter,
|
||||
}
|
||||
}
|
||||
// Verify if marker has prefix.
|
||||
if keyMarker != "" && !strings.HasPrefix(keyMarker, prefix) {
|
||||
return ListMultipartsInfo{}, InvalidMarkerPrefixCombination{
|
||||
Marker: keyMarker,
|
||||
Prefix: prefix,
|
||||
}
|
||||
}
|
||||
if uploadIDMarker != "" {
|
||||
if strings.HasSuffix(keyMarker, slashSeparator) {
|
||||
return ListMultipartsInfo{}, InvalidUploadIDKeyCombination{
|
||||
UploadIDMarker: uploadIDMarker,
|
||||
KeyMarker: keyMarker,
|
||||
}
|
||||
}
|
||||
id, err := uuid.Parse(uploadIDMarker)
|
||||
if err != nil {
|
||||
return ListMultipartsInfo{}, err
|
||||
}
|
||||
if id.IsZero() {
|
||||
return ListMultipartsInfo{}, MalformedUploadID{
|
||||
UploadID: uploadIDMarker,
|
||||
}
|
||||
}
|
||||
}
|
||||
return fs.listMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||
}
|
||||
|
||||
// newMultipartUpload - wrapper for initializing a new multipart
|
||||
// request, returns back a unique upload id.
|
||||
//
|
||||
// Internally this function creates 'uploads.json' associated for the
|
||||
// incoming object at '.minio/multipart/bucket/object/uploads.json' on
|
||||
// all the disks. `uploads.json` carries metadata regarding on going
|
||||
// multipart operation on the object.
|
||||
func (fs fsObjects) newMultipartUpload(bucket string, object string, meta map[string]string) (uploadID string, err error) {
|
||||
// No metadata is set, allocate a new one.
|
||||
if meta == nil {
|
||||
meta = make(map[string]string)
|
||||
}
|
||||
|
||||
// Initialize `fs.json` values.
|
||||
fsMeta := newFSMetaV1()
|
||||
|
||||
// This lock needs to be held for any changes to the directory contents of ".minio/multipart/object/"
|
||||
nsMutex.Lock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, object))
|
||||
defer nsMutex.Unlock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, object))
|
||||
|
||||
uploadID = getUUID()
|
||||
initiated := time.Now().UTC()
|
||||
// Create 'uploads.json'
|
||||
if err = writeUploadJSON(bucket, object, uploadID, initiated, fs.storage); err != nil {
|
||||
return "", err
|
||||
}
|
||||
uploadIDPath := path.Join(mpartMetaPrefix, bucket, object, uploadID)
|
||||
tempUploadIDPath := path.Join(tmpMetaPrefix, uploadID)
|
||||
if err = fs.writeFSMetadata(minioMetaBucket, tempUploadIDPath, fsMeta); err != nil {
|
||||
return "", toObjectErr(err, minioMetaBucket, tempUploadIDPath)
|
||||
}
|
||||
err = fs.storage.RenameFile(minioMetaBucket, path.Join(tempUploadIDPath, fsMetaJSONFile), minioMetaBucket, path.Join(uploadIDPath, fsMetaJSONFile))
|
||||
if err != nil {
|
||||
if dErr := fs.storage.DeleteFile(minioMetaBucket, path.Join(tempUploadIDPath, fsMetaJSONFile)); dErr != nil {
|
||||
return "", toObjectErr(dErr, minioMetaBucket, tempUploadIDPath)
|
||||
}
|
||||
return "", toObjectErr(err, minioMetaBucket, uploadIDPath)
|
||||
}
|
||||
// Return success.
|
||||
return uploadID, nil
|
||||
}
|
||||
|
||||
// NewMultipartUpload - initialize a new multipart upload, returns a
|
||||
// unique id. The unique id returned here is of UUID form, for each
|
||||
// subsequent request each UUID is unique.
|
||||
//
|
||||
// Implements S3 compatible initiate multipart API.
|
||||
func (fs fsObjects) NewMultipartUpload(bucket, object string, meta map[string]string) (string, error) {
|
||||
meta = make(map[string]string) // Reset the meta value, we are not going to save headers for fs.
|
||||
// Verify if bucket name is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return "", BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
// Verify whether the bucket exists.
|
||||
if !fs.isBucketExist(bucket) {
|
||||
return "", BucketNotFound{Bucket: bucket}
|
||||
}
|
||||
// Verify if object name is valid.
|
||||
if !IsValidObjectName(object) {
|
||||
return "", ObjectNameInvalid{Bucket: bucket, Object: object}
|
||||
}
|
||||
return fs.newMultipartUpload(bucket, object, meta)
|
||||
}
|
||||
|
||||
// PutObjectPart - reads incoming data until EOF for the part file on
|
||||
// an ongoing multipart transaction. Internally incoming data is
|
||||
// written to '.minio/tmp' location and safely renamed to
|
||||
// '.minio/multipart' for reach parts.
|
||||
func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, size int64, data io.Reader, md5Hex string) (string, error) {
|
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return "", BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
// Verify whether the bucket exists.
|
||||
if !fs.isBucketExist(bucket) {
|
||||
return "", BucketNotFound{Bucket: bucket}
|
||||
}
|
||||
if !IsValidObjectName(object) {
|
||||
return "", ObjectNameInvalid{Bucket: bucket, Object: object}
|
||||
}
|
||||
|
||||
uploadIDPath := path.Join(mpartMetaPrefix, bucket, object, uploadID)
|
||||
|
||||
nsMutex.RLock(minioMetaBucket, uploadIDPath)
|
||||
// Just check if the uploadID exists to avoid copy if it doesn't.
|
||||
uploadIDExists := fs.isUploadIDExists(bucket, object, uploadID)
|
||||
nsMutex.RUnlock(minioMetaBucket, uploadIDPath)
|
||||
if !uploadIDExists {
|
||||
return "", InvalidUploadID{UploadID: uploadID}
|
||||
}
|
||||
|
||||
// Hold write lock on the part so that there is no parallel upload on the part.
|
||||
nsMutex.Lock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, object, uploadID, strconv.Itoa(partID)))
|
||||
defer nsMutex.Unlock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, object, uploadID, strconv.Itoa(partID)))
|
||||
|
||||
partSuffix := fmt.Sprintf("object%d", partID)
|
||||
tmpPartPath := path.Join(tmpMetaPrefix, uploadID, partSuffix)
|
||||
|
||||
// Initialize md5 writer.
|
||||
md5Writer := md5.New()
|
||||
|
||||
var buf = make([]byte, blockSizeV1)
|
||||
for {
|
||||
n, err := io.ReadFull(data, buf)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil && err != io.ErrUnexpectedEOF {
|
||||
return "", toObjectErr(err, bucket, object)
|
||||
}
|
||||
// Update md5 writer.
|
||||
md5Writer.Write(buf[:n])
|
||||
m, err := fs.storage.AppendFile(minioMetaBucket, tmpPartPath, buf[:n])
|
||||
if err != nil {
|
||||
return "", toObjectErr(err, bucket, object)
|
||||
}
|
||||
if m != int64(len(buf[:n])) {
|
||||
return "", toObjectErr(errUnexpected, bucket, object)
|
||||
}
|
||||
}
|
||||
|
||||
newMD5Hex := hex.EncodeToString(md5Writer.Sum(nil))
|
||||
if md5Hex != "" {
|
||||
if newMD5Hex != md5Hex {
|
||||
return "", BadDigest{md5Hex, newMD5Hex}
|
||||
}
|
||||
}
|
||||
|
||||
// Hold write lock as we are updating fs.json
|
||||
nsMutex.Lock(minioMetaBucket, uploadIDPath)
|
||||
defer nsMutex.Unlock(minioMetaBucket, uploadIDPath)
|
||||
|
||||
// Just check if the uploadID exists to avoid copy if it doesn't.
|
||||
if !fs.isUploadIDExists(bucket, object, uploadID) {
|
||||
return "", InvalidUploadID{UploadID: uploadID}
|
||||
}
|
||||
|
||||
fsMeta, err := fs.readFSMetadata(minioMetaBucket, uploadIDPath)
|
||||
if err != nil {
|
||||
return "", toObjectErr(err, minioMetaBucket, uploadIDPath)
|
||||
}
|
||||
fsMeta.AddObjectPart(partID, partSuffix, newMD5Hex, size)
|
||||
|
||||
partPath := path.Join(mpartMetaPrefix, bucket, object, uploadID, partSuffix)
|
||||
err = fs.storage.RenameFile(minioMetaBucket, tmpPartPath, minioMetaBucket, partPath)
|
||||
if err != nil {
|
||||
if dErr := fs.storage.DeleteFile(minioMetaBucket, tmpPartPath); dErr != nil {
|
||||
return "", toObjectErr(dErr, minioMetaBucket, tmpPartPath)
|
||||
}
|
||||
return "", toObjectErr(err, minioMetaBucket, partPath)
|
||||
}
|
||||
uploadIDPath = path.Join(mpartMetaPrefix, bucket, object, uploadID)
|
||||
tempUploadIDPath := path.Join(tmpMetaPrefix, uploadID)
|
||||
if err = fs.writeFSMetadata(minioMetaBucket, tempUploadIDPath, fsMeta); err != nil {
|
||||
return "", toObjectErr(err, minioMetaBucket, tempUploadIDPath)
|
||||
}
|
||||
err = fs.storage.RenameFile(minioMetaBucket, path.Join(tempUploadIDPath, fsMetaJSONFile), minioMetaBucket, path.Join(uploadIDPath, fsMetaJSONFile))
|
||||
if err != nil {
|
||||
if dErr := fs.storage.DeleteFile(minioMetaBucket, path.Join(tempUploadIDPath, fsMetaJSONFile)); dErr != nil {
|
||||
return "", toObjectErr(dErr, minioMetaBucket, tempUploadIDPath)
|
||||
}
|
||||
return "", toObjectErr(err, minioMetaBucket, uploadIDPath)
|
||||
}
|
||||
return newMD5Hex, nil
|
||||
}
|
||||
|
||||
// listObjectParts - wrapper scanning through
|
||||
// '.minio/multipart/bucket/object/UPLOADID'. Lists all the parts
|
||||
// saved inside '.minio/multipart/bucket/object/UPLOADID'.
|
||||
func (fs fsObjects) listObjectParts(bucket, object, uploadID string, partNumberMarker, maxParts int) (ListPartsInfo, error) {
|
||||
result := ListPartsInfo{}
|
||||
|
||||
uploadIDPath := path.Join(mpartMetaPrefix, bucket, object, uploadID)
|
||||
fsMeta, err := fs.readFSMetadata(minioMetaBucket, uploadIDPath)
|
||||
if err != nil {
|
||||
return ListPartsInfo{}, toObjectErr(err, minioMetaBucket, uploadIDPath)
|
||||
}
|
||||
// Only parts with higher part numbers will be listed.
|
||||
partIdx := fsMeta.ObjectPartIndex(partNumberMarker)
|
||||
parts := fsMeta.Parts
|
||||
if partIdx != -1 {
|
||||
parts = fsMeta.Parts[partIdx+1:]
|
||||
}
|
||||
count := maxParts
|
||||
for _, part := range parts {
|
||||
var fi FileInfo
|
||||
partNamePath := path.Join(mpartMetaPrefix, bucket, object, uploadID, part.Name)
|
||||
fi, err = fs.storage.StatFile(minioMetaBucket, partNamePath)
|
||||
if err != nil {
|
||||
return ListPartsInfo{}, toObjectErr(err, minioMetaBucket, partNamePath)
|
||||
}
|
||||
result.Parts = append(result.Parts, partInfo{
|
||||
PartNumber: part.Number,
|
||||
ETag: part.ETag,
|
||||
LastModified: fi.ModTime,
|
||||
Size: fi.Size,
|
||||
})
|
||||
count--
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// If listed entries are more than maxParts, we set IsTruncated as true.
|
||||
if len(parts) > len(result.Parts) {
|
||||
result.IsTruncated = true
|
||||
// Make sure to fill next part number marker if IsTruncated is
|
||||
// true for subsequent listing.
|
||||
nextPartNumberMarker := result.Parts[len(result.Parts)-1].PartNumber
|
||||
result.NextPartNumberMarker = nextPartNumberMarker
|
||||
}
|
||||
result.Bucket = bucket
|
||||
result.Object = object
|
||||
result.UploadID = uploadID
|
||||
result.MaxParts = maxParts
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListObjectParts - lists all previously uploaded parts for a given
|
||||
// object and uploadID. Takes additional input of part-number-marker
|
||||
// to indicate where the listing should begin from.
|
||||
//
|
||||
// Implements S3 compatible ListObjectParts API. The resulting
|
||||
// ListPartsInfo structure is unmarshalled directly into XML and
|
||||
// replied back to the client.
|
||||
func (fs fsObjects) ListObjectParts(bucket, object, uploadID string, partNumberMarker, maxParts int) (ListPartsInfo, error) {
|
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return ListPartsInfo{}, BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
// Verify whether the bucket exists.
|
||||
if !fs.isBucketExist(bucket) {
|
||||
return ListPartsInfo{}, BucketNotFound{Bucket: bucket}
|
||||
}
|
||||
if !IsValidObjectName(object) {
|
||||
return ListPartsInfo{}, ObjectNameInvalid{Bucket: bucket, Object: object}
|
||||
}
|
||||
// Hold lock so that there is no competing abort-multipart-upload or complete-multipart-upload.
|
||||
nsMutex.Lock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, object, uploadID))
|
||||
defer nsMutex.Unlock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, object, uploadID))
|
||||
|
||||
if !fs.isUploadIDExists(bucket, object, uploadID) {
|
||||
return ListPartsInfo{}, InvalidUploadID{UploadID: uploadID}
|
||||
}
|
||||
return fs.listObjectParts(bucket, object, uploadID, partNumberMarker, maxParts)
|
||||
}
|
||||
|
||||
// CompleteMultipartUpload - completes an ongoing multipart
|
||||
// transaction after receiving all the parts indicated by the client.
|
||||
// Returns an md5sum calculated by concatenating all the individual
|
||||
// md5sums of all the parts.
|
||||
//
|
||||
// Implements S3 compatible Complete multipart API.
|
||||
func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, uploadID string, parts []completePart) (string, error) {
|
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return "", BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
// Verify whether the bucket exists.
|
||||
if !fs.isBucketExist(bucket) {
|
||||
return "", BucketNotFound{Bucket: bucket}
|
||||
}
|
||||
if !IsValidObjectName(object) {
|
||||
return "", ObjectNameInvalid{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
}
|
||||
}
|
||||
|
||||
uploadIDPath := path.Join(mpartMetaPrefix, bucket, object, uploadID)
|
||||
// Hold lock so that
|
||||
// 1) no one aborts this multipart upload
|
||||
// 2) no one does a parallel complete-multipart-upload on this
|
||||
// multipart upload
|
||||
nsMutex.Lock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, object, uploadID))
|
||||
defer nsMutex.Unlock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, object, uploadID))
|
||||
|
||||
if !fs.isUploadIDExists(bucket, object, uploadID) {
|
||||
return "", InvalidUploadID{UploadID: uploadID}
|
||||
}
|
||||
|
||||
// Read saved fs metadata for ongoing multipart.
|
||||
fsMeta, err := fs.readFSMetadata(minioMetaBucket, uploadIDPath)
|
||||
if err != nil {
|
||||
return "", toObjectErr(err, minioMetaBucket, uploadIDPath)
|
||||
}
|
||||
|
||||
// Calculate s3 compatible md5sum for complete multipart.
|
||||
s3MD5, err := completeMultipartMD5(parts...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tempObj := path.Join(tmpMetaPrefix, uploadID, "object1")
|
||||
var buffer = make([]byte, blockSizeV1)
|
||||
|
||||
// Loop through all parts, validate them and then commit to disk.
|
||||
for i, part := range parts {
|
||||
partIdx := fsMeta.ObjectPartIndex(part.PartNumber)
|
||||
if partIdx == -1 {
|
||||
return "", InvalidPart{}
|
||||
}
|
||||
if fsMeta.Parts[partIdx].ETag != part.ETag {
|
||||
return "", BadDigest{}
|
||||
}
|
||||
// All parts except the last part has to be atleast 5MB.
|
||||
if (i < len(parts)-1) && !isMinAllowedPartSize(fsMeta.Parts[partIdx].Size) {
|
||||
return "", PartTooSmall{}
|
||||
}
|
||||
// Construct part suffix.
|
||||
partSuffix := fmt.Sprintf("object%d", part.PartNumber)
|
||||
multipartPartFile := path.Join(mpartMetaPrefix, bucket, object, uploadID, partSuffix)
|
||||
offset := int64(0)
|
||||
totalLeft := fsMeta.Parts[partIdx].Size
|
||||
for totalLeft > 0 {
|
||||
var n int64
|
||||
n, err = fs.storage.ReadFile(minioMetaBucket, multipartPartFile, offset, buffer)
|
||||
if err != nil {
|
||||
if err == errFileNotFound {
|
||||
return "", InvalidPart{}
|
||||
}
|
||||
return "", toObjectErr(err, minioMetaBucket, multipartPartFile)
|
||||
}
|
||||
n, err = fs.storage.AppendFile(minioMetaBucket, tempObj, buffer[:n])
|
||||
if err != nil {
|
||||
return "", toObjectErr(err, minioMetaBucket, tempObj)
|
||||
}
|
||||
offset += n
|
||||
totalLeft -= n
|
||||
}
|
||||
}
|
||||
|
||||
// Rename the file back to original location, if not delete the temporary object.
|
||||
err = fs.storage.RenameFile(minioMetaBucket, tempObj, bucket, object)
|
||||
if err != nil {
|
||||
if dErr := fs.storage.DeleteFile(minioMetaBucket, tempObj); dErr != nil {
|
||||
return "", toObjectErr(dErr, minioMetaBucket, tempObj)
|
||||
}
|
||||
return "", toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
// Cleanup all the parts if everything else has been safely committed.
|
||||
if err = cleanupUploadedParts(bucket, object, uploadID, fs.storage); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Hold the lock so that two parallel complete-multipart-uploads do not
|
||||
// leave a stale uploads.json behind.
|
||||
nsMutex.Lock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, object))
|
||||
defer nsMutex.Unlock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, object))
|
||||
|
||||
// Validate if there are other incomplete upload-id's present for
|
||||
// the object, if yes do not attempt to delete 'uploads.json'.
|
||||
uploadsJSON, err := readUploadsJSON(bucket, object, fs.storage)
|
||||
if err != nil {
|
||||
return "", toObjectErr(err, minioMetaBucket, object)
|
||||
}
|
||||
// If we have successfully read `uploads.json`, then we proceed to
|
||||
// purge or update `uploads.json`.
|
||||
uploadIDIdx := uploadsJSON.Index(uploadID)
|
||||
if uploadIDIdx != -1 {
|
||||
uploadsJSON.Uploads = append(uploadsJSON.Uploads[:uploadIDIdx], uploadsJSON.Uploads[uploadIDIdx+1:]...)
|
||||
}
|
||||
if len(uploadsJSON.Uploads) > 0 {
|
||||
if err = updateUploadsJSON(bucket, object, uploadsJSON, fs.storage); err != nil {
|
||||
return "", toObjectErr(err, minioMetaBucket, path.Join(mpartMetaPrefix, bucket, object))
|
||||
}
|
||||
// Return success.
|
||||
return s3MD5, nil
|
||||
}
|
||||
|
||||
if err = fs.storage.DeleteFile(minioMetaBucket, path.Join(mpartMetaPrefix, bucket, object, uploadsJSONFile)); err != nil {
|
||||
return "", toObjectErr(err, minioMetaBucket, path.Join(mpartMetaPrefix, bucket, object))
|
||||
}
|
||||
|
||||
// Return md5sum.
|
||||
return s3MD5, nil
|
||||
}
|
||||
|
||||
// abortMultipartUpload - wrapper for purging an ongoing multipart
|
||||
// transaction, deletes uploadID entry from `uploads.json` and purges
|
||||
// the directory at '.minio/multipart/bucket/object/uploadID' holding
|
||||
// all the upload parts.
|
||||
func (fs fsObjects) abortMultipartUpload(bucket, object, uploadID string) error {
|
||||
// Cleanup all uploaded parts.
|
||||
if err := cleanupUploadedParts(bucket, object, uploadID, fs.storage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate if there are other incomplete upload-id's present for
|
||||
// the object, if yes do not attempt to delete 'uploads.json'.
|
||||
uploadsJSON, err := readUploadsJSON(bucket, object, fs.storage)
|
||||
if err == nil {
|
||||
uploadIDIdx := uploadsJSON.Index(uploadID)
|
||||
if uploadIDIdx != -1 {
|
||||
uploadsJSON.Uploads = append(uploadsJSON.Uploads[:uploadIDIdx], uploadsJSON.Uploads[uploadIDIdx+1:]...)
|
||||
}
|
||||
// There are pending uploads for the same object, preserve
|
||||
// them update 'uploads.json' in-place.
|
||||
if len(uploadsJSON.Uploads) > 0 {
|
||||
err = updateUploadsJSON(bucket, object, uploadsJSON, fs.storage)
|
||||
if err != nil {
|
||||
return toObjectErr(err, bucket, object)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
} // No more pending uploads for the object, we purge the entire
|
||||
// entry at '.minio/multipart/bucket/object'.
|
||||
if err = fs.storage.DeleteFile(minioMetaBucket, path.Join(mpartMetaPrefix, bucket, object, uploadsJSONFile)); err != nil {
|
||||
return toObjectErr(err, minioMetaBucket, path.Join(mpartMetaPrefix, bucket, object))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AbortMultipartUpload - aborts an ongoing multipart operation
|
||||
// signified by the input uploadID. This is an atomic operation
|
||||
// doesn't require clients to initiate multiple such requests.
|
||||
//
|
||||
// All parts are purged from all disks and reference to the uploadID
|
||||
// would be removed from the system, rollback is not possible on this
|
||||
// operation.
|
||||
//
|
||||
// Implements S3 compatible Abort multipart API, slight difference is
|
||||
// that this is an atomic idempotent operation. Subsequent calls have
|
||||
// no affect and further requests to the same uploadID would not be
|
||||
// honored.
|
||||
func (fs fsObjects) AbortMultipartUpload(bucket, object, uploadID string) error {
|
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
if !fs.isBucketExist(bucket) {
|
||||
return BucketNotFound{Bucket: bucket}
|
||||
}
|
||||
if !IsValidObjectName(object) {
|
||||
return ObjectNameInvalid{Bucket: bucket, Object: object}
|
||||
}
|
||||
|
||||
// Hold lock so that there is no competing complete-multipart-upload or put-object-part.
|
||||
nsMutex.Lock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, object, uploadID))
|
||||
defer nsMutex.Unlock(minioMetaBucket, pathJoin(mpartMetaPrefix, bucket, object, uploadID))
|
||||
|
||||
if !fs.isUploadIDExists(bucket, object, uploadID) {
|
||||
return InvalidUploadID{UploadID: uploadID}
|
||||
}
|
||||
|
||||
err := fs.abortMultipartUpload(bucket, object, uploadID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
/*
|
||||
* 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 (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
"github.com/minio/minio/pkg/mimedb"
|
||||
)
|
||||
|
||||
// fsObjects - Implements fs object layer.
|
||||
type fsObjects struct {
|
||||
storage StorageAPI
|
||||
physicalDisk string
|
||||
listObjectMap map[listParams][]*treeWalkerFS
|
||||
listObjectMapMutex *sync.Mutex
|
||||
}
|
||||
|
||||
// newFSObjects - initialize new fs object layer.
|
||||
func newFSObjects(disk string) (ObjectLayer, error) {
|
||||
storage, err := newStorageAPI(disk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Runs house keeping code, like creating minioMetaBucket, cleaning up tmp files etc.
|
||||
fsHouseKeeping(storage)
|
||||
|
||||
// Return successfully initialized object layer.
|
||||
return fsObjects{
|
||||
storage: storage,
|
||||
physicalDisk: disk,
|
||||
listObjectMap: make(map[listParams][]*treeWalkerFS),
|
||||
listObjectMapMutex: &sync.Mutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StorageInfo - returns underlying storage statistics.
|
||||
func (fs fsObjects) StorageInfo() StorageInfo {
|
||||
info, err := disk.GetInfo(fs.physicalDisk)
|
||||
fatalIf(err, "Unable to get disk info "+fs.physicalDisk)
|
||||
return StorageInfo{
|
||||
Total: info.Total,
|
||||
Free: info.Free,
|
||||
}
|
||||
}
|
||||
|
||||
/// Bucket operations
|
||||
|
||||
// MakeBucket - make a bucket.
|
||||
func (fs fsObjects) MakeBucket(bucket string) error {
|
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
if err := fs.storage.MakeVol(bucket); err != nil {
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBucketInfo - get bucket info.
|
||||
func (fs fsObjects) GetBucketInfo(bucket string) (BucketInfo, error) {
|
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return BucketInfo{}, BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
vi, err := fs.storage.StatVol(bucket)
|
||||
if err != nil {
|
||||
return BucketInfo{}, toObjectErr(err, bucket)
|
||||
}
|
||||
return BucketInfo{
|
||||
Name: bucket,
|
||||
Created: vi.Created,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListBuckets - list buckets.
|
||||
func (fs fsObjects) ListBuckets() ([]BucketInfo, error) {
|
||||
var bucketInfos []BucketInfo
|
||||
vols, err := fs.storage.ListVols()
|
||||
if err != nil {
|
||||
return nil, toObjectErr(err)
|
||||
}
|
||||
for _, vol := range vols {
|
||||
// StorageAPI can send volume names which are incompatible
|
||||
// with buckets, handle it and skip them.
|
||||
if !IsValidBucketName(vol.Name) {
|
||||
continue
|
||||
}
|
||||
bucketInfos = append(bucketInfos, BucketInfo{
|
||||
Name: vol.Name,
|
||||
Created: vol.Created,
|
||||
})
|
||||
}
|
||||
sort.Sort(byBucketName(bucketInfos))
|
||||
return bucketInfos, nil
|
||||
}
|
||||
|
||||
// DeleteBucket - delete a bucket.
|
||||
func (fs fsObjects) DeleteBucket(bucket string) error {
|
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
if err := fs.storage.DeleteVol(bucket); err != nil {
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Object Operations
|
||||
|
||||
// GetObject - get an object.
|
||||
func (fs fsObjects) GetObject(bucket, object string, startOffset int64, length int64, writer io.Writer) error {
|
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
// Verify if object is valid.
|
||||
if !IsValidObjectName(object) {
|
||||
return ObjectNameInvalid{Bucket: bucket, Object: object}
|
||||
}
|
||||
var totalLeft = length
|
||||
for totalLeft > 0 {
|
||||
// Figure out the right blockSize as it was encoded before.
|
||||
var curBlockSize int64
|
||||
if blockSizeV1 < totalLeft {
|
||||
curBlockSize = blockSizeV1
|
||||
} else {
|
||||
curBlockSize = totalLeft
|
||||
}
|
||||
buf := make([]byte, curBlockSize)
|
||||
n, err := fs.storage.ReadFile(bucket, object, startOffset, buf)
|
||||
if err != nil {
|
||||
return toObjectErr(err, bucket, object)
|
||||
}
|
||||
_, err = writer.Write(buf[:n])
|
||||
if err != nil {
|
||||
return toObjectErr(err, bucket, object)
|
||||
}
|
||||
totalLeft -= n
|
||||
startOffset += n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetObjectInfo - get object info.
|
||||
func (fs fsObjects) GetObjectInfo(bucket, object string) (ObjectInfo, error) {
|
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return ObjectInfo{}, (BucketNameInvalid{Bucket: bucket})
|
||||
}
|
||||
// Verify if object is valid.
|
||||
if !IsValidObjectName(object) {
|
||||
return ObjectInfo{}, (ObjectNameInvalid{Bucket: bucket, Object: object})
|
||||
}
|
||||
fi, err := fs.storage.StatFile(bucket, object)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
contentType := "application/octet-stream"
|
||||
if objectExt := filepath.Ext(object); objectExt != "" {
|
||||
content, ok := mimedb.DB[strings.ToLower(strings.TrimPrefix(objectExt, "."))]
|
||||
if ok {
|
||||
contentType = content.ContentType
|
||||
}
|
||||
}
|
||||
return ObjectInfo{
|
||||
Bucket: bucket,
|
||||
Name: object,
|
||||
ModTime: fi.ModTime,
|
||||
Size: fi.Size,
|
||||
IsDir: fi.Mode.IsDir(),
|
||||
ContentType: contentType,
|
||||
MD5Sum: "", // Read from metadata.
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PutObject - create an object.
|
||||
func (fs fsObjects) PutObject(bucket string, object string, size int64, data io.Reader, metadata map[string]string) (string, error) {
|
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return "", BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
if !IsValidObjectName(object) {
|
||||
return "", ObjectNameInvalid{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
}
|
||||
}
|
||||
|
||||
uniqueID := getUUID()
|
||||
|
||||
// Temporary object.
|
||||
tempObj := path.Join(tmpMetaPrefix, uniqueID)
|
||||
|
||||
// Initialize md5 writer.
|
||||
md5Writer := md5.New()
|
||||
|
||||
if size == 0 {
|
||||
// For size 0 we write a 0byte file.
|
||||
_, err := fs.storage.AppendFile(minioMetaBucket, tempObj, []byte(""))
|
||||
if err != nil {
|
||||
return "", toObjectErr(err, bucket, object)
|
||||
}
|
||||
} else {
|
||||
// Allocate buffer.
|
||||
buf := make([]byte, blockSizeV1)
|
||||
for {
|
||||
n, rErr := data.Read(buf)
|
||||
if rErr == io.EOF {
|
||||
break
|
||||
}
|
||||
if rErr != nil {
|
||||
return "", toObjectErr(rErr, bucket, object)
|
||||
}
|
||||
// Update md5 writer.
|
||||
md5Writer.Write(buf[:n])
|
||||
m, wErr := fs.storage.AppendFile(minioMetaBucket, tempObj, buf[:n])
|
||||
if wErr != nil {
|
||||
return "", toObjectErr(wErr, bucket, object)
|
||||
}
|
||||
if m != int64(len(buf[:n])) {
|
||||
return "", toObjectErr(errUnexpected, bucket, object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newMD5Hex := hex.EncodeToString(md5Writer.Sum(nil))
|
||||
// md5Hex representation.
|
||||
var md5Hex string
|
||||
if len(metadata) != 0 {
|
||||
md5Hex = metadata["md5Sum"]
|
||||
}
|
||||
if md5Hex != "" {
|
||||
if newMD5Hex != md5Hex {
|
||||
return "", BadDigest{md5Hex, newMD5Hex}
|
||||
}
|
||||
}
|
||||
|
||||
err := fs.storage.RenameFile(minioMetaBucket, tempObj, bucket, object)
|
||||
if err != nil {
|
||||
return "", toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
// Return md5sum, successfully wrote object.
|
||||
return newMD5Hex, nil
|
||||
}
|
||||
|
||||
func (fs fsObjects) DeleteObject(bucket, object string) error {
|
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
if !IsValidObjectName(object) {
|
||||
return ObjectNameInvalid{Bucket: bucket, Object: object}
|
||||
}
|
||||
if err := fs.storage.DeleteFile(bucket, object); err != nil {
|
||||
return toObjectErr(err, bucket, object)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checks whether bucket exists.
|
||||
func isBucketExist(storage StorageAPI, bucketName string) bool {
|
||||
// Check whether bucket exists.
|
||||
_, err := storage.StatVol(bucketName)
|
||||
if err != nil {
|
||||
if err == errVolumeNotFound {
|
||||
return false
|
||||
}
|
||||
errorIf(err, "Stat failed on bucket "+bucketName+".")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (fs fsObjects) listObjects(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
||||
// Convert entry to FileInfo
|
||||
entryToFileInfo := func(entry string) (fileInfo FileInfo, err error) {
|
||||
if strings.HasSuffix(entry, slashSeparator) {
|
||||
// Object name needs to be full path.
|
||||
fileInfo.Name = entry
|
||||
fileInfo.Mode = os.ModeDir
|
||||
return
|
||||
}
|
||||
if fileInfo, err = fs.storage.StatFile(bucket, entry); err != nil {
|
||||
return
|
||||
}
|
||||
// Object name needs to be full path.
|
||||
fileInfo.Name = entry
|
||||
return
|
||||
}
|
||||
|
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return ListObjectsInfo{}, BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
// Verify if bucket exists.
|
||||
if !isBucketExist(fs.storage, bucket) {
|
||||
return ListObjectsInfo{}, BucketNotFound{Bucket: bucket}
|
||||
}
|
||||
if !IsValidObjectPrefix(prefix) {
|
||||
return ListObjectsInfo{}, ObjectNameInvalid{Bucket: bucket, Object: prefix}
|
||||
}
|
||||
// Verify if delimiter is anything other than '/', which we do not support.
|
||||
if delimiter != "" && delimiter != slashSeparator {
|
||||
return ListObjectsInfo{}, UnsupportedDelimiter{
|
||||
Delimiter: delimiter,
|
||||
}
|
||||
}
|
||||
// Verify if marker has prefix.
|
||||
if marker != "" {
|
||||
if !strings.HasPrefix(marker, prefix) {
|
||||
return ListObjectsInfo{}, InvalidMarkerPrefixCombination{
|
||||
Marker: marker,
|
||||
Prefix: prefix,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With max keys of zero we have reached eof, return right here.
|
||||
if maxKeys == 0 {
|
||||
return ListObjectsInfo{}, nil
|
||||
}
|
||||
|
||||
// For delimiter and prefix as '/' we do not list anything at all
|
||||
// since according to s3 spec we stop at the 'delimiter'
|
||||
// along // with the prefix. On a flat namespace with 'prefix'
|
||||
// as '/' we don't have any entries, since all the keys are
|
||||
// of form 'keyName/...'
|
||||
if delimiter == slashSeparator && prefix == slashSeparator {
|
||||
return ListObjectsInfo{}, nil
|
||||
}
|
||||
|
||||
// Over flowing count - reset to maxObjectList.
|
||||
if maxKeys < 0 || maxKeys > maxObjectList {
|
||||
maxKeys = maxObjectList
|
||||
}
|
||||
|
||||
// Default is recursive, if delimiter is set then list non recursive.
|
||||
recursive := true
|
||||
if delimiter == slashSeparator {
|
||||
recursive = false
|
||||
}
|
||||
|
||||
walker := fs.lookupTreeWalk(listParams{bucket, recursive, marker, prefix})
|
||||
if walker == nil {
|
||||
walker = fs.startTreeWalk(bucket, prefix, marker, recursive, func(bucket, object string) bool {
|
||||
return !strings.HasSuffix(object, slashSeparator)
|
||||
})
|
||||
}
|
||||
var fileInfos []FileInfo
|
||||
var eof bool
|
||||
var nextMarker string
|
||||
for i := 0; i < maxKeys; {
|
||||
walkResult, ok := <-walker.ch
|
||||
if !ok {
|
||||
// Closed channel.
|
||||
eof = true
|
||||
break
|
||||
}
|
||||
// For any walk error return right away.
|
||||
if walkResult.err != nil {
|
||||
// File not found is a valid case.
|
||||
if walkResult.err == errFileNotFound {
|
||||
return ListObjectsInfo{}, nil
|
||||
}
|
||||
return ListObjectsInfo{}, toObjectErr(walkResult.err, bucket, prefix)
|
||||
}
|
||||
fileInfo, err := entryToFileInfo(walkResult.entry)
|
||||
if err != nil {
|
||||
return ListObjectsInfo{}, nil
|
||||
}
|
||||
nextMarker = fileInfo.Name
|
||||
fileInfos = append(fileInfos, fileInfo)
|
||||
if walkResult.end {
|
||||
eof = true
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
params := listParams{bucket, recursive, nextMarker, prefix}
|
||||
if !eof {
|
||||
fs.saveTreeWalk(params, walker)
|
||||
}
|
||||
|
||||
result := ListObjectsInfo{IsTruncated: !eof}
|
||||
for _, fileInfo := range fileInfos {
|
||||
// With delimiter set we fill in NextMarker and Prefixes.
|
||||
if delimiter == slashSeparator {
|
||||
result.NextMarker = fileInfo.Name
|
||||
if fileInfo.Mode.IsDir() {
|
||||
result.Prefixes = append(result.Prefixes, fileInfo.Name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
result.Objects = append(result.Objects, ObjectInfo{
|
||||
Name: fileInfo.Name,
|
||||
ModTime: fileInfo.ModTime,
|
||||
Size: fileInfo.Size,
|
||||
IsDir: false,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListObjects - list all objects.
|
||||
func (fs fsObjects) ListObjects(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
||||
return fs.listObjects(bucket, prefix, marker, delimiter, maxKeys)
|
||||
}
|
||||
+149
-92
@@ -17,8 +17,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -38,59 +39,36 @@ func registerHandlers(mux *router.Router, handlerFns ...HandlerFunc) http.Handle
|
||||
return f
|
||||
}
|
||||
|
||||
type timeHandler struct {
|
||||
handler http.Handler
|
||||
// Adds redirect rules for incoming requests.
|
||||
type redirectHandler struct {
|
||||
handler http.Handler
|
||||
locationPrefix string
|
||||
}
|
||||
|
||||
type resourceHandler struct {
|
||||
handler http.Handler
|
||||
// Reserved bucket.
|
||||
const (
|
||||
reservedBucket = "/minio"
|
||||
)
|
||||
|
||||
func setBrowserRedirectHandler(h http.Handler) http.Handler {
|
||||
return redirectHandler{handler: h, locationPrefix: reservedBucket}
|
||||
}
|
||||
|
||||
type ignoreSignatureV2RequestHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func parseDate(req *http.Request) (time.Time, error) {
|
||||
amzDate := req.Header.Get(http.CanonicalHeaderKey("x-amz-date"))
|
||||
switch {
|
||||
case amzDate != "":
|
||||
if _, err := time.Parse(time.RFC1123, amzDate); err == nil {
|
||||
return time.Parse(time.RFC1123, amzDate)
|
||||
}
|
||||
if _, err := time.Parse(time.RFC1123Z, amzDate); err == nil {
|
||||
return time.Parse(time.RFC1123Z, amzDate)
|
||||
}
|
||||
if _, err := time.Parse(iso8601Format, amzDate); err == nil {
|
||||
return time.Parse(iso8601Format, amzDate)
|
||||
func (h redirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Re-direction handled specifically for browsers.
|
||||
if strings.Contains(r.Header.Get("User-Agent"), "Mozilla") {
|
||||
// '/' is redirected to 'locationPrefix/'
|
||||
// '/webrpc' is redirected to 'locationPrefix/webrpc'
|
||||
// '/login' is redirected to 'locationPrefix/login'
|
||||
switch r.URL.Path {
|
||||
case "/", "/webrpc", "/login", "/favicon.ico":
|
||||
location := h.locationPrefix + r.URL.Path
|
||||
// Redirect to new location.
|
||||
http.Redirect(w, r, location, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
minioDate := req.Header.Get(http.CanonicalHeaderKey("x-minio-date"))
|
||||
switch {
|
||||
case minioDate != "":
|
||||
if _, err := time.Parse(time.RFC1123, minioDate); err == nil {
|
||||
return time.Parse(time.RFC1123, minioDate)
|
||||
}
|
||||
if _, err := time.Parse(time.RFC1123Z, minioDate); err == nil {
|
||||
return time.Parse(time.RFC1123Z, minioDate)
|
||||
}
|
||||
if _, err := time.Parse(iso8601Format, minioDate); err == nil {
|
||||
return time.Parse(iso8601Format, minioDate)
|
||||
}
|
||||
}
|
||||
date := req.Header.Get("Date")
|
||||
switch {
|
||||
case date != "":
|
||||
if _, err := time.Parse(time.RFC1123, date); err == nil {
|
||||
return time.Parse(time.RFC1123, date)
|
||||
}
|
||||
if _, err := time.Parse(time.RFC1123Z, date); err == nil {
|
||||
return time.Parse(time.RFC1123Z, date)
|
||||
}
|
||||
if _, err := time.Parse(iso8601Format, amzDate); err == nil {
|
||||
return time.Parse(iso8601Format, amzDate)
|
||||
}
|
||||
}
|
||||
return time.Time{}, errors.New("invalid request")
|
||||
h.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Adds Cache-Control header
|
||||
@@ -98,18 +76,92 @@ type cacheControlHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func setCacheControlHandler(h http.Handler) http.Handler {
|
||||
func setBrowserCacheControlHandler(h http.Handler) http.Handler {
|
||||
return cacheControlHandler{h}
|
||||
}
|
||||
|
||||
func (h cacheControlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
// expire the cache in one week
|
||||
w.Header().Set("Cache-Control", "public, max-age=604800")
|
||||
if r.Method == "GET" && strings.Contains(r.Header.Get("User-Agent"), "Mozilla") {
|
||||
// For all browser requests set appropriate Cache-Control policies
|
||||
match, e := regexp.MatchString(reservedBucket+`/([^/]+\.js|favicon.ico)`, r.URL.Path)
|
||||
if e != nil {
|
||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
||||
return
|
||||
}
|
||||
if match {
|
||||
// For assets set cache expiry of one year. For each release, the name
|
||||
// of the asset name will change and hence it can not be served from cache.
|
||||
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||
} else if strings.HasPrefix(r.URL.Path, reservedBucket+"/") {
|
||||
// For non asset requests we serve index.html which will never be cached.
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
}
|
||||
}
|
||||
h.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Adds verification for incoming paths.
|
||||
type minioPrivateBucketHandler struct {
|
||||
handler http.Handler
|
||||
reservedBucket string
|
||||
}
|
||||
|
||||
func setPrivateBucketHandler(h http.Handler) http.Handler {
|
||||
return minioPrivateBucketHandler{handler: h, reservedBucket: reservedBucket}
|
||||
}
|
||||
|
||||
func (h minioPrivateBucketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// For all non browser requests, reject access to 'reservedBucket'.
|
||||
if !strings.Contains(r.Header.Get("User-Agent"), "Mozilla") && path.Clean(r.URL.Path) == reservedBucket {
|
||||
writeErrorResponse(w, r, ErrAllAccessDisabled, r.URL.Path)
|
||||
return
|
||||
}
|
||||
h.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Supported Amz date formats.
|
||||
var amzDateFormats = []string{
|
||||
time.RFC1123,
|
||||
time.RFC1123Z,
|
||||
iso8601Format,
|
||||
// Add new AMZ date formats here.
|
||||
}
|
||||
|
||||
// parseAmzDate - parses date string into supported amz date formats.
|
||||
func parseAmzDate(amzDateStr string) (amzDate time.Time, apiErr APIErrorCode) {
|
||||
for _, dateFormat := range amzDateFormats {
|
||||
amzDate, e := time.Parse(dateFormat, amzDateStr)
|
||||
if e == nil {
|
||||
return amzDate, ErrNone
|
||||
}
|
||||
}
|
||||
return time.Time{}, ErrMalformedDate
|
||||
}
|
||||
|
||||
// Supported Amz date headers.
|
||||
var amzDateHeaders = []string{
|
||||
"x-amz-date",
|
||||
"x-minio-date",
|
||||
"date",
|
||||
}
|
||||
|
||||
// parseAmzDateHeader - parses supported amz date headers, in
|
||||
// supported amz date formats.
|
||||
func parseAmzDateHeader(req *http.Request) (time.Time, APIErrorCode) {
|
||||
for _, amzDateHeader := range amzDateHeaders {
|
||||
amzDateStr := req.Header.Get(http.CanonicalHeaderKey(amzDateHeader))
|
||||
if amzDateStr != "" {
|
||||
return parseAmzDate(amzDateStr)
|
||||
}
|
||||
}
|
||||
// Date header missing.
|
||||
return time.Time{}, ErrMissingDateHeader
|
||||
}
|
||||
|
||||
type timeHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
// setTimeValidityHandler to validate parsable time over http header
|
||||
func setTimeValidityHandler(h http.Handler) http.Handler {
|
||||
return timeHandler{h}
|
||||
@@ -117,66 +169,51 @@ func setTimeValidityHandler(h http.Handler) http.Handler {
|
||||
|
||||
func (h timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify if date headers are set, if not reject the request
|
||||
if r.Header.Get("Authorization") != "" {
|
||||
if r.Header.Get(http.CanonicalHeaderKey("x-amz-date")) == "" && r.Header.Get(http.CanonicalHeaderKey("x-minio-date")) == "" && r.Header.Get("Date") == "" {
|
||||
// there is no way to knowing if this is a valid request, could be a attack reject such clients
|
||||
writeErrorResponse(w, r, RequestTimeTooSkewed, r.URL.Path)
|
||||
if _, ok := r.Header["Authorization"]; ok {
|
||||
amzDate, apiErr := parseAmzDateHeader(r)
|
||||
if apiErr != ErrNone {
|
||||
// All our internal APIs are sensitive towards Date
|
||||
// header, for all requests where Date header is not
|
||||
// present we will reject such clients.
|
||||
writeErrorResponse(w, r, apiErr, r.URL.Path)
|
||||
return
|
||||
}
|
||||
date, err := parseDate(r)
|
||||
if err != nil {
|
||||
// there is no way to knowing if this is a valid request, could be a attack reject such clients
|
||||
writeErrorResponse(w, r, RequestTimeTooSkewed, r.URL.Path)
|
||||
return
|
||||
}
|
||||
duration := time.Since(date)
|
||||
minutes := time.Duration(5) * time.Minute
|
||||
if duration.Minutes() > minutes.Minutes() {
|
||||
writeErrorResponse(w, r, RequestTimeTooSkewed, r.URL.Path)
|
||||
// Verify if the request date header is more than 5minutes
|
||||
// late, reject such clients.
|
||||
if time.Now().UTC().Sub(amzDate)/time.Minute > time.Duration(5)*time.Minute {
|
||||
writeErrorResponse(w, r, ErrRequestTimeTooSkewed, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
h.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
type resourceHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
// setCorsHandler handler for CORS (Cross Origin Resource Sharing)
|
||||
func setCorsHandler(h http.Handler) http.Handler {
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "HEAD", "POST", "PUT"},
|
||||
AllowedHeaders: []string{"*"},
|
||||
ExposedHeaders: []string{"ETag"},
|
||||
})
|
||||
return c.Handler(h)
|
||||
}
|
||||
|
||||
// setIgnoreSignatureV2RequestHandler -
|
||||
// Verify if authorization header has signature version '2', reject it cleanly.
|
||||
func setIgnoreSignatureV2RequestHandler(h http.Handler) http.Handler {
|
||||
return ignoreSignatureV2RequestHandler{h}
|
||||
}
|
||||
|
||||
// Ignore signature version '2' ServerHTTP() wrapper.
|
||||
func (h ignoreSignatureV2RequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := r.Header["Authorization"]; ok {
|
||||
if !strings.HasPrefix(r.Header.Get("Authorization"), authHeaderPrefix) {
|
||||
writeErrorResponse(w, r, SignatureVersionNotSupported, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
h.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// setIgnoreResourcesHandler -
|
||||
// Ignore resources handler is wrapper handler used for API request resource validation
|
||||
// Since we do not support all the S3 queries, it is necessary for us to throw back a
|
||||
// valid error message indicating such a feature is not implemented.
|
||||
// valid error message indicating that requested feature is not implemented.
|
||||
func setIgnoreResourcesHandler(h http.Handler) http.Handler {
|
||||
return resourceHandler{h}
|
||||
}
|
||||
|
||||
// Resource handler ServeHTTP() wrapper
|
||||
func (h resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip the first element which is usally '/' and split the rest.
|
||||
// Skip the first element which is usually '/' and split the rest.
|
||||
splits := strings.SplitN(r.URL.Path[1:], "/", 2)
|
||||
|
||||
// Save bucketName and objectName extracted from url Path.
|
||||
@@ -192,20 +229,20 @@ func (h resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// level resource queries.
|
||||
if bucketName != "" && objectName == "" {
|
||||
if ignoreNotImplementedBucketResources(r) {
|
||||
writeErrorResponse(w, r, NotImplemented, r.URL.Path)
|
||||
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
// If bucketName and objectName are present check for its resource queries.
|
||||
if bucketName != "" && objectName != "" {
|
||||
if ignoreNotImplementedObjectResources(r) {
|
||||
writeErrorResponse(w, r, NotImplemented, r.URL.Path)
|
||||
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
|
||||
return
|
||||
}
|
||||
}
|
||||
// A put method on path "/" doesn't make sense, ignore it.
|
||||
if r.Method == "PUT" && r.URL.Path == "/" {
|
||||
writeErrorResponse(w, r, NotImplemented, r.URL.Path)
|
||||
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
|
||||
return
|
||||
}
|
||||
h.handler.ServeHTTP(w, r)
|
||||
@@ -215,8 +252,7 @@ func (h resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Checks requests for not implemented Bucket resources
|
||||
func ignoreNotImplementedBucketResources(req *http.Request) bool {
|
||||
q := req.URL.Query()
|
||||
for name := range q {
|
||||
for name := range req.URL.Query() {
|
||||
if notimplementedBucketResourceNames[name] {
|
||||
return true
|
||||
}
|
||||
@@ -226,11 +262,32 @@ func ignoreNotImplementedBucketResources(req *http.Request) bool {
|
||||
|
||||
// Checks requests for not implemented Object resources
|
||||
func ignoreNotImplementedObjectResources(req *http.Request) bool {
|
||||
q := req.URL.Query()
|
||||
for name := range q {
|
||||
for name := range req.URL.Query() {
|
||||
if notimplementedObjectResourceNames[name] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// List of not implemented bucket queries
|
||||
var notimplementedBucketResourceNames = map[string]bool{
|
||||
"acl": true,
|
||||
"cors": true,
|
||||
"lifecycle": true,
|
||||
"logging": true,
|
||||
"notification": true,
|
||||
"replication": true,
|
||||
"tagging": true,
|
||||
"versions": true,
|
||||
"requestPayment": true,
|
||||
"versioning": true,
|
||||
"website": true,
|
||||
}
|
||||
|
||||
// List of not implemented object queries
|
||||
var notimplementedObjectResourceNames = map[string]bool{
|
||||
"torrent": true,
|
||||
"acl": true,
|
||||
"policy": true,
|
||||
}
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 "github.com/fatih/color"
|
||||
|
||||
// Global constants for Minio.
|
||||
const (
|
||||
minGoVersion = ">= 1.6" // Minio requires at least Go v1.6
|
||||
)
|
||||
|
||||
// minio configuration related constants.
|
||||
const (
|
||||
globalMinioConfigVersion = "4"
|
||||
globalMinioConfigDir = ".minio"
|
||||
globalMinioCertsDir = ".minio/certs"
|
||||
globalMinioCertFile = "public.crt"
|
||||
globalMinioKeyFile = "private.key"
|
||||
globalMinioConfigFile = "config.json"
|
||||
globalMinioProfilePath = "profile"
|
||||
// Add new global values here.
|
||||
)
|
||||
|
||||
var (
|
||||
globalQuiet = false // Quiet flag set via command line
|
||||
globalTrace = false // Trace flag set via environment setting.
|
||||
// Add new global flags here.
|
||||
)
|
||||
|
||||
// global colors.
|
||||
var (
|
||||
colorMagenta = color.New(color.FgMagenta, color.Bold).SprintfFunc()
|
||||
colorWhite = color.New(color.FgWhite, color.Bold).SprintfFunc()
|
||||
colorGreen = color.New(color.FgGreen, color.Bold).SprintfFunc()
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 (
|
||||
"io"
|
||||
)
|
||||
|
||||
// validates location constraint from the request body.
|
||||
// the location value in the request body should match the Region in serverConfig.
|
||||
// other values of location are not accepted.
|
||||
// make bucket fails in such cases.
|
||||
func isValidLocationContraint(reqBody io.Reader, serverRegion string) APIErrorCode {
|
||||
var locationContraint createBucketLocationConfiguration
|
||||
var errCode APIErrorCode
|
||||
errCode = ErrNone
|
||||
e := xmlDecoder(reqBody, &locationContraint)
|
||||
if e != nil {
|
||||
if e == io.EOF {
|
||||
// Do nothing.
|
||||
// failed due to empty body. The location will be set to default value from the serverConfig.
|
||||
// this is valid.
|
||||
errCode = ErrNone
|
||||
} else {
|
||||
// Failed due to malformed configuration.
|
||||
errCode = ErrMalformedXML
|
||||
//writeErrorResponse(w, r, ErrMalformedXML, r.URL.Path)
|
||||
}
|
||||
} else {
|
||||
// Region obtained from the body.
|
||||
// It should be equal to Region in serverConfig.
|
||||
// Else ErrInvalidRegion returned.
|
||||
// For empty value location will be to set to default value from the serverConfig.
|
||||
if locationContraint.Location != "" && serverRegion != locationContraint.Location {
|
||||
//writeErrorResponse(w, r, ErrInvalidRegion, r.URL.Path)
|
||||
errCode = ErrInvalidRegion
|
||||
}
|
||||
}
|
||||
return errCode
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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/xml"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests validate bucket LocationConstraint.
|
||||
func TestIsValidLocationContraint(t *testing.T) {
|
||||
// generates the input request with XML bucket configuration set to the request body.
|
||||
createExpectedRequest := func(req *http.Request, location string) (*http.Request, error) {
|
||||
createBucketConfig := createBucketLocationConfiguration{}
|
||||
createBucketConfig.Location = location
|
||||
var createBucketConfigBytes []byte
|
||||
createBucketConfigBytes, e := xml.Marshal(createBucketConfig)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
createBucketConfigBuffer := bytes.NewBuffer(createBucketConfigBytes)
|
||||
req.Body = ioutil.NopCloser(createBucketConfigBuffer)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
locationForInputRequest string
|
||||
serverConfigRegion string
|
||||
expectedCode APIErrorCode
|
||||
}{
|
||||
// Test case - 1.
|
||||
{"us-east-1", "us-east-1", ErrNone},
|
||||
// Test case - 2.
|
||||
// In case of empty request body ErrNone is returned.
|
||||
{"", "us-east-1", ErrNone},
|
||||
// Test case - 3.
|
||||
{"eu-central-1", "us-east-1", ErrInvalidRegion},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
inputRequest, e := createExpectedRequest(&http.Request{}, testCase.locationForInputRequest)
|
||||
if e != nil {
|
||||
t.Fatalf("Test %d: Failed to Marshal bucket configuration", i+1)
|
||||
}
|
||||
actualCode := isValidLocationContraint(inputRequest.Body, testCase.serverConfigRegion)
|
||||
if testCase.expectedCode != actualCode {
|
||||
t.Errorf("Test %d: Expected the APIErrCode to be %d, but instead found %d", i+1, testCase.expectedCode, actualCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-16
@@ -21,15 +21,19 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-xl/pkg/probe"
|
||||
"github.com/minio/minio/pkg/fs"
|
||||
)
|
||||
|
||||
const (
|
||||
b = "bytes="
|
||||
)
|
||||
|
||||
// InvalidRange - invalid range
|
||||
type InvalidRange struct{}
|
||||
|
||||
func (e InvalidRange) Error() string {
|
||||
return "The requested range is not satisfiable"
|
||||
}
|
||||
|
||||
// HttpRange specifies the byte range to be sent to the client.
|
||||
type httpRange struct {
|
||||
start, length, size int64
|
||||
@@ -41,7 +45,7 @@ func (r *httpRange) String() string {
|
||||
}
|
||||
|
||||
// Grab new range from request header
|
||||
func getRequestedRange(hrange string, size int64) (*httpRange, *probe.Error) {
|
||||
func getRequestedRange(hrange string, size int64) (*httpRange, error) {
|
||||
r := &httpRange{
|
||||
start: 0,
|
||||
length: 0,
|
||||
@@ -51,16 +55,16 @@ func getRequestedRange(hrange string, size int64) (*httpRange, *probe.Error) {
|
||||
if hrange != "" {
|
||||
err := r.parseRange(hrange)
|
||||
if err != nil {
|
||||
return nil, err.Trace()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *httpRange) parse(ra string) *probe.Error {
|
||||
func (r *httpRange) parse(ra string) error {
|
||||
i := strings.Index(ra, "-")
|
||||
if i < 0 {
|
||||
return probe.NewError(fs.InvalidRange{})
|
||||
return InvalidRange{}
|
||||
}
|
||||
start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:])
|
||||
if start == "" {
|
||||
@@ -68,7 +72,7 @@ func (r *httpRange) parse(ra string) *probe.Error {
|
||||
// range start relative to the end of the file.
|
||||
i, err := strconv.ParseInt(end, 10, 64)
|
||||
if err != nil {
|
||||
return probe.NewError(fs.InvalidRange{})
|
||||
return InvalidRange{}
|
||||
}
|
||||
if i > r.size {
|
||||
i = r.size
|
||||
@@ -78,7 +82,7 @@ func (r *httpRange) parse(ra string) *probe.Error {
|
||||
} else {
|
||||
i, err := strconv.ParseInt(start, 10, 64)
|
||||
if err != nil || i > r.size || i < 0 {
|
||||
return probe.NewError(fs.InvalidRange{})
|
||||
return InvalidRange{}
|
||||
}
|
||||
r.start = i
|
||||
if end == "" {
|
||||
@@ -87,7 +91,7 @@ func (r *httpRange) parse(ra string) *probe.Error {
|
||||
} else {
|
||||
i, err := strconv.ParseInt(end, 10, 64)
|
||||
if err != nil || r.start > i {
|
||||
return probe.NewError(fs.InvalidRange{})
|
||||
return InvalidRange{}
|
||||
}
|
||||
if i >= r.size {
|
||||
i = r.size - 1
|
||||
@@ -99,26 +103,26 @@ func (r *httpRange) parse(ra string) *probe.Error {
|
||||
}
|
||||
|
||||
// parseRange parses a Range header string as per RFC 2616.
|
||||
func (r *httpRange) parseRange(s string) *probe.Error {
|
||||
func (r *httpRange) parseRange(s string) error {
|
||||
if s == "" {
|
||||
return probe.NewError(errors.New("header not present"))
|
||||
return errors.New("header not present")
|
||||
}
|
||||
if !strings.HasPrefix(s, b) {
|
||||
return probe.NewError(fs.InvalidRange{})
|
||||
return InvalidRange{}
|
||||
}
|
||||
|
||||
ras := strings.Split(s[len(b):], ",")
|
||||
if len(ras) == 0 {
|
||||
return probe.NewError(errors.New("invalid request"))
|
||||
return errors.New("invalid request")
|
||||
}
|
||||
// Just pick the first one and ignore the rest, we only support one range per object
|
||||
if len(ras) > 1 {
|
||||
return probe.NewError(errors.New("multiple ranges specified"))
|
||||
return errors.New("multiple ranges specified")
|
||||
}
|
||||
|
||||
ra := strings.TrimSpace(ras[0])
|
||||
if ra == "" {
|
||||
return probe.NewError(fs.InvalidRange{})
|
||||
return InvalidRange{}
|
||||
}
|
||||
return r.parse(ra)
|
||||
}
|
||||
|
||||
@@ -1,57 +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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
jwtgo "github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
type jwtAuthHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
// setJWTAuthHandler -
|
||||
// Verify if authorization header is of form JWT, reject it otherwise.
|
||||
func setJWTAuthHandler(h http.Handler) http.Handler {
|
||||
return jwtAuthHandler{h}
|
||||
}
|
||||
|
||||
// Ignore request if authorization header is not valid.
|
||||
func (h jwtAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Let the top level caller handle if the requests should be
|
||||
// allowed, if there are no Authorization headers.
|
||||
if r.Header.Get("Authorization") == "" {
|
||||
h.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
// Validate Authorization header to be valid.
|
||||
jwt := InitJWT()
|
||||
token, e := jwtgo.ParseFromRequest(r, func(token *jwtgo.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwtgo.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return jwt.secretAccessKey, nil
|
||||
})
|
||||
if e != nil || !token.Valid {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
h.handler.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 (
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
// consoleLogger - default logger if not other logging is enabled.
|
||||
type consoleLogger struct {
|
||||
Enable bool `json:"enable"`
|
||||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
// enable console logger.
|
||||
func enableConsoleLogger() {
|
||||
clogger := serverConfig.GetConsoleLogger()
|
||||
if !clogger.Enable {
|
||||
// Disable console logger if asked for.
|
||||
log.Out = ioutil.Discard
|
||||
return
|
||||
}
|
||||
|
||||
// log.Out and log.Formatter use the default versions.
|
||||
// Only set specific log level.
|
||||
lvl, err := logrus.ParseLevel(clogger.Level)
|
||||
fatalIf(err, "Unknown log level found in the config file.")
|
||||
|
||||
log.Level = lvl
|
||||
}
|
||||
+26
-22
@@ -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.
|
||||
@@ -21,32 +21,39 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/minio/minio-xl/pkg/probe"
|
||||
)
|
||||
|
||||
type fileLogger struct {
|
||||
Enable bool `json:"enable"`
|
||||
Filename string `json:"fileName"`
|
||||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
type localFile struct {
|
||||
*os.File
|
||||
}
|
||||
|
||||
func log2File(filename string) *probe.Error {
|
||||
fileHook, e := newFile(filename)
|
||||
if e != nil {
|
||||
return probe.NewError(e)
|
||||
func enableFileLogger() {
|
||||
flogger := serverConfig.GetFileLogger()
|
||||
if !flogger.Enable || flogger.Filename == "" {
|
||||
return
|
||||
}
|
||||
log.Hooks.Add(fileHook) // Add a local file hook.
|
||||
log.Formatter = &logrus.JSONFormatter{} // JSON formatted log.
|
||||
log.Level = logrus.InfoLevel // Minimum log level.
|
||||
return nil
|
||||
}
|
||||
|
||||
func newFile(filename string) (*localFile, error) {
|
||||
file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &localFile{file}, nil
|
||||
|
||||
file, err := os.OpenFile(flogger.Filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
fatalIf(err, "Unable to open log file.")
|
||||
|
||||
// Add a local file hook.
|
||||
log.Hooks.Add(&localFile{file})
|
||||
|
||||
lvl, err := logrus.ParseLevel(flogger.Level)
|
||||
fatalIf(err, "Unknown log level found in the config file.")
|
||||
|
||||
// Set default JSON formatter.
|
||||
log.Formatter = new(logrus.JSONFormatter)
|
||||
log.Level = lvl // Minimum log level.
|
||||
}
|
||||
|
||||
// Fire fires the file logger hook and logs to the file.
|
||||
func (l *localFile) Fire(entry *logrus.Entry) error {
|
||||
line, err := entry.String()
|
||||
if err != nil {
|
||||
@@ -57,14 +64,11 @@ func (l *localFile) Fire(entry *logrus.Entry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Levels -
|
||||
// Levels - indicate log levels supported.
|
||||
func (l *localFile) Levels() []logrus.Level {
|
||||
return []logrus.Level{
|
||||
logrus.PanicLevel,
|
||||
logrus.FatalLevel,
|
||||
logrus.ErrorLevel,
|
||||
logrus.WarnLevel,
|
||||
logrus.InfoLevel,
|
||||
logrus.DebugLevel,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +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"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/minio/minio-xl/pkg/probe"
|
||||
"gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
// mongoDB collection
|
||||
type mongoDB struct {
|
||||
c *mgo.Collection
|
||||
}
|
||||
|
||||
func log2Mongo(url, db, collection string) *probe.Error {
|
||||
mongoHook, e := newMongo(url, db, collection)
|
||||
if e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
log.Hooks.Add(mongoHook) // Add mongodb hook.
|
||||
log.Formatter = &logrus.JSONFormatter{} // JSON formatted log.
|
||||
log.Level = logrus.InfoLevel // Minimum log level.
|
||||
return nil
|
||||
}
|
||||
|
||||
// newMongo -
|
||||
func newMongo(mgoEndpoint, db, collection string) (*mongoDB, error) {
|
||||
session, err := mgo.Dial(mgoEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mongoDB{c: session.DB(db).C(collection)}, nil
|
||||
}
|
||||
|
||||
// Fire - the log event
|
||||
func (h *mongoDB) Fire(entry *logrus.Entry) error {
|
||||
entry.Data["Level"] = entry.Level.String()
|
||||
entry.Data["Time"] = entry.Time
|
||||
entry.Data["Message"] = entry.Message
|
||||
mgoErr := h.c.Insert(bson.M(entry.Data))
|
||||
if mgoErr != nil {
|
||||
return fmt.Errorf("Failed to send log entry to mongodb: %s", mgoErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Levels -
|
||||
func (h *mongoDB) Levels() []logrus.Level {
|
||||
return []logrus.Level{
|
||||
logrus.PanicLevel,
|
||||
logrus.FatalLevel,
|
||||
logrus.ErrorLevel,
|
||||
logrus.WarnLevel,
|
||||
logrus.InfoLevel,
|
||||
logrus.DebugLevel,
|
||||
}
|
||||
}
|
||||
+13
-18
@@ -1,7 +1,7 @@
|
||||
// +build !windows
|
||||
|
||||
/*
|
||||
* 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.
|
||||
@@ -23,9 +23,14 @@ import (
|
||||
"log/syslog"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/minio/minio-xl/pkg/probe"
|
||||
)
|
||||
|
||||
type syslogLogger struct {
|
||||
Enable bool `json:"enable"`
|
||||
Addr string `json:"address"`
|
||||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
// syslogHook to send logs via syslog.
|
||||
type syslogHook struct {
|
||||
writer *syslog.Writer
|
||||
@@ -33,15 +38,14 @@ type syslogHook struct {
|
||||
syslogRaddr string
|
||||
}
|
||||
|
||||
func log2Syslog(network, raddr string) *probe.Error {
|
||||
syslogHook, e := newSyslog(network, raddr, syslog.LOG_ERR, "MINIO")
|
||||
if e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
// enableSyslogLogger - enable logger at raddr.
|
||||
func enableSyslogLogger(raddr string) {
|
||||
syslogHook, err := newSyslog("udp", raddr, syslog.LOG_ERR, "MINIO")
|
||||
fatalIf(err, "Unable to initialize syslog logger.")
|
||||
|
||||
log.Hooks.Add(syslogHook) // Add syslog hook.
|
||||
log.Formatter = &logrus.JSONFormatter{} // JSON formatted log.
|
||||
log.Level = logrus.InfoLevel // Minimum log level.
|
||||
return nil
|
||||
log.Level = logrus.ErrorLevel // Minimum log level.
|
||||
}
|
||||
|
||||
// newSyslog - Creates a hook to be added to an instance of logger.
|
||||
@@ -63,12 +67,6 @@ func (hook *syslogHook) Fire(entry *logrus.Entry) error {
|
||||
return hook.writer.Crit(line)
|
||||
case logrus.ErrorLevel:
|
||||
return hook.writer.Err(line)
|
||||
case logrus.WarnLevel:
|
||||
return hook.writer.Warning(line)
|
||||
case logrus.InfoLevel:
|
||||
return hook.writer.Info(line)
|
||||
case logrus.DebugLevel:
|
||||
return hook.writer.Debug(line)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -80,8 +78,5 @@ func (hook *syslogHook) Levels() []logrus.Level {
|
||||
logrus.PanicLevel,
|
||||
logrus.FatalLevel,
|
||||
logrus.ErrorLevel,
|
||||
logrus.WarnLevel,
|
||||
logrus.InfoLevel,
|
||||
logrus.DebugLevel,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// +build windows
|
||||
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,8 +18,13 @@
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/minio/minio-xl/pkg/probe"
|
||||
|
||||
func log2Syslog(network, raddr string) *probe.Error {
|
||||
return probe.NewError(errSysLogNotSupported)
|
||||
type syslogLogger struct {
|
||||
Enable bool `json:"enable"`
|
||||
Addr string `json:"address"`
|
||||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
// enableSyslogLogger - unsupported on windows.
|
||||
func enableSyslogLogger(raddr string) {
|
||||
fatalIf(errSyslogNotSupported, "Unable to enable syslog.")
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -17,49 +17,105 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/minio/minio-xl/pkg/probe"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
type fields map[string]interface{}
|
||||
|
||||
var log = logrus.New() // Default console logger.
|
||||
|
||||
func errorIf(err *probe.Error, msg string, fields map[string]interface{}) {
|
||||
// logger carries logging configuration for various supported loggers.
|
||||
// Currently supported loggers are
|
||||
//
|
||||
// - console [default]
|
||||
// - file
|
||||
// - syslog
|
||||
//
|
||||
type logger struct {
|
||||
Console consoleLogger `json:"console"`
|
||||
File fileLogger `json:"file"`
|
||||
Syslog syslogLogger `json:"syslog"`
|
||||
// Add new loggers here.
|
||||
}
|
||||
|
||||
// sysInfo returns useful system statistics.
|
||||
func sysInfo() map[string]string {
|
||||
host, err := os.Hostname()
|
||||
if err != nil {
|
||||
host = ""
|
||||
}
|
||||
memstats := &runtime.MemStats{}
|
||||
runtime.ReadMemStats(memstats)
|
||||
return map[string]string{
|
||||
"host.name": host,
|
||||
"host.os": runtime.GOOS,
|
||||
"host.arch": runtime.GOARCH,
|
||||
"host.lang": runtime.Version(),
|
||||
"host.cpus": strconv.Itoa(runtime.NumCPU()),
|
||||
"mem.used": humanize.Bytes(memstats.Alloc),
|
||||
"mem.total": humanize.Bytes(memstats.Sys),
|
||||
"mem.heap.used": humanize.Bytes(memstats.HeapAlloc),
|
||||
"mem.heap.total": humanize.Bytes(memstats.HeapSys),
|
||||
}
|
||||
}
|
||||
|
||||
// stackInfo returns printable stack trace.
|
||||
func stackInfo() string {
|
||||
// Convert stack-trace bytes to io.Reader.
|
||||
rawStack := bufio.NewReader(bytes.NewBuffer(debug.Stack()))
|
||||
// Skip stack trace lines until our real caller.
|
||||
for i := 0; i <= 4; i++ {
|
||||
rawStack.ReadLine()
|
||||
}
|
||||
|
||||
// Read the rest of useful stack trace.
|
||||
stackBuf := new(bytes.Buffer)
|
||||
stackBuf.ReadFrom(rawStack)
|
||||
|
||||
// Strip GOPATH of the build system and return.
|
||||
return strings.Replace(stackBuf.String(), minioGOPATH+"/src/", "", -1)
|
||||
}
|
||||
|
||||
// errorIf synonymous with fatalIf but doesn't exit on error != nil
|
||||
func errorIf(err error, msg string, data ...interface{}) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if fields == nil {
|
||||
fields = make(map[string]interface{})
|
||||
sysInfo := sysInfo()
|
||||
fields := logrus.Fields{
|
||||
"cause": err.Error(),
|
||||
"type": reflect.TypeOf(err),
|
||||
"sysInfo": sysInfo,
|
||||
}
|
||||
fields["Error"] = struct {
|
||||
Cause string `json:"cause,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
CallTrace []probe.TracePoint `json:"trace,omitempty"`
|
||||
SysInfo map[string]string `json:"sysinfo,omitempty"`
|
||||
}{
|
||||
err.Cause.Error(),
|
||||
reflect.TypeOf(err.Cause).String(),
|
||||
err.CallTrace,
|
||||
err.SysInfo,
|
||||
if globalTrace {
|
||||
fields["stack"] = "\n" + stackInfo()
|
||||
}
|
||||
log.WithFields(fields).Error(msg)
|
||||
log.WithFields(fields).Errorf(msg, data...)
|
||||
}
|
||||
|
||||
func fatalIf(err *probe.Error, msg string, fields map[string]interface{}) {
|
||||
// fatalIf wrapper function which takes error and prints jsonic error messages.
|
||||
func fatalIf(err error, msg string, data ...interface{}) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if fields == nil {
|
||||
fields = make(map[string]interface{})
|
||||
sysInfo := sysInfo()
|
||||
fields := logrus.Fields{
|
||||
"cause": err.Error(),
|
||||
"type": reflect.TypeOf(err),
|
||||
"sysInfo": sysInfo,
|
||||
}
|
||||
|
||||
fields["error"] = err.ToGoError()
|
||||
if jsonErr, e := json.Marshal(err); e == nil {
|
||||
fields["probe"] = string(jsonErr)
|
||||
if globalTrace {
|
||||
fields["stack"] = "\n" + stackInfo()
|
||||
}
|
||||
log.WithFields(fields).Fatal(msg)
|
||||
log.WithFields(fields).Fatalf(msg, data...)
|
||||
}
|
||||
|
||||
+3
-4
@@ -22,7 +22,6 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/minio/minio-xl/pkg/probe"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
@@ -37,12 +36,12 @@ func (s *LoggerSuite) TestLogger(c *C) {
|
||||
log.Out = &buffer
|
||||
log.Formatter = new(logrus.JSONFormatter)
|
||||
|
||||
errorIf(probe.NewError(errors.New("Fake error")), "Failed with error.", nil)
|
||||
errorIf(errors.New("Fake error"), "Failed with error.")
|
||||
err := json.Unmarshal(buffer.Bytes(), &fields)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(fields["level"], Equals, "error")
|
||||
|
||||
msg, ok := fields["Error"]
|
||||
msg, ok := fields["cause"]
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(msg.(map[string]interface{})["cause"], Equals, "Fake error")
|
||||
c.Assert(msg, Equals, "Fake error")
|
||||
}
|
||||
|
||||
@@ -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,12 +19,22 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio-xl/pkg/probe"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/pkg/profile"
|
||||
)
|
||||
|
||||
var (
|
||||
// global flags for minio.
|
||||
minioFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Show help.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Help template for minio.
|
||||
@@ -45,55 +55,36 @@ FLAGS:
|
||||
{{end}}{{end}}
|
||||
VERSION:
|
||||
` + minioVersion +
|
||||
`{{ "\n"}}{{range $key, $value := ExtraInfo}}
|
||||
{{$key}}:
|
||||
{{$value}}
|
||||
{{end}}`
|
||||
`{{ "\n"}}`
|
||||
|
||||
// init - check the environment before main starts
|
||||
func init() {
|
||||
// Check if minio was compiled using a supported version of Golang.
|
||||
checkGolangRuntimeVersion()
|
||||
checkGoVersion()
|
||||
|
||||
if os.Getenv("DOCKERIMAGE") == "1" {
|
||||
// the further checks are ignored for docker image
|
||||
return
|
||||
// Set global trace flag.
|
||||
globalTrace = os.Getenv("MINIO_TRACE") == "1"
|
||||
|
||||
// It is an unsafe practice to run network services as
|
||||
// root. Containers are an exception.
|
||||
if !isContainerized() && os.Geteuid() == 0 {
|
||||
console.Fatalln("Please run ‘minio’ as a non-root user.")
|
||||
}
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
Fatalln("Please run ‘minio’ as a non-root user.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func migrate() {
|
||||
// Migrate config file
|
||||
migrateConfig()
|
||||
|
||||
// Migrate other configs here.
|
||||
}
|
||||
|
||||
// Tries to get os/arch/platform specific information
|
||||
// Returns a map of current os/arch/platform/memstats
|
||||
func getSystemData() map[string]string {
|
||||
host, err := os.Hostname()
|
||||
if err != nil {
|
||||
host = ""
|
||||
}
|
||||
memstats := &runtime.MemStats{}
|
||||
runtime.ReadMemStats(memstats)
|
||||
mem := fmt.Sprintf("Used: %s | Allocated: %s | Used-Heap: %s | Allocated-Heap: %s",
|
||||
humanize.Bytes(memstats.Alloc),
|
||||
humanize.Bytes(memstats.TotalAlloc),
|
||||
humanize.Bytes(memstats.HeapAlloc),
|
||||
humanize.Bytes(memstats.HeapSys))
|
||||
platform := fmt.Sprintf("Host: %s | OS: %s | Arch: %s",
|
||||
host,
|
||||
runtime.GOOS,
|
||||
runtime.GOARCH)
|
||||
goruntime := fmt.Sprintf("Version: %s | CPUs: %s", runtime.Version(), strconv.Itoa(runtime.NumCPU()))
|
||||
return map[string]string{
|
||||
"PLATFORM": platform,
|
||||
"RUNTIME": goruntime,
|
||||
"MEM": mem,
|
||||
}
|
||||
func enableLoggers() {
|
||||
// Enable all loggers here.
|
||||
enableConsoleLogger()
|
||||
enableFileLogger()
|
||||
|
||||
// Add your logger here.
|
||||
}
|
||||
|
||||
func findClosestCommands(command string) []string {
|
||||
@@ -101,34 +92,39 @@ func findClosestCommands(command string) []string {
|
||||
for _, value := range commandsTree.PrefixMatch(command) {
|
||||
closestCommands = append(closestCommands, value.(string))
|
||||
}
|
||||
sort.Strings(closestCommands)
|
||||
// Suggest other close commands - allow missed, wrongly added and
|
||||
// even transposed characters
|
||||
for _, value := range commandsTree.walk(commandsTree.root) {
|
||||
if sort.SearchStrings(closestCommands, value.(string)) < len(closestCommands) {
|
||||
continue
|
||||
}
|
||||
// 2 is arbitrary and represents the max
|
||||
// allowed number of typed errors
|
||||
if DamerauLevenshteinDistance(command, value.(string)) < 2 {
|
||||
closestCommands = append(closestCommands, value.(string))
|
||||
}
|
||||
}
|
||||
return closestCommands
|
||||
}
|
||||
|
||||
func registerApp() *cli.App {
|
||||
// register all commands
|
||||
// Register all commands.
|
||||
registerCommand(serverCmd)
|
||||
registerCommand(configCmd)
|
||||
registerCommand(versionCmd)
|
||||
registerCommand(updateCmd)
|
||||
|
||||
// register all flags
|
||||
registerFlag(configFolderFlag)
|
||||
registerFlag(addressFlag)
|
||||
registerFlag(accessLogFlag)
|
||||
registerFlag(certFlag)
|
||||
registerFlag(keyFlag)
|
||||
|
||||
// set up app
|
||||
// Set up app.
|
||||
app := cli.NewApp()
|
||||
app.Name = "Minio"
|
||||
app.Author = "Minio.io"
|
||||
app.Usage = "Cloud Storage Server for Micro Services."
|
||||
app.Description = `Micro services environment provisions one Minio server per application instance. Scalability is achieved through large number of smaller personalized instances. This version of the Minio binary is built using Filesystem storage backend for magnetic and solid state disks.`
|
||||
app.Flags = flags
|
||||
app.Usage = "Cloud Storage Server."
|
||||
app.Description = `Minio is an Amazon S3 compatible object storage server. Use it to store photos, videos, VMs, containers, log files, or any blob of data as objects.`
|
||||
app.Flags = append(minioFlags, globalFlags...)
|
||||
app.Commands = commands
|
||||
app.CustomAppHelpTemplate = minioHelpTemplate
|
||||
app.CommandNotFound = func(ctx *cli.Context, command string) {
|
||||
msg := fmt.Sprintf("‘%s’ is not a minio sub-command. See ‘minio help’.", command)
|
||||
msg := fmt.Sprintf("‘%s’ is not a minio sub-command. See ‘minio --help’.", command)
|
||||
closestCommands := findClosestCommands(command)
|
||||
if len(closestCommands) > 0 {
|
||||
msg += fmt.Sprintf("\n\nDid you mean one of these?\n")
|
||||
@@ -136,7 +132,7 @@ func registerApp() *cli.App {
|
||||
msg += fmt.Sprintf(" ‘%s’\n", cmd)
|
||||
}
|
||||
}
|
||||
Fatalln(msg)
|
||||
console.Fatalln(msg)
|
||||
}
|
||||
return app
|
||||
}
|
||||
@@ -144,25 +140,23 @@ func registerApp() *cli.App {
|
||||
func checkMainSyntax(c *cli.Context) {
|
||||
configPath, err := getConfigPath()
|
||||
if err != nil {
|
||||
Fatalf("Unable to obtain user's home directory. \nError: %s\n", err)
|
||||
console.Fatalf("Unable to obtain user's home directory. \nError: %s\n", err)
|
||||
}
|
||||
if configPath == "" {
|
||||
Fatalf("Config folder cannot be empty, please specify --config-folder <foldername>.")
|
||||
console.Fatalln("Config folder cannot be empty, please specify --config-dir <foldername>.")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
probe.Init() // Set project's root source path.
|
||||
probe.SetAppInfo("Release-Tag", minioReleaseTag)
|
||||
probe.SetAppInfo("Commit-ID", minioShortCommitID)
|
||||
if os.Getenv("DOCKERIMAGE") == "1" {
|
||||
probe.SetAppInfo("Docker-Image", "true")
|
||||
}
|
||||
// mustGetProfilePath must get location that the profile will be written to.
|
||||
func mustGetProfilePath() string {
|
||||
return filepath.Join(mustGetConfigPath(), globalMinioProfilePath)
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := registerApp()
|
||||
app.Before = func(c *cli.Context) error {
|
||||
// Sets new config folder.
|
||||
setGlobalConfigPath(c.GlobalString("config-folder"))
|
||||
setGlobalConfigPath(c.GlobalString("config-dir"))
|
||||
|
||||
// Valid input arguments to main.
|
||||
checkMainSyntax(c)
|
||||
@@ -170,11 +164,44 @@ func main() {
|
||||
// Migrate any old version of config / state files to newer format.
|
||||
migrate()
|
||||
|
||||
// Initialize config.
|
||||
err := initConfig()
|
||||
fatalIf(err, "Unable to initialize minio config.")
|
||||
|
||||
// Enable all loggers by now.
|
||||
enableLoggers()
|
||||
|
||||
// Initialize name space lock.
|
||||
initNSLock()
|
||||
|
||||
// Set global quiet flag.
|
||||
globalQuiet = c.Bool("quiet") || c.GlobalBool("quiet")
|
||||
|
||||
// Do not print update messages, if quiet flag is set.
|
||||
if !globalQuiet {
|
||||
// Do not print any errors in release update function.
|
||||
noError := true
|
||||
updateMsg := getReleaseUpdate(minioUpdateStableURL, noError)
|
||||
if updateMsg.Update {
|
||||
console.Println(updateMsg)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
app.ExtraInfo = func() map[string]string {
|
||||
return getSystemData()
|
||||
|
||||
// Set ``MINIO_PROFILE_DIR`` to the directory where profiling information should be persisted
|
||||
profileDir := os.Getenv("MINIO_PROFILE_DIR")
|
||||
// Enable profiler if ``MINIO_PROFILER`` is set. Supported options are [cpu, mem, block].
|
||||
switch os.Getenv("MINIO_PROFILER") {
|
||||
case "cpu":
|
||||
defer profile.Start(profile.CPUProfile, profile.ProfilePath(profileDir)).Stop()
|
||||
case "mem":
|
||||
defer profile.Start(profile.MemProfile, profile.ProfilePath(profileDir)).Stop()
|
||||
case "block":
|
||||
defer profile.Start(profile.BlockProfile, profile.ProfilePath(profileDir)).Stop()
|
||||
}
|
||||
|
||||
// Run the app - exit on error.
|
||||
app.RunAndExitOnError()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// nsParam - carries name space resource.
|
||||
type nsParam struct {
|
||||
volume string
|
||||
path string
|
||||
}
|
||||
|
||||
// nsLock - provides primitives for locking critical namespace regions.
|
||||
type nsLock struct {
|
||||
*sync.RWMutex
|
||||
ref uint
|
||||
}
|
||||
|
||||
// nsLockMap - namespace lock map, provides primitives to Lock,
|
||||
// Unlock, RLock and RUnlock.
|
||||
type nsLockMap struct {
|
||||
lockMap map[nsParam]*nsLock
|
||||
mutex *sync.Mutex
|
||||
}
|
||||
|
||||
// Global name space lock.
|
||||
var nsMutex *nsLockMap
|
||||
|
||||
// initNSLock - initialize name space lock map.
|
||||
func initNSLock() {
|
||||
nsMutex = &nsLockMap{
|
||||
lockMap: make(map[nsParam]*nsLock),
|
||||
mutex: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Lock the namespace resource.
|
||||
func (n *nsLockMap) lock(volume, path string, readLock bool) {
|
||||
n.mutex.Lock()
|
||||
|
||||
param := nsParam{volume, path}
|
||||
nsLk, found := n.lockMap[param]
|
||||
if !found {
|
||||
nsLk = &nsLock{
|
||||
RWMutex: &sync.RWMutex{},
|
||||
ref: 0,
|
||||
}
|
||||
n.lockMap[param] = nsLk
|
||||
}
|
||||
nsLk.ref++ // Update ref count here to avoid multiple races.
|
||||
// Unlock map before Locking NS which might block.
|
||||
n.mutex.Unlock()
|
||||
|
||||
// Locking here can block.
|
||||
if readLock {
|
||||
nsLk.RLock()
|
||||
} else {
|
||||
nsLk.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock the namespace resource.
|
||||
func (n *nsLockMap) unlock(volume, path string, readLock bool) {
|
||||
// nsLk.Unlock() will not block, hence locking the map for the entire function is fine.
|
||||
n.mutex.Lock()
|
||||
defer n.mutex.Unlock()
|
||||
|
||||
param := nsParam{volume, path}
|
||||
if nsLk, found := n.lockMap[param]; found {
|
||||
if readLock {
|
||||
nsLk.RUnlock()
|
||||
} else {
|
||||
nsLk.Unlock()
|
||||
}
|
||||
if nsLk.ref == 0 {
|
||||
errorIf(errors.New("Namespace reference count cannot be 0."), "Invalid reference count detected.")
|
||||
}
|
||||
if nsLk.ref != 0 {
|
||||
nsLk.ref--
|
||||
}
|
||||
if nsLk.ref == 0 {
|
||||
// Remove from the map if there are no more references.
|
||||
delete(n.lockMap, param)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lock - locks the given resource for writes, using a previously
|
||||
// allocated name space lock or initializing a new one.
|
||||
func (n *nsLockMap) Lock(volume, path string) {
|
||||
readLock := false
|
||||
n.lock(volume, path, readLock)
|
||||
}
|
||||
|
||||
// Unlock - unlocks any previously acquired write locks.
|
||||
func (n *nsLockMap) Unlock(volume, path string) {
|
||||
readLock := false
|
||||
n.unlock(volume, path, readLock)
|
||||
}
|
||||
|
||||
// RLock - locks any previously acquired read locks.
|
||||
func (n *nsLockMap) RLock(volume, path string) {
|
||||
readLock := true
|
||||
n.lock(volume, path, readLock)
|
||||
}
|
||||
|
||||
// RUnlock - unlocks any previously acquired read locks.
|
||||
func (n *nsLockMap) RUnlock(volume, path string) {
|
||||
readLock := true
|
||||
n.unlock(volume, path, readLock)
|
||||
}
|
||||
+18
-25
@@ -18,69 +18,62 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/minio-xl/pkg/probe"
|
||||
"github.com/olekukonko/ts"
|
||||
)
|
||||
|
||||
// colorizeUpdateMessage - inspired from Yeoman project npm package https://github.com/yeoman/update-notifier
|
||||
func colorizeUpdateMessage(updateString string) (string, *probe.Error) {
|
||||
// initialize coloring
|
||||
func colorizeUpdateMessage(updateString string) (string, error) {
|
||||
// Initialize coloring.
|
||||
cyan := color.New(color.FgCyan, color.Bold).SprintFunc()
|
||||
yellow := color.New(color.FgYellow, color.Bold).SprintfFunc()
|
||||
|
||||
// calculate length without color coding, due to ANSI color characters padded to actual
|
||||
// string the final length is wrong than the original string length
|
||||
line1Str := fmt.Sprintf(" Update available: ")
|
||||
line2Str := fmt.Sprintf(" Run \"%s\" to update. ", updateString)
|
||||
// Calculate length without color coding, due to ANSI color
|
||||
// characters padded to actual string the final length is wrong
|
||||
// than the original string length.
|
||||
line1Str := fmt.Sprintf(" New update: %s ", updateString)
|
||||
line1Length := len(line1Str)
|
||||
line2Length := len(line2Str)
|
||||
|
||||
// populate lines with color coding
|
||||
line1InColor := line1Str
|
||||
line2InColor := fmt.Sprintf(" Run \"%s\" to update. ", cyan(updateString))
|
||||
// Populate lines with color coding.
|
||||
line1InColor := fmt.Sprintf(" New update: %s ", cyan(updateString))
|
||||
|
||||
// calculate the rectangular box size
|
||||
maxContentWidth := int(math.Max(float64(line1Length), float64(line2Length)))
|
||||
// Calculate the rectangular box size.
|
||||
maxContentWidth := line1Length
|
||||
line1Rest := maxContentWidth - line1Length
|
||||
line2Rest := maxContentWidth - line2Length
|
||||
|
||||
terminal, err := ts.GetSize()
|
||||
if err != nil {
|
||||
return "", probe.NewError(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
var message string
|
||||
switch {
|
||||
case len(line2Str) > terminal.Col():
|
||||
message = "\n" + line1InColor + "\n" + line2InColor + "\n"
|
||||
case len(line1Str) > terminal.Col():
|
||||
message = "\n" + line1InColor + "\n"
|
||||
default:
|
||||
// on windows terminal turn off unicode characters
|
||||
// On windows terminal turn off unicode characters.
|
||||
var top, bottom, sideBar string
|
||||
if runtime.GOOS == "windows" {
|
||||
top = yellow("*" + strings.Repeat("*", maxContentWidth) + "*")
|
||||
bottom = yellow("*" + strings.Repeat("*", maxContentWidth) + "*")
|
||||
sideBar = yellow("|")
|
||||
} else {
|
||||
// color the rectangular box, use unicode characters here
|
||||
// Color the rectangular box, use unicode characters here.
|
||||
top = yellow("┏" + strings.Repeat("━", maxContentWidth) + "┓")
|
||||
bottom = yellow("┗" + strings.Repeat("━", maxContentWidth) + "┛")
|
||||
sideBar = yellow("┃")
|
||||
}
|
||||
// fill spaces to the rest of the area
|
||||
// Fill spaces to the rest of the area.
|
||||
spacePaddingLine1 := strings.Repeat(" ", line1Rest)
|
||||
spacePaddingLine2 := strings.Repeat(" ", line2Rest)
|
||||
|
||||
// construct the final message
|
||||
// Construct the final message.
|
||||
message = "\n" + top + "\n" +
|
||||
sideBar + line1InColor + spacePaddingLine1 + sideBar + "\n" +
|
||||
sideBar + line2InColor + spacePaddingLine2 + sideBar + "\n" +
|
||||
bottom + "\n"
|
||||
}
|
||||
// return the final message
|
||||
// Return the final message.
|
||||
return message, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* 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"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Wrapper for calling GetObjectInfo tests for both XL multiple disks and single node setup.
|
||||
func TestGetObjectInfo(t *testing.T) {
|
||||
ExecObjectLayerTest(t, testGetObjectInfo)
|
||||
}
|
||||
|
||||
// Testing GetObjectInfo().
|
||||
func testGetObjectInfo(obj ObjectLayer, instanceType string, t *testing.T) {
|
||||
// This bucket is used for testing getObjectInfo operations.
|
||||
err := obj.MakeBucket("test-getobjectinfo")
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
_, err = obj.PutObject("test-getobjectinfo", "Asia/asiapics.jpg", int64(len("asiapics")), bytes.NewBufferString("asiapics"), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
resultCases := []ObjectInfo{
|
||||
// ObjectInfo -1.
|
||||
// ObjectName set to a existing object in the test case (Test case 14).
|
||||
{Bucket: "test-getobjectinfo", Name: "Asia/asiapics.jpg", ContentType: "image/jpeg", IsDir: false},
|
||||
}
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
objectName string
|
||||
|
||||
// Expected output of GetObjectInfo.
|
||||
result ObjectInfo
|
||||
err error
|
||||
// Flag indicating whether the test is expected to pass or not.
|
||||
shouldPass bool
|
||||
}{
|
||||
// Test cases with invalid bucket names ( Test number 1-4 ).
|
||||
{".test", "", ObjectInfo{}, BucketNameInvalid{Bucket: ".test"}, false},
|
||||
{"Test", "", ObjectInfo{}, BucketNameInvalid{Bucket: "Test"}, false},
|
||||
{"---", "", ObjectInfo{}, BucketNameInvalid{Bucket: "---"}, false},
|
||||
{"ad", "", ObjectInfo{}, BucketNameInvalid{Bucket: "ad"}, false},
|
||||
// Test cases with valid but non-existing bucket names (Test number 5-7).
|
||||
{"abcdefgh", "abc", ObjectInfo{}, BucketNotFound{Bucket: "abcdefgh"}, false},
|
||||
{"ijklmnop", "efg", ObjectInfo{}, BucketNotFound{Bucket: "ijklmnop"}, false},
|
||||
// Test cases with valid but non-existing bucket names and invalid object name (Test number 8-9).
|
||||
{"test-getobjectinfo", "", ObjectInfo{}, ObjectNameInvalid{Bucket: "test-getobjectinfo", Object: ""}, false},
|
||||
{"test-getobjectinfo", "", ObjectInfo{}, ObjectNameInvalid{Bucket: "test-getobjectinfo", Object: ""}, false},
|
||||
// Test cases with non-existing object name with existing bucket (Test number 10-12).
|
||||
{"test-getobjectinfo", "Africa", ObjectInfo{}, ObjectNotFound{Bucket: "test-getobjectinfo", Object: "Africa"}, false},
|
||||
{"test-getobjectinfo", "Antartica", ObjectInfo{}, ObjectNotFound{Bucket: "test-getobjectinfo", Object: "Antartica"}, false},
|
||||
{"test-getobjectinfo", "Asia/myfile", ObjectInfo{}, ObjectNotFound{Bucket: "test-getobjectinfo", Object: "Asia/myfile"}, false},
|
||||
// Test case with existing bucket but object name set to a directory (Test number 13).
|
||||
{"test-getobjectinfo", "Asia", ObjectInfo{}, ObjectNotFound{Bucket: "test-getobjectinfo", Object: "Asia"}, false},
|
||||
// Valid case with existing object (Test number 14).
|
||||
{"test-getobjectinfo", "Asia/asiapics.jpg", resultCases[0], nil, true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
result, err := obj.GetObjectInfo(testCase.bucketName, testCase.objectName)
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: %s: Expected to pass, but failed with: <ERROR> %s", i+1, instanceType, err.Error())
|
||||
}
|
||||
if err == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: %s: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, instanceType, testCase.err.Error())
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if err != nil && !testCase.shouldPass {
|
||||
if testCase.err.Error() != err.Error() {
|
||||
t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, instanceType, 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.result.Bucket != result.Bucket {
|
||||
t.Fatalf("Test %d: %s: Expected Bucket name to be '%s', but found '%s' instead", i+1, instanceType, testCase.result.Bucket, result.Bucket)
|
||||
}
|
||||
if testCase.result.Name != result.Name {
|
||||
t.Errorf("Test %d: %s: Expected Object name to be %s, but instead found it to be %s", i+1, instanceType, testCase.result.Name, result.Name)
|
||||
}
|
||||
if testCase.result.ContentType != result.ContentType {
|
||||
t.Errorf("Test %d: %s: Expected Content Type of the object to be %v, but instead found it to be %v", i+1, instanceType, testCase.result.ContentType, result.ContentType)
|
||||
}
|
||||
if testCase.result.IsDir != result.IsDir {
|
||||
t.Errorf("Test %d: %s: Expected IsDir flag of the object to be %v, but instead found it to be %v", i+1, instanceType, testCase.result.IsDir, result.IsDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetObjectFS(b *testing.B) {
|
||||
// Make a temporary directory to use as the obj.
|
||||
directory, err := ioutil.TempDir("", "minio-benchmark-getobject")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(directory)
|
||||
|
||||
// Create the obj.
|
||||
obj, err := newFSObjects(directory)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// Make a bucket and put in a few objects.
|
||||
err = obj.MakeBucket("bucket")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
text := "Jack and Jill went up the hill / To fetch a pail of water."
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(text))
|
||||
metadata := make(map[string]string)
|
||||
for i := 0; i < 10; i++ {
|
||||
metadata["md5Sum"] = hex.EncodeToString(hasher.Sum(nil))
|
||||
_, err = obj.PutObject("bucket", "object"+strconv.Itoa(i), int64(len(text)), bytes.NewBufferString(text), metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
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(len([]byte(text))), buffer)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
if buffer.Len() != len(text) {
|
||||
b.Errorf("GetObject returned incorrect length %d (should be %d)\n", buffer.Len(), len(text))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,611 @@
|
||||
/*
|
||||
* 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"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Wrapper for calling ListObjects tests for both XL multiple disks and single node setup.
|
||||
func TestListObjects(t *testing.T) {
|
||||
ExecObjectLayerTest(t, testListObjects)
|
||||
}
|
||||
|
||||
// Unit test for ListObjects in general.
|
||||
func testListObjects(obj ObjectLayer, instanceType string, t *testing.T) {
|
||||
|
||||
// This bucket is used for testing ListObject operations.
|
||||
err := obj.MakeBucket("test-bucket-list-object")
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
// Will not store any objects in this bucket,
|
||||
// Its to test ListObjects on an empty bucket.
|
||||
err = obj.MakeBucket("empty-bucket")
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
|
||||
tmpfile, err := ioutil.TempFile("", "simple-file.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
defer os.Remove(tmpfile.Name()) // clean up
|
||||
|
||||
_, err = obj.PutObject("test-bucket-list-object", "Asia-maps", int64(len("asia-maps")), bytes.NewBufferString("asia-maps"), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
|
||||
_, err = obj.PutObject("test-bucket-list-object", "Asia/India/India-summer-photos-1", int64(len("contentstring")), bytes.NewBufferString("contentstring"), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
|
||||
_, err = obj.PutObject("test-bucket-list-object", "Asia/India/Karnataka/Bangalore/Koramangala/pics", int64(len("contentstring")), bytes.NewBufferString("contentstring"), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
key := "newPrefix" + strconv.Itoa(i)
|
||||
_, err = obj.PutObject("test-bucket-list-object", key, int64(len(key)), bytes.NewBufferString(key), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
}
|
||||
_, err = obj.PutObject("test-bucket-list-object", "newzen/zen/recurse/again/again/again/pics", int64(len("recurse")), bytes.NewBufferString("recurse"), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
key := "obj" + strconv.Itoa(i)
|
||||
_, err = obj.PutObject("test-bucket-list-object", key, int64(len(key)), bytes.NewBufferString(key), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Formualting the result data set to be expected from ListObjects call inside the tests,
|
||||
// This will be used in testCases and used for asserting the correctness of ListObjects output in the tests.
|
||||
|
||||
resultCases := []ListObjectsInfo{
|
||||
// ListObjectsResult-0.
|
||||
// Testing for listing all objects in the bucket, (testCase 20,21,22).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "Asia-maps"},
|
||||
{Name: "Asia/India/India-summer-photos-1"},
|
||||
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||
{Name: "newPrefix0"},
|
||||
{Name: "newPrefix1"},
|
||||
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||
{Name: "obj0"},
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-1.
|
||||
// Used for asserting the truncated case, (testCase 23).
|
||||
{
|
||||
IsTruncated: true,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "Asia-maps"},
|
||||
{Name: "Asia/India/India-summer-photos-1"},
|
||||
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||
{Name: "newPrefix0"},
|
||||
{Name: "newPrefix1"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-2.
|
||||
// (TestCase 24).
|
||||
{
|
||||
IsTruncated: true,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "Asia-maps"},
|
||||
{Name: "Asia/India/India-summer-photos-1"},
|
||||
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||
{Name: "newPrefix0"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-3.
|
||||
// (TestCase 25).
|
||||
{
|
||||
IsTruncated: true,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "Asia-maps"},
|
||||
{Name: "Asia/India/India-summer-photos-1"},
|
||||
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-4.
|
||||
// Again used for truncated case.
|
||||
// (TestCase 26).
|
||||
{
|
||||
IsTruncated: true,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "Asia-maps"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-5.
|
||||
// Used for Asserting prefixes.
|
||||
// Used for test case with prefix "new", (testCase 27-29).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "newPrefix0"},
|
||||
{Name: "newPrefix1"},
|
||||
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-6.
|
||||
// Used for Asserting prefixes.
|
||||
// Used for test case with prefix = "obj", (testCase 30).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "obj0"},
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-7.
|
||||
// Used for Asserting prefixes and truncation.
|
||||
// Used for test case with prefix = "new" and maxKeys = 1, (testCase 31).
|
||||
{
|
||||
IsTruncated: true,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "newPrefix0"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-8.
|
||||
// Used for Asserting prefixes.
|
||||
// Used for test case with prefix = "obj" and maxKeys = 2, (testCase 32).
|
||||
{
|
||||
IsTruncated: true,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "obj0"},
|
||||
{Name: "obj1"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-9.
|
||||
// Used for asserting the case with marker, but without prefix.
|
||||
//marker is set to "newPrefix0" in the testCase, (testCase 33).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "newPrefix1"},
|
||||
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||
{Name: "obj0"},
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-10.
|
||||
//marker is set to "newPrefix1" in the testCase, (testCase 34).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||
{Name: "obj0"},
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-11.
|
||||
//marker is set to "obj0" in the testCase, (testCase 35).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-12.
|
||||
// Marker is set to "obj1" in the testCase, (testCase 36).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-13.
|
||||
// Marker is set to "man" in the testCase, (testCase37).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "newPrefix0"},
|
||||
{Name: "newPrefix1"},
|
||||
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||
{Name: "obj0"},
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-14.
|
||||
// Marker is set to "Abc" in the testCase, (testCase 39).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "Asia-maps"},
|
||||
{Name: "Asia/India/India-summer-photos-1"},
|
||||
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||
{Name: "newPrefix0"},
|
||||
{Name: "newPrefix1"},
|
||||
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||
{Name: "obj0"},
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-15.
|
||||
// Marker is set to "Asia/India/India-summer-photos-1" in the testCase, (testCase 40).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||
{Name: "newPrefix0"},
|
||||
{Name: "newPrefix1"},
|
||||
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||
{Name: "obj0"},
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-16.
|
||||
// Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase, (testCase 41).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "newPrefix0"},
|
||||
{Name: "newPrefix1"},
|
||||
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||
{Name: "obj0"},
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-17.
|
||||
// Used for asserting the case with marker, without prefix but with truncation.
|
||||
// Marker = "newPrefix0" & maxKeys = 3 in the testCase, (testCase42).
|
||||
// Output truncated to 3 values.
|
||||
{
|
||||
IsTruncated: true,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "newPrefix1"},
|
||||
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||
{Name: "obj0"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-18.
|
||||
// Marker = "newPrefix1" & maxkeys = 1 in the testCase, (testCase43).
|
||||
// Output truncated to 1 value.
|
||||
{
|
||||
IsTruncated: true,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-19.
|
||||
// Marker = "obj0" & maxKeys = 1 in the testCase, (testCase44).
|
||||
// Output truncated to 1 value.
|
||||
{
|
||||
IsTruncated: true,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "obj1"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-20.
|
||||
// Marker = "obj0" & prefix = "obj" in the testCase, (testCase 45).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-21.
|
||||
// Marker = "obj1" & prefix = "obj" in the testCase, (testCase 46).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-22.
|
||||
// Marker = "newPrefix0" & prefix = "new" in the testCase,, (testCase 47).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "newPrefix1"},
|
||||
{Name: "newzen/zen/recurse/again/again/again/pics"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-23.
|
||||
// Prefix is set to "Asia/India/" in the testCase, and delimiter is not set (testCase 55).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "Asia/India/India-summer-photos-1"},
|
||||
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||
},
|
||||
},
|
||||
|
||||
// ListObjectsResult-24.
|
||||
// Prefix is set to "Asia" in the testCase, and delimiter is not set (testCase 56).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "Asia-maps"},
|
||||
{Name: "Asia/India/India-summer-photos-1"},
|
||||
{Name: "Asia/India/Karnataka/Bangalore/Koramangala/pics"},
|
||||
},
|
||||
},
|
||||
|
||||
// ListObjectsResult-25.
|
||||
// Prefix is set to "Asia" in the testCase, and delimiter is set (testCase 57).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "Asia-maps"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-26.
|
||||
// prefix = "new" and delimiter is set in the testCase.(testCase 58).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "newPrefix0"},
|
||||
{Name: "newPrefix1"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-27.
|
||||
// Prefix is set to "Asia/India/" in the testCase, and delimiter is set to forward slash '/' (testCase 59).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "Asia/India/India-summer-photos-1"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-28.
|
||||
// Marker is set to "Asia/India/India-summer-photos-1" and delimiter set in the testCase, (testCase 60).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "newPrefix0"},
|
||||
{Name: "newPrefix1"},
|
||||
{Name: "obj0"},
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-29.
|
||||
// Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase and delimeter set, (testCase 61).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "newPrefix0"},
|
||||
{Name: "newPrefix1"},
|
||||
{Name: "obj0"},
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-30.
|
||||
// Prefix and Delimiter is set to '/', (testCase 62).
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
// Inputs to ListObjects.
|
||||
bucketName string
|
||||
prefix string
|
||||
marker string
|
||||
delimeter string
|
||||
maxKeys int
|
||||
// Expected output of ListObjects.
|
||||
result ListObjectsInfo
|
||||
err error
|
||||
// Flag indicating whether the test is expected to pass or not.
|
||||
shouldPass bool
|
||||
}{
|
||||
// Test cases with invalid bucket names ( Test number 1-4 ).
|
||||
{".test", "", "", "", 0, ListObjectsInfo{}, BucketNameInvalid{Bucket: ".test"}, false},
|
||||
{"Test", "", "", "", 0, ListObjectsInfo{}, BucketNameInvalid{Bucket: "Test"}, false},
|
||||
{"---", "", "", "", 0, ListObjectsInfo{}, BucketNameInvalid{Bucket: "---"}, false},
|
||||
{"ad", "", "", "", 0, ListObjectsInfo{}, BucketNameInvalid{Bucket: "ad"}, false},
|
||||
// Using an existing file for bucket name, but its not a directory (5).
|
||||
{"simple-file.txt", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "simple-file.txt"}, false},
|
||||
// Valid bucket names, but they donot exist (6-8).
|
||||
{"volatile-bucket-1", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-1"}, false},
|
||||
{"volatile-bucket-2", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-2"}, false},
|
||||
{"volatile-bucket-3", "", "", "", 0, ListObjectsInfo{}, BucketNotFound{Bucket: "volatile-bucket-3"}, false},
|
||||
// Valid, existing bucket, but sending invalid delimeter values (9-10).
|
||||
// Empty string < "" > and forward slash < / > are the ony two valid arguments for delimeter.
|
||||
{"test-bucket-list-object", "", "", "*", 0, ListObjectsInfo{}, fmt.Errorf("delimiter '%s' is not supported", "*"), false},
|
||||
{"test-bucket-list-object", "", "", "-", 0, ListObjectsInfo{}, fmt.Errorf("delimiter '%s' is not supported", "-"), false},
|
||||
// Testing for failure cases with both perfix and marker (13).
|
||||
// The prefix and marker combination to be valid it should satisy strings.HasPrefix(marker, prefix).
|
||||
{"test-bucket-list-object", "asia", "europe-object", "", 0, ListObjectsInfo{}, fmt.Errorf("Invalid combination of marker '%s' and prefix '%s'", "europe-object", "asia"), false},
|
||||
// Setting a non-existing directory to be prefix (14-15).
|
||||
{"empty-bucket", "europe/france/", "", "", 1, ListObjectsInfo{}, nil, true},
|
||||
{"empty-bucket", "europe/tunisia/", "", "", 1, ListObjectsInfo{}, nil, true},
|
||||
// Testing on empty bucket, that is, bucket without any objects in it (16).
|
||||
{"empty-bucket", "", "", "", 0, ListObjectsInfo{}, nil, true},
|
||||
// Setting maxKeys to negative value (17-18).
|
||||
{"empty-bucket", "", "", "", -1, ListObjectsInfo{}, nil, true},
|
||||
{"empty-bucket", "", "", "", 1, ListObjectsInfo{}, nil, true},
|
||||
// Setting maxKeys to a very large value (19).
|
||||
{"empty-bucket", "", "", "", 1111000000000000, ListObjectsInfo{}, nil, true},
|
||||
// Testing for all 7 objects in the bucket (20).
|
||||
{"test-bucket-list-object", "", "", "", 9, resultCases[0], nil, true},
|
||||
//Testing for negative value of maxKey, this should set maxKeys to listObjectsLimit (21).
|
||||
{"test-bucket-list-object", "", "", "", -1, resultCases[0], nil, true},
|
||||
// Testing for very large value of maxKey, this should set maxKeys to listObjectsLimit (22).
|
||||
{"test-bucket-list-object", "", "", "", 1234567891011, resultCases[0], nil, true},
|
||||
// Testing for trancated value (23-26).
|
||||
{"test-bucket-list-object", "", "", "", 5, resultCases[1], nil, true},
|
||||
{"test-bucket-list-object", "", "", "", 4, resultCases[2], nil, true},
|
||||
{"test-bucket-list-object", "", "", "", 3, resultCases[3], nil, true},
|
||||
{"test-bucket-list-object", "", "", "", 1, resultCases[4], nil, true},
|
||||
// Testing with prefix (27-30).
|
||||
{"test-bucket-list-object", "new", "", "", 3, resultCases[5], nil, true},
|
||||
{"test-bucket-list-object", "new", "", "", 4, resultCases[5], nil, true},
|
||||
{"test-bucket-list-object", "new", "", "", 5, resultCases[5], nil, true},
|
||||
{"test-bucket-list-object", "obj", "", "", 3, resultCases[6], nil, true},
|
||||
// Testing with prefix and truncation (31-32).
|
||||
{"test-bucket-list-object", "new", "", "", 1, resultCases[7], nil, true},
|
||||
{"test-bucket-list-object", "obj", "", "", 2, resultCases[8], nil, true},
|
||||
// Testing with marker, but without prefix and truncation (33-37).
|
||||
{"test-bucket-list-object", "", "newPrefix0", "", 5, resultCases[9], nil, true},
|
||||
{"test-bucket-list-object", "", "newPrefix1", "", 4, resultCases[10], nil, true},
|
||||
{"test-bucket-list-object", "", "obj0", "", 2, resultCases[11], nil, true},
|
||||
{"test-bucket-list-object", "", "obj1", "", 1, resultCases[12], nil, true},
|
||||
{"test-bucket-list-object", "", "man", "", 10, resultCases[13], nil, true},
|
||||
// Marker being set to a value which is greater than and all object names when sorted (38).
|
||||
// Expected to send an empty response in this case.
|
||||
{"test-bucket-list-object", "", "zen", "", 10, ListObjectsInfo{}, nil, true},
|
||||
// Marker being set to a value which is lesser than and all object names when sorted (39).
|
||||
// Expected to send all the objects in the bucket in this case.
|
||||
{"test-bucket-list-object", "", "Abc", "", 10, resultCases[14], nil, true},
|
||||
// Marker is to a hierarhical value (40-41).
|
||||
{"test-bucket-list-object", "", "Asia/India/India-summer-photos-1", "", 10, resultCases[15], nil, true},
|
||||
{"test-bucket-list-object", "", "Asia/India/Karnataka/Bangalore/Koramangala/pics", "", 10, resultCases[16], nil, true},
|
||||
// Testing with marker and truncation, but no prefix (42-44).
|
||||
{"test-bucket-list-object", "", "newPrefix0", "", 3, resultCases[17], nil, true},
|
||||
{"test-bucket-list-object", "", "newPrefix1", "", 1, resultCases[18], nil, true},
|
||||
{"test-bucket-list-object", "", "obj0", "", 1, resultCases[19], nil, true},
|
||||
// Testing with both marker and prefix, but without truncation (45-47).
|
||||
// The valid combination of marker and prefix should satisfy strings.HasPrefix(marker, prefix).
|
||||
{"test-bucket-list-object", "obj", "obj0", "", 2, resultCases[20], nil, true},
|
||||
{"test-bucket-list-object", "obj", "obj1", "", 1, resultCases[21], nil, true},
|
||||
{"test-bucket-list-object", "new", "newPrefix0", "", 2, resultCases[22], nil, true},
|
||||
// Testing with maxKeys set to 0 (48-54).
|
||||
// The parameters have to valid.
|
||||
{"test-bucket-list-object", "", "obj1", "", 0, ListObjectsInfo{}, nil, true},
|
||||
{"test-bucket-list-object", "", "obj0", "", 0, ListObjectsInfo{}, nil, true},
|
||||
{"test-bucket-list-object", "new", "", "", 0, ListObjectsInfo{}, nil, true},
|
||||
{"test-bucket-list-object", "obj", "", "", 0, ListObjectsInfo{}, nil, true},
|
||||
{"test-bucket-list-object", "obj", "obj0", "", 0, ListObjectsInfo{}, nil, true},
|
||||
{"test-bucket-list-object", "obj", "obj1", "", 0, ListObjectsInfo{}, nil, true},
|
||||
{"test-bucket-list-object", "new", "newPrefix0", "", 0, ListObjectsInfo{}, nil, true},
|
||||
// Tests on hierarchical key names as prefix.
|
||||
// Without delimteter the code should recurse into the prefix Dir.
|
||||
// Tests with prefix, but without delimiter (55-56).
|
||||
{"test-bucket-list-object", "Asia/India/", "", "", 10, resultCases[23], nil, true},
|
||||
{"test-bucket-list-object", "Asia", "", "", 10, resultCases[24], nil, true},
|
||||
// Tests with prefix and delimiter (57-59).
|
||||
// With delimeter the code shouldnot recurse into the sub-directories of prefix Dir.
|
||||
{"test-bucket-list-object", "Asia", "", "/", 10, resultCases[25], nil, true},
|
||||
{"test-bucket-list-object", "new", "", "/", 10, resultCases[26], nil, true},
|
||||
{"test-bucket-list-object", "Asia/India/", "", "/", 10, resultCases[27], nil, true},
|
||||
// Test with marker set as hierarhical value and with delimiter. (60-61)
|
||||
{"test-bucket-list-object", "", "Asia/India/India-summer-photos-1", "/", 10, resultCases[28], nil, true},
|
||||
{"test-bucket-list-object", "", "Asia/India/Karnataka/Bangalore/Koramangala/pics", "/", 10, resultCases[29], nil, true},
|
||||
// Test with prefix and delimiter set to '/'. (62)
|
||||
{"test-bucket-list-object", "/", "", "/", 10, resultCases[30], nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
result, err := obj.ListObjects(testCase.bucketName, testCase.prefix, testCase.marker, testCase.delimeter, testCase.maxKeys)
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: %s: Expected to pass, but failed with: <ERROR> %s", i+1, instanceType, err.Error())
|
||||
}
|
||||
if err == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: %s: Expected to fail with <ERROR> \"%s\", but passed instead", i+1, instanceType, testCase.err.Error())
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if err != nil && !testCase.shouldPass {
|
||||
if !strings.Contains(err.Error(), testCase.err.Error()) {
|
||||
t.Errorf("Test %d: %s: Expected to fail with error \"%s\", but instead failed with error \"%s\" instead", i+1, instanceType, testCase.err.Error(), err.Error())
|
||||
}
|
||||
}
|
||||
// Since there are cases for which ListObjects fails, this is
|
||||
// necessary. Test passes as expected, but the output values
|
||||
// are verified for correctness here.
|
||||
if err == nil && testCase.shouldPass {
|
||||
// The length of the expected ListObjectsResult.Objects
|
||||
// should match in both expected result from test cases
|
||||
// and in the output. On failure calling t.Fatalf,
|
||||
// otherwise it may lead to index out of range error in
|
||||
// assertion following this.
|
||||
if len(testCase.result.Objects) != len(result.Objects) {
|
||||
t.Fatalf("Test %d: %s: Expected number of object in the result to be '%d', but found '%d' objects instead", i+1, instanceType, len(testCase.result.Objects), len(result.Objects))
|
||||
}
|
||||
for j := 0; j < len(testCase.result.Objects); j++ {
|
||||
if testCase.result.Objects[j].Name != result.Objects[j].Name {
|
||||
t.Errorf("Test %d: %s: Expected object name to be \"%s\", but found \"%s\" instead", i+1, instanceType, testCase.result.Objects[j].Name, result.Objects[j].Name)
|
||||
}
|
||||
}
|
||||
if testCase.result.IsTruncated != result.IsTruncated {
|
||||
t.Errorf("Test %d: %s: Expected IsTruncated flag to be %v, but instead found it to be %v", i+1, instanceType, testCase.result.IsTruncated, result.IsTruncated)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkListObjects(b *testing.B) {
|
||||
// Make a temporary directory to use as the obj.
|
||||
directory, err := ioutil.TempDir("", "minio-list-benchmark")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(directory)
|
||||
|
||||
// Create the obj.
|
||||
obj, err := newFSObjects(directory)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a bucket.
|
||||
err = obj.MakeBucket("ls-benchmark-bucket")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 0; i < 20000; i++ {
|
||||
key := "obj" + strconv.Itoa(i)
|
||||
_, err = obj.PutObject("ls-benchmark-bucket", key, int64(len(key)), bytes.NewBufferString(key), nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
// List the buckets over and over and over.
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err = obj.ListObjects("ls-benchmark-bucket", "", "obj9000", "", -1)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
@@ -14,31 +14,54 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package fs
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) { TestingT(t) }
|
||||
|
||||
type MySuite struct{}
|
||||
|
||||
var _ = Suite(&MySuite{})
|
||||
|
||||
func (s *MySuite) TestAPISuite(c *C) {
|
||||
func (s *MySuite) TestFSAPISuite(c *C) {
|
||||
var storageList []string
|
||||
create := func() Filesystem {
|
||||
path, e := ioutil.TempDir(os.TempDir(), "minio-")
|
||||
c.Check(e, IsNil)
|
||||
storageList = append(storageList, path)
|
||||
store, err := New(path, 0)
|
||||
|
||||
// Initialize name space lock.
|
||||
initNSLock()
|
||||
|
||||
create := func() ObjectLayer {
|
||||
path, err := ioutil.TempDir(os.TempDir(), "minio-")
|
||||
c.Check(err, IsNil)
|
||||
return store
|
||||
objAPI, err := newFSObjects(path)
|
||||
c.Check(err, IsNil)
|
||||
storageList = append(storageList, path)
|
||||
return objAPI
|
||||
}
|
||||
APITestSuite(c, create)
|
||||
defer removeRoots(c, storageList)
|
||||
}
|
||||
|
||||
func (s *MySuite) TestXLAPISuite(c *C) {
|
||||
var storageList []string
|
||||
|
||||
// Initialize name space lock.
|
||||
initNSLock()
|
||||
|
||||
create := func() ObjectLayer {
|
||||
var nDisks = 16 // Maximum disks.
|
||||
var erasureDisks []string
|
||||
for i := 0; i < nDisks; i++ {
|
||||
path, err := ioutil.TempDir(os.TempDir(), "minio-")
|
||||
c.Check(err, IsNil)
|
||||
erasureDisks = append(erasureDisks, path)
|
||||
}
|
||||
objAPI, err := newXLObjects(erasureDisks)
|
||||
c.Check(err, IsNil)
|
||||
return objAPI
|
||||
}
|
||||
APITestSuite(c, create)
|
||||
defer removeRoots(c, storageList)
|
||||
@@ -46,7 +69,6 @@ func (s *MySuite) TestAPISuite(c *C) {
|
||||
|
||||
func removeRoots(c *C, roots []string) {
|
||||
for _, root := range roots {
|
||||
err := os.RemoveAll(root)
|
||||
c.Check(err, IsNil)
|
||||
os.RemoveAll(root)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Block size used for all internal operations version 1.
|
||||
blockSizeV1 = 10 * 1024 * 1024 // 10MiB.
|
||||
)
|
||||
|
||||
// House keeping code needed for FS.
|
||||
func fsHouseKeeping(storageDisk StorageAPI) error {
|
||||
// Attempt to create `.minio`.
|
||||
err := storageDisk.MakeVol(minioMetaBucket)
|
||||
if err != nil {
|
||||
if err != errVolumeExists && err != errDiskNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Cleanup all temp entries upon start.
|
||||
err = cleanupDir(storageDisk, minioMetaBucket, tmpMetaPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Depending on the disk type network or local, initialize storage API.
|
||||
func newStorageAPI(disk string) (storage StorageAPI, err error) {
|
||||
if !strings.ContainsRune(disk, ':') || filepath.VolumeName(disk) != "" {
|
||||
// Initialize filesystem storage API.
|
||||
return newPosix(disk)
|
||||
}
|
||||
// Initialize rpc client storage API.
|
||||
return newRPCClient(disk)
|
||||
}
|
||||
|
||||
// House keeping code needed for XL.
|
||||
func xlHouseKeeping(storageDisks []StorageAPI) error {
|
||||
// This happens for the first time, but keep this here since this
|
||||
// is the only place where it can be made expensive optimizing all
|
||||
// other calls. Create minio meta volume, if it doesn't exist yet.
|
||||
var wg = &sync.WaitGroup{}
|
||||
|
||||
// Initialize errs to collect errors inside go-routine.
|
||||
var errs = make([]error, len(storageDisks))
|
||||
|
||||
// Initialize all disks in parallel.
|
||||
for index, disk := range storageDisks {
|
||||
if disk == nil {
|
||||
errs[index] = errDiskNotFound
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(index int, disk StorageAPI) {
|
||||
// Indicate this wait group is done.
|
||||
defer wg.Done()
|
||||
|
||||
// Attempt to create `.minio`.
|
||||
err := disk.MakeVol(minioMetaBucket)
|
||||
if err != nil && err != errVolumeExists && err != errDiskNotFound {
|
||||
errs[index] = err
|
||||
return
|
||||
}
|
||||
// Cleanup all temp entries upon start.
|
||||
err = cleanupDir(disk, minioMetaBucket, tmpMetaPrefix)
|
||||
if err != nil {
|
||||
errs[index] = err
|
||||
return
|
||||
}
|
||||
errs[index] = nil
|
||||
}(index, disk)
|
||||
}
|
||||
|
||||
// Wait for all cleanup to finish.
|
||||
wg.Wait()
|
||||
|
||||
// Return upon first error.
|
||||
for _, err := range errs {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
return toObjectErr(err, minioMetaBucket, tmpMetaPrefix)
|
||||
}
|
||||
|
||||
// Return success here.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup a directory recursively.
|
||||
func cleanupDir(storage StorageAPI, volume, dirPath string) error {
|
||||
var delFunc func(string) error
|
||||
// Function to delete entries recursively.
|
||||
delFunc = func(entryPath string) error {
|
||||
if !strings.HasSuffix(entryPath, slashSeparator) {
|
||||
// No trailing "/" means that this is a file which can be deleted.
|
||||
return storage.DeleteFile(volume, entryPath)
|
||||
}
|
||||
// If it's a directory, list and call delFunc() for each entry.
|
||||
entries, err := storage.ListDir(volume, entryPath)
|
||||
if err != nil {
|
||||
if err == errFileNotFound {
|
||||
// if dirPath prefix never existed.
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
err = delFunc(pathJoin(entryPath, entry))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err := delFunc(retainSlash(pathJoin(dirPath)))
|
||||
return 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
|
||||
|
||||
import "time"
|
||||
|
||||
// StorageInfo - represents total capacity of underlying storage.
|
||||
type StorageInfo struct {
|
||||
Total int64 // Total disk space.
|
||||
Free int64 // Free total available disk space.
|
||||
}
|
||||
|
||||
// BucketInfo - bucket name and create date
|
||||
type BucketInfo struct {
|
||||
Name string
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
// ObjectInfo - object info.
|
||||
type ObjectInfo struct {
|
||||
Bucket string
|
||||
Name string
|
||||
ModTime time.Time
|
||||
Size int64
|
||||
IsDir bool
|
||||
MD5Sum string
|
||||
ContentType string
|
||||
ContentEncoding string
|
||||
}
|
||||
|
||||
// ListPartsInfo - various types of object resources.
|
||||
type ListPartsInfo struct {
|
||||
Bucket string
|
||||
Object string
|
||||
UploadID string
|
||||
StorageClass string
|
||||
PartNumberMarker int
|
||||
NextPartNumberMarker int
|
||||
MaxParts int
|
||||
IsTruncated bool
|
||||
|
||||
Parts []partInfo
|
||||
EncodingType string
|
||||
}
|
||||
|
||||
// ListMultipartsInfo - various types of bucket resources for inprogress multipart uploads.
|
||||
type ListMultipartsInfo struct {
|
||||
KeyMarker string
|
||||
UploadIDMarker string
|
||||
NextKeyMarker string
|
||||
NextUploadIDMarker string
|
||||
EncodingType string
|
||||
MaxUploads int
|
||||
IsTruncated bool
|
||||
Uploads []uploadMetadata
|
||||
Prefix string
|
||||
Delimiter string
|
||||
CommonPrefixes []string
|
||||
}
|
||||
|
||||
// ListObjectsInfo - container for list objects.
|
||||
type ListObjectsInfo struct {
|
||||
IsTruncated bool
|
||||
NextMarker string
|
||||
Objects []ObjectInfo
|
||||
Prefixes []string
|
||||
}
|
||||
|
||||
// partInfo - various types of individual part resources.
|
||||
type partInfo struct {
|
||||
PartNumber int
|
||||
LastModified time.Time
|
||||
ETag string
|
||||
Size int64
|
||||
}
|
||||
|
||||
// uploadMetadata container capturing metadata on in progress multipart upload in a given bucket
|
||||
type uploadMetadata struct {
|
||||
Object string
|
||||
UploadID string
|
||||
StorageClass string
|
||||
Initiated time.Time
|
||||
}
|
||||
|
||||
// completePart - completed part container.
|
||||
type completePart struct {
|
||||
PartNumber int
|
||||
ETag string
|
||||
}
|
||||
|
||||
// completedParts is a sortable interface for Part slice
|
||||
type completedParts []completePart
|
||||
|
||||
func (a completedParts) Len() int { return len(a) }
|
||||
func (a completedParts) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a completedParts) Less(i, j int) bool { return a[i].PartNumber < a[j].PartNumber }
|
||||
|
||||
// completeMultipartUpload container for completing multipart upload
|
||||
type completeMultipartUpload struct {
|
||||
Parts []completePart `xml:"Part"`
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* 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"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Converts underlying storage error. Convenience function written to
|
||||
// handle all cases where we have known types of errors returned by
|
||||
// underlying storage layer.
|
||||
func toObjectErr(err error, params ...string) error {
|
||||
switch err {
|
||||
case errVolumeNotFound:
|
||||
if len(params) >= 1 {
|
||||
return BucketNotFound{Bucket: params[0]}
|
||||
}
|
||||
case errVolumeNotEmpty:
|
||||
if len(params) >= 1 {
|
||||
return BucketNotEmpty{Bucket: params[0]}
|
||||
}
|
||||
case errVolumeExists:
|
||||
if len(params) >= 1 {
|
||||
return BucketExists{Bucket: params[0]}
|
||||
}
|
||||
case errDiskFull:
|
||||
return StorageFull{}
|
||||
case errIsNotRegular, errFileAccessDenied:
|
||||
if len(params) >= 2 {
|
||||
return ObjectExistsAsDirectory{
|
||||
Bucket: params[0],
|
||||
Object: params[1],
|
||||
}
|
||||
}
|
||||
case errFileNotFound:
|
||||
if len(params) >= 2 {
|
||||
return ObjectNotFound{
|
||||
Bucket: params[0],
|
||||
Object: params[1],
|
||||
}
|
||||
}
|
||||
case errFileNameTooLong:
|
||||
if len(params) >= 2 {
|
||||
return ObjectNameInvalid{
|
||||
Bucket: params[0],
|
||||
Object: params[1],
|
||||
}
|
||||
}
|
||||
case errXLReadQuorum:
|
||||
return InsufficientReadQuorum{}
|
||||
case errXLWriteQuorum:
|
||||
return InsufficientWriteQuorum{}
|
||||
case io.ErrUnexpectedEOF, io.ErrShortWrite:
|
||||
return IncompleteBody{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// StorageFull storage ran out of space.
|
||||
type StorageFull struct{}
|
||||
|
||||
func (e StorageFull) Error() string {
|
||||
return "Storage reached its minimum free disk threshold."
|
||||
}
|
||||
|
||||
// InsufficientReadQuorum storage cannot satisfy quorum for read operation.
|
||||
type InsufficientReadQuorum struct{}
|
||||
|
||||
func (e InsufficientReadQuorum) Error() string {
|
||||
return "Storage resources are insufficient for the read operation."
|
||||
}
|
||||
|
||||
// InsufficientWriteQuorum storage cannot satisfy quorum for write operation.
|
||||
type InsufficientWriteQuorum struct{}
|
||||
|
||||
func (e InsufficientWriteQuorum) Error() string {
|
||||
return "Storage resources are insufficient for the write operation."
|
||||
}
|
||||
|
||||
// GenericError - generic object layer error.
|
||||
type GenericError struct {
|
||||
Bucket string
|
||||
Object string
|
||||
}
|
||||
|
||||
// BucketNotFound bucket does not exist.
|
||||
type BucketNotFound GenericError
|
||||
|
||||
func (e BucketNotFound) Error() string {
|
||||
return "Bucket not found: " + e.Bucket
|
||||
}
|
||||
|
||||
// BucketNotEmpty bucket is not empty.
|
||||
type BucketNotEmpty GenericError
|
||||
|
||||
func (e BucketNotEmpty) Error() string {
|
||||
return "Bucket not empty: " + e.Bucket
|
||||
}
|
||||
|
||||
// ObjectNotFound object does not exist.
|
||||
type ObjectNotFound GenericError
|
||||
|
||||
func (e ObjectNotFound) Error() string {
|
||||
return "Object not found: " + e.Bucket + "#" + e.Object
|
||||
}
|
||||
|
||||
// ObjectExistsAsDirectory object already exists as a directory.
|
||||
type ObjectExistsAsDirectory GenericError
|
||||
|
||||
func (e ObjectExistsAsDirectory) Error() string {
|
||||
return "Object exists on : " + e.Bucket + " as directory " + e.Object
|
||||
}
|
||||
|
||||
// BucketExists bucket exists.
|
||||
type BucketExists GenericError
|
||||
|
||||
func (e BucketExists) Error() string {
|
||||
return "Bucket exists: " + e.Bucket
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// UnsupportedDelimiter - unsupported delimiter.
|
||||
type UnsupportedDelimiter struct {
|
||||
Delimiter string
|
||||
}
|
||||
|
||||
func (e UnsupportedDelimiter) Error() string {
|
||||
return fmt.Sprintf("delimiter '%s' is not supported. Only '/' is supported", e.Delimiter)
|
||||
}
|
||||
|
||||
// InvalidUploadIDKeyCombination - invalid upload id and key marker combination.
|
||||
type InvalidUploadIDKeyCombination struct {
|
||||
UploadIDMarker, KeyMarker string
|
||||
}
|
||||
|
||||
func (e InvalidUploadIDKeyCombination) Error() string {
|
||||
return fmt.Sprintf("Invalid combination of uploadID marker '%s' and marker '%s'", e.UploadIDMarker, e.KeyMarker)
|
||||
}
|
||||
|
||||
// InvalidMarkerPrefixCombination - invalid marker and prefix combination.
|
||||
type InvalidMarkerPrefixCombination struct {
|
||||
Marker, Prefix string
|
||||
}
|
||||
|
||||
func (e InvalidMarkerPrefixCombination) Error() string {
|
||||
return fmt.Sprintf("Invalid combination of marker '%s' and prefix '%s'", e.Marker, e.Prefix)
|
||||
}
|
||||
|
||||
// BucketPolicyNotFound - no bucket policy found.
|
||||
type BucketPolicyNotFound GenericError
|
||||
|
||||
func (e BucketPolicyNotFound) Error() string {
|
||||
return "No bucket policy found for bucket: " + e.Bucket
|
||||
}
|
||||
|
||||
/// Bucket related errors.
|
||||
|
||||
// BucketNameInvalid - bucketname provided is invalid.
|
||||
type BucketNameInvalid GenericError
|
||||
|
||||
// Return string an error formatted as the given text.
|
||||
func (e BucketNameInvalid) Error() string {
|
||||
return "Bucket name invalid: " + e.Bucket
|
||||
}
|
||||
|
||||
/// Object related errors.
|
||||
|
||||
// ObjectNameInvalid - object name provided is invalid.
|
||||
type ObjectNameInvalid GenericError
|
||||
|
||||
// Return string an error formatted as the given text.
|
||||
func (e ObjectNameInvalid) Error() string {
|
||||
return "Object name invalid: " + e.Bucket + "#" + e.Object
|
||||
}
|
||||
|
||||
// UnExpectedDataSize - Reader contains more/less data than specified.
|
||||
type UnExpectedDataSize struct {
|
||||
Size int
|
||||
}
|
||||
|
||||
func (e UnExpectedDataSize) Error() string {
|
||||
return fmt.Sprintf("Contains more data than specified size of %d bytes.", e.Size)
|
||||
}
|
||||
|
||||
// IncompleteBody You did not provide the number of bytes specified by the Content-Length HTTP header.
|
||||
type IncompleteBody GenericError
|
||||
|
||||
// Return string an error formatted as the given text.
|
||||
func (e IncompleteBody) Error() string {
|
||||
return e.Bucket + "#" + e.Object + "has incomplete body"
|
||||
}
|
||||
|
||||
/// Multipart related errors.
|
||||
|
||||
// MalformedUploadID malformed upload id.
|
||||
type MalformedUploadID struct {
|
||||
UploadID string
|
||||
}
|
||||
|
||||
func (e MalformedUploadID) Error() string {
|
||||
return "Malformed upload id " + e.UploadID
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// PartTooSmall - error if part size is less than 5MB.
|
||||
type PartTooSmall struct{}
|
||||
|
||||
func (e PartTooSmall) Error() string {
|
||||
return "Part size should be atleast 5MB"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user