mirror of
https://github.com/pgsty/minio.git
synced 2026-07-31 07:45:19 +03:00
Compare commits
306 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1a6c32d80 | |||
| bbd05a8f1c | |||
| 4393afb7e2 | |||
| 1a53734477 | |||
| e95c0bb913 | |||
| fce556b8a0 | |||
| b97f99766f | |||
| f7b6f7b22f | |||
| 2a0a62b78d | |||
| 67f66c40c1 | |||
| 5a2bdf6959 | |||
| 51e78a3e20 | |||
| 9eb52ec7c7 | |||
| 0827a2747b | |||
| e7a724de0d | |||
| d57d57ddf5 | |||
| a4d6195244 | |||
| 7d3eaf79ff | |||
| b63c37b28e | |||
| 8d59f35523 | |||
| f460eceb6d | |||
| d10679866c | |||
| a79a7e570c | |||
| 16ecaac4fc | |||
| ca6b4773ed | |||
| 7e7ae29d89 | |||
| 7d18f00116 | |||
| 719f8c258a | |||
| dcdb07433a | |||
| 26e9f78a86 | |||
| 34a1b58a75 | |||
| 32c6b62932 | |||
| 8d584bd819 | |||
| 031767791d | |||
| 3b917067d9 | |||
| 203ac8edaa | |||
| 8bbfb1b714 | |||
| b4ddccc2f7 | |||
| 6400f506da | |||
| 7195ac7f14 | |||
| 03df692ae2 | |||
| a011fe8450 | |||
| 95d97c2d6d | |||
| bc8b936d4b | |||
| d23ded0d83 | |||
| db3fed2279 | |||
| 866dffcd62 | |||
| 5eb210dd2e | |||
| 758d5458f0 | |||
| 8a40da3fd0 | |||
| 1d8a8c63db | |||
| 65a817fe8c | |||
| 1256b0b818 | |||
| 7e05b826fa | |||
| d82a1da511 | |||
| f598f4fd1b | |||
| e65d8fdc7d | |||
| b919462610 | |||
| 8287ab091c | |||
| f25bec6bf1 | |||
| 75865efb0e | |||
| 3d2d63f71e | |||
| 53e133e844 | |||
| b2cbade477 | |||
| 0c0d1e4150 | |||
| 3d0dced23c | |||
| ad53c5d859 | |||
| 45463b1d6b | |||
| d5895d3243 | |||
| db1edfe487 | |||
| 9c16f73334 | |||
| f50bec2987 | |||
| e2bff5cdc0 | |||
| 02a5f1e96a | |||
| 4deefa3695 | |||
| d28b3d8801 | |||
| 099b5293a9 | |||
| 4a0a491ca1 | |||
| db6b6e9518 | |||
| d8a11c8f4b | |||
| 0b546ddfd4 | |||
| d1712a46a7 | |||
| c46c2a6dd6 | |||
| 88938340b3 | |||
| 13a7033505 | |||
| 670e3e0c8b | |||
| 89d528a4ed | |||
| 53f3d2fd65 | |||
| a4f26aec00 | |||
| 60cc6184d2 | |||
| ce2d185211 | |||
| b05351c420 | |||
| b415c600e1 | |||
| 4c9fae90ff | |||
| 94670a387e | |||
| 789270af3c | |||
| a5fbe1e16c | |||
| 3c836b5f34 | |||
| 02af37a394 | |||
| 4879cd73f8 | |||
| b5dc4b5873 | |||
| 6dcfaa877c | |||
| 0bf981278e | |||
| d3eb5815d9 | |||
| 7e6b5bdbb7 | |||
| 4cadb33da2 | |||
| c3ff402fcb | |||
| aceaa1ec48 | |||
| 330f79b40e | |||
| 6d7df1d1cb | |||
| 3c0d3f7510 | |||
| f7ae3be586 | |||
| 70fec0a53f | |||
| fba1669966 | |||
| 71201273e2 | |||
| 79ba4d3f33 | |||
| f8024cadbb | |||
| f680b8482f | |||
| a56f7e2f23 | |||
| 6d5d49bfb1 | |||
| 8e4842b665 | |||
| b74ef6d5f4 | |||
| 9925640da8 | |||
| f45e0a44b8 | |||
| 3e632a49ee | |||
| 1bb3a03099 | |||
| 42b3795304 | |||
| 302fcb3b17 | |||
| b9fc4150f6 | |||
| f66239e82f | |||
| 6c2bc0568b | |||
| 0d154871d5 | |||
| 189b6682d6 | |||
| cf479eb401 | |||
| 72490bf8db | |||
| 5a73aecb5c | |||
| e26a706dff | |||
| 9e9c7b4f22 | |||
| ff8e2b5b4f | |||
| 61e0b1454a | |||
| 93f126364e | |||
| 6dca044ea8 | |||
| 9e9faf7b53 | |||
| 2bca51ab2c | |||
| 34e780e690 | |||
| 6cab6d802d | |||
| ea65350308 | |||
| 1bb9d49eaa | |||
| db5af1b126 | |||
| b233345f19 | |||
| 07ccf4c0d0 | |||
| 77d2870f5b | |||
| 3a73c675a6 | |||
| b694c1a4d7 | |||
| 2e6ee68409 | |||
| 7505bac037 | |||
| 9dca0c1889 | |||
| 69555f1224 | |||
| 879cef37a1 | |||
| 3d21119ec8 | |||
| a2f2044528 | |||
| 85fcee1919 | |||
| 617f2394fb | |||
| 1729e82361 | |||
| 58633a383a | |||
| d4b107adf4 | |||
| 53f84d6084 | |||
| d864e00e24 | |||
| b69aa9c4d0 | |||
| d20cdac304 | |||
| fffe4ac7e6 | |||
| 98b62cbec8 | |||
| 1978b9d8f9 | |||
| 3544e5ad01 | |||
| e7cdd8f02c | |||
| 3ff09b9b72 | |||
| b3b42c72a9 | |||
| 32da1aa9d6 | |||
| b10fa507b2 | |||
| 1e8925cea0 | |||
| f346ca44f0 | |||
| 6f7ace3d3e | |||
| 812142f007 | |||
| fa52d491c5 | |||
| 283ad7e5e5 | |||
| 748b1d6495 | |||
| b4dc6df35c | |||
| 218049300c | |||
| 5db533c024 | |||
| aeafe668d8 | |||
| 54f3a0946f | |||
| be6bd52978 | |||
| 75c43bfb6c | |||
| 0aca2ab970 | |||
| 28bc5899fd | |||
| fcc61fa46a | |||
| f7889e1af7 | |||
| bccc386994 | |||
| 0f401b67ad | |||
| 108decfa76 | |||
| 6afbd502e8 | |||
| ec5293ce29 | |||
| 4785555d34 | |||
| b918a6592f | |||
| eb787d8613 | |||
| 0a1501bc1b | |||
| f8bd9cfd83 | |||
| c59b995f7b | |||
| bc73a1a1cb | |||
| ce7c9c651d | |||
| c3dd7c1f6c | |||
| 2d23cd4f39 | |||
| ce7af3aae1 | |||
| 1b92c5136b | |||
| cc8a8cb877 | |||
| ce403fdaa0 | |||
| 45fbb0d618 | |||
| f5ce685aa1 | |||
| c83055500d | |||
| b0fbddc051 | |||
| 4e0c08e9c5 | |||
| 344f9ec608 | |||
| 28ff62716f | |||
| 1f69a75efa | |||
| 02a81ee564 | |||
| e91e9e8a38 | |||
| e45f9057d6 | |||
| 53e7fdc847 | |||
| 1af331c05c | |||
| 0e5b9c7fe4 | |||
| a86dc8a4c5 | |||
| c88dca984d | |||
| 1054f9cbf0 | |||
| aaacce4c43 | |||
| 7bd1f44491 | |||
| ba6d997d18 | |||
| eaa41e4086 | |||
| 6b70f429ed | |||
| 0a6e9a1834 | |||
| 15b65a8342 | |||
| 8b7df7da37 | |||
| 237c8af5ef | |||
| ff036c171f | |||
| a3b085300d | |||
| b90cefdb88 | |||
| f3506b8958 | |||
| 46897b1100 | |||
| cec8b238f3 | |||
| 13ab8e17e2 | |||
| 9d3d64df2d | |||
| 146bc3e638 | |||
| 0543d45fb3 | |||
| fe426944ea | |||
| c1a6ca0c33 | |||
| 5a78266821 | |||
| f5b4b0765a | |||
| 3928c1e14c | |||
| 94241cd153 | |||
| a86c2e2ce1 | |||
| e99244be02 | |||
| 58833711e0 | |||
| f99f218999 | |||
| 4fb5fc72d7 | |||
| d86973dcca | |||
| 8085ba4494 | |||
| 9bd0eb1a9e | |||
| e4e0abfc05 | |||
| 12b2fc894b | |||
| 2aa76e7407 | |||
| 0dab038858 | |||
| a76556ec1b | |||
| 91c7bb65c5 | |||
| 5d602034ea | |||
| b829ec4a6b | |||
| 9c50a9f567 | |||
| 52122c0309 | |||
| 3b9d313c87 | |||
| bfff251e2a | |||
| 52b500cce9 | |||
| c63cdca11f | |||
| 4430085981 | |||
| 5c78415b31 | |||
| bd67117756 | |||
| f3e5e9fb29 | |||
| 2de1921fe8 | |||
| dd7e47f264 | |||
| fe9d826bef | |||
| 6dbc5aba09 | |||
| de5374f74c | |||
| bf55591c64 | |||
| 2d814e340f | |||
| 0a8cf1a6b0 | |||
| 07949f68d8 | |||
| 909a89647b | |||
| 6508da5fde | |||
| 3379f005a5 | |||
| 4be609eb82 | |||
| dd5b975001 | |||
| 9ac3538141 | |||
| ace4f9fd15 | |||
| b283a2c21f | |||
| ed37bf3703 | |||
| d0f18dc1b5 | |||
| 8293f546af | |||
| af8071c86a | |||
| dce76d9307 |
@@ -24,5 +24,6 @@
|
||||
- [ ] My code follows the code style of this project.
|
||||
- [ ] My change requires a change to the documentation.
|
||||
- [ ] I have updated the documentation accordingly.
|
||||
- [ ] I have added tests to cover my changes.
|
||||
- [ ] I have added unit tests to cover my changes.
|
||||
- [ ] I have added/updated functional tests in [mint](https://github.com/minio/mint). (If yes, add `mint` PR # here: )
|
||||
- [ ] All new and existing tests passed.
|
||||
@@ -16,3 +16,10 @@ release
|
||||
.DS_Store
|
||||
*.syso
|
||||
coverage.txt
|
||||
.vscode/
|
||||
.snap
|
||||
*.tar.bz2
|
||||
parts/
|
||||
prime/
|
||||
snap/.snapcraft/
|
||||
stage/
|
||||
|
||||
+1
-14
@@ -13,7 +13,6 @@ os:
|
||||
|
||||
env:
|
||||
- ARCH=x86_64
|
||||
- ARCH=i686
|
||||
|
||||
script:
|
||||
## Run all the tests
|
||||
@@ -23,20 +22,8 @@ script:
|
||||
- make test GOFLAGS="-timeout 15m -race -v"
|
||||
- make coverage
|
||||
|
||||
# Refer https://blog.hypriot.com/post/setup-simple-ci-pipeline-for-arm-images/
|
||||
# push image
|
||||
- >
|
||||
if [ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$ARCH" == "x86_64" ]; then
|
||||
docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
docker build -t minio/minio:edge-armhf . -f Dockerfile.armhf
|
||||
docker build -t minio/minio:edge-aarch64 . -f Dockerfile.aarch64
|
||||
docker login -u="$DOCKER_USER" -p="$DOCKER_PASS"
|
||||
docker push minio/minio:edge-armhf
|
||||
docker push minio/minio:edge-aarch64
|
||||
fi
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
go:
|
||||
- 1.7.5
|
||||
- 1.9.1
|
||||
|
||||
+49
-49
@@ -1,9 +1,14 @@
|
||||
### Install Golang
|
||||
# Minio Contribution Guide [](https://slack.minio.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/) [](https://codecov.io/gh/minio/minio)
|
||||
|
||||
If you do not have a working Golang environment setup please follow [Golang Installation Guide](https://docs.minio.io/docs/how-to-install-golang).
|
||||
``Minio`` community welcomes your contribution. To make the process as seamless as possible, we recommend you read this contribution guide.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
Start by forking the Minio GitHub repository, make changes in a branch and then send a pull request. We encourage pull requests to discuss code changes. Here are the steps in details:
|
||||
|
||||
### Setup your Minio Github Repository
|
||||
Fork [Minio upstream](https://github.com/minio/minio/fork) source repository to your own personal repository. Copy the URL for minio from your personal github repo (you will need it for the `git clone` command below).
|
||||
Fork [Minio upstream](https://github.com/minio/minio/fork) source repository to your own personal repository. Copy the URL of your Minio fork (you will need it for the `git clone` command below).
|
||||
|
||||
```sh
|
||||
$ mkdir -p $GOPATH/src/github.com/minio
|
||||
$ cd $GOPATH/src/github.com/minio
|
||||
@@ -11,61 +16,56 @@ $ git clone <paste saved URL for personal forked minio repo>
|
||||
$ cd minio
|
||||
```
|
||||
|
||||
### Compiling Minio from source
|
||||
Minio uses ``Makefile`` to wrap around some of redundant checks done through command line.
|
||||
|
||||
```sh
|
||||
$ make
|
||||
Checking if proper environment variables are set.. Done
|
||||
...
|
||||
Checking dependencies for Minio.. Done
|
||||
Installed govet
|
||||
Building Libraries
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
### Setting up git remote as ``upstream``
|
||||
### Set up git remote as ``upstream``
|
||||
```sh
|
||||
$ cd $GOPATH/src/github.com/minio/minio
|
||||
$ git remote add upstream https://github.com/minio/minio
|
||||
$ git fetch upstream
|
||||
$ git merge upstream/master
|
||||
...
|
||||
...
|
||||
$ make
|
||||
Checking if proper environment variables are set.. Done
|
||||
...
|
||||
Checking dependencies for Minio.. Done
|
||||
Installed govet
|
||||
Building Libraries
|
||||
...
|
||||
```
|
||||
|
||||
### Developer Guidelines
|
||||
``Minio`` community welcomes your contribution. To make the process as seamless as possible, we ask for the following:
|
||||
* Go ahead and fork the project and make your changes. We encourage pull requests to discuss code changes.
|
||||
- Fork it
|
||||
- Create your feature branch (git checkout -b my-new-feature)
|
||||
- Commit your changes (git commit -am 'Add some feature')
|
||||
- Push to the branch (git push origin my-new-feature)
|
||||
- Create new Pull Request
|
||||
### Create your feature branch
|
||||
Before making code changes, make sure you create a separate branch for these changes
|
||||
|
||||
* If you have additional dependencies for ``Minio``, ``Minio`` manages its dependencies using [govendor](https://github.com/kardianos/govendor)
|
||||
- Run `go get foo/bar`
|
||||
- Edit your code to import foo/bar
|
||||
- Run `make pkg-add PKG=foo/bar` from top-level directory
|
||||
```
|
||||
$ git checkout -b my-new-feature
|
||||
```
|
||||
|
||||
* If you have dependencies for ``Minio`` which needs to be removed
|
||||
- Edit your code to not import foo/bar
|
||||
- Run `make pkg-remove PKG=foo/bar` from top-level directory
|
||||
### Test Minio server changes
|
||||
After your code changes, make sure
|
||||
|
||||
* When you're ready to create a pull request, be sure to:
|
||||
- Have test cases for the new code. If you have questions about how to do it, please ask in your pull request.
|
||||
- Run `make verifiers`
|
||||
- Squash your commits into a single commit. `git rebase -i`. It's okay to force update your pull request.
|
||||
- Make sure `go test -race ./...` and `go build` completes.
|
||||
- To add test cases for the new code. If you have questions about how to do it, please ask on our [Slack](slack.minio.io) channel.
|
||||
- To run `make verifiers`
|
||||
- To squash your commits into a single commit. `git rebase -i`. It's okay to force update your pull request.
|
||||
- To run `go test -race ./...` and `go build` completes.
|
||||
|
||||
* Read [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project
|
||||
- `Minio` project is fully conformant with Golang style
|
||||
- if you happen to observe offending code, please feel free to send a pull request
|
||||
### Commit changes
|
||||
After verification, commit your changes. This is a [great post](https://chris.beams.io/posts/git-commit/) on how to write useful commit messages
|
||||
|
||||
```
|
||||
$ git commit -am 'Add some feature'
|
||||
```
|
||||
|
||||
### Push to the branch
|
||||
Push your locally committed changes to the remote origin (your fork)
|
||||
```
|
||||
$ git push origin my-new-feature
|
||||
```
|
||||
|
||||
### Create a Pull Request
|
||||
Pull requests can be created via GitHub. Refer to [this document](https://help.github.com/articles/creating-a-pull-request/) for detailed steps on how to create a pull request. After a Pull Request gets peer reviewed and approved, it will be merged.
|
||||
|
||||
## FAQs
|
||||
### How does ``Minio`` manages dependencies?
|
||||
``Minio`` manages its dependencies using [govendor](https://github.com/kardianos/govendor). To add a dependency
|
||||
- Run `go get foo/bar`
|
||||
- Edit your code to import foo/bar
|
||||
- Run `make pkg-add PKG=foo/bar` from top-level directory
|
||||
|
||||
To remove a dependency
|
||||
- Edit your code to not import foo/bar
|
||||
- Run `make pkg-remove PKG=foo/bar` from top-level directory
|
||||
|
||||
### What are the coding guidelines for Minio?
|
||||
``Minio`` is fully conformant with Golang style. Refer: [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project. If you observe offending code, please feel free to send a pull request or ping us on [Slack](slack.minio.io).
|
||||
|
||||
+8
-7
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.5
|
||||
FROM golang:1.9.1-alpine3.6
|
||||
|
||||
MAINTAINER Minio Inc <dev@minio.io>
|
||||
|
||||
@@ -8,9 +8,11 @@ ENV CGO_ENABLED 0
|
||||
|
||||
WORKDIR /go/src/github.com/minio/
|
||||
|
||||
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates && \
|
||||
apk add --no-cache --virtual .build-deps git go musl-dev && \
|
||||
apk add --no-cache ca-certificates curl && \
|
||||
apk add --no-cache --virtual .build-deps git && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
go get -v -d github.com/minio/minio && \
|
||||
cd /go/src/github.com/minio/minio && \
|
||||
@@ -19,12 +21,11 @@ RUN \
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
COPY buildscripts/docker-entrypoint.sh /usr/bin/
|
||||
|
||||
RUN chmod +x /usr/bin/docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
|
||||
VOLUME ["/export"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s \
|
||||
CMD /usr/bin/healthcheck.sh
|
||||
|
||||
CMD ["minio"]
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
FROM resin/aarch64-alpine:3.5
|
||||
|
||||
MAINTAINER Minio Inc <dev@minio.io>
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV PATH $PATH:$GOPATH/bin
|
||||
ENV CGO_ENABLED 0
|
||||
|
||||
WORKDIR /go/src/github.com/minio/
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates && \
|
||||
apk add --no-cache --virtual .build-deps git go musl-dev && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
go get -v -d github.com/minio/minio && \
|
||||
cd /go/src/github.com/minio/minio && \
|
||||
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
|
||||
rm -rf /go/pkg /go/src /usr/local/go && apk del .build-deps
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
COPY buildscripts/docker-entrypoint.sh /usr/bin/
|
||||
|
||||
RUN chmod +x /usr/bin/docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
|
||||
VOLUME ["/export"]
|
||||
|
||||
CMD ["minio"]
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM resin/armhf-alpine:3.5
|
||||
FROM golang:1.9.1-alpine3.6
|
||||
|
||||
MAINTAINER Minio Inc <dev@minio.io>
|
||||
|
||||
@@ -8,23 +8,25 @@ ENV CGO_ENABLED 0
|
||||
|
||||
WORKDIR /go/src/github.com/minio/
|
||||
|
||||
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||
|
||||
COPY . /go/src/github.com/minio/minio
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates && \
|
||||
apk add --no-cache --virtual .build-deps git go musl-dev && \
|
||||
apk add --no-cache ca-certificates curl && \
|
||||
apk add --no-cache --virtual .build-deps git && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
go get -v -d github.com/minio/minio && \
|
||||
cd /go/src/github.com/minio/minio && \
|
||||
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
|
||||
rm -rf /go/pkg /go/src /usr/local/go && apk del .build-deps
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
COPY buildscripts/docker-entrypoint.sh /usr/bin/
|
||||
|
||||
RUN chmod +x /usr/bin/docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
|
||||
VOLUME ["/export"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s \
|
||||
CMD /usr/bin/healthcheck.sh
|
||||
|
||||
CMD ["minio"]
|
||||
+9
-6
@@ -1,23 +1,26 @@
|
||||
FROM alpine:3.5
|
||||
FROM alpine:3.6
|
||||
|
||||
MAINTAINER Minio Inc <dev@minio.io>
|
||||
|
||||
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates && \
|
||||
apk add --no-cache --virtual .build-deps curl && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
curl https://dl.minio.io/server/minio/release/linux-amd64/minio > /usr/bin/minio && \
|
||||
chmod +x /usr/bin/minio && apk del .build-deps
|
||||
chmod +x /usr/bin/minio && \
|
||||
chmod +x /usr/bin/docker-entrypoint.sh && \
|
||||
chmod +x /usr/bin/healthcheck.sh
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
COPY buildscripts/docker-entrypoint.sh /usr/bin/
|
||||
|
||||
RUN chmod +x /usr/bin/docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
|
||||
VOLUME ["/export"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s \
|
||||
CMD /usr/bin/healthcheck.sh
|
||||
|
||||
CMD ["minio"]
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
FROM resin/aarch64-alpine:3.5
|
||||
|
||||
MAINTAINER Minio Inc <dev@minio.io>
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates && \
|
||||
apk add --no-cache --virtual .build-deps curl && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
curl https://dl.minio.io/server/minio/release/linux-arm64/minio > /usr/bin/minio && \
|
||||
chmod +x /usr/bin/minio && apk del .build-deps
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
COPY buildscripts/docker-entrypoint.sh /usr/bin/
|
||||
|
||||
RUN chmod +x /usr/bin/docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
|
||||
VOLUME ["/export"]
|
||||
|
||||
CMD ["minio"]
|
||||
@@ -1,22 +0,0 @@
|
||||
FROM resin/armhf-alpine:3.5
|
||||
|
||||
MAINTAINER Minio Inc <dev@minio.io>
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates && \
|
||||
apk add --no-cache --virtual .build-deps curl && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
curl https://dl.minio.io/server/minio/release/linux-arm/minio > /usr/bin/minio && \
|
||||
chmod +x /usr/bin/minio && apk del .build-deps
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
COPY buildscripts/docker-entrypoint.sh /usr/bin/
|
||||
|
||||
RUN chmod +x /usr/bin/docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
|
||||
VOLUME ["/export"]
|
||||
|
||||
CMD ["minio"]
|
||||
@@ -1,60 +1,10 @@
|
||||
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
||||
PWD := $(shell pwd)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
||||
|
||||
BUILD_LDFLAGS := '$(LDFLAGS)'
|
||||
TAG := latest
|
||||
|
||||
HOST ?= $(shell uname)
|
||||
CPU ?= $(shell uname -m)
|
||||
|
||||
# if no host is identifed (no uname tool)
|
||||
# we assume a Linux-64bit build
|
||||
ifeq ($(HOST),)
|
||||
HOST = Linux
|
||||
endif
|
||||
|
||||
# identify CPU
|
||||
ifeq ($(CPU), x86_64)
|
||||
HOST := $(HOST)64
|
||||
else
|
||||
ifeq ($(CPU), amd64)
|
||||
HOST := $(HOST)64
|
||||
else
|
||||
ifeq ($(CPU), i686)
|
||||
HOST := $(HOST)32
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
#############################################
|
||||
# now we find out the target OS for
|
||||
# which we are going to compile in case
|
||||
# the caller didn't yet define OS himself
|
||||
ifndef (OS)
|
||||
ifeq ($(HOST), Linux64)
|
||||
arch = gcc
|
||||
else
|
||||
ifeq ($(HOST), Linux32)
|
||||
arch = 32
|
||||
else
|
||||
ifeq ($(HOST), Darwin64)
|
||||
arch = clang
|
||||
else
|
||||
ifeq ($(HOST), Darwin32)
|
||||
arch = clang
|
||||
else
|
||||
ifeq ($(HOST), FreeBSD64)
|
||||
arch = gcc
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
all: install
|
||||
all: build
|
||||
|
||||
checks:
|
||||
@echo "Check deps"
|
||||
@@ -69,7 +19,7 @@ getdeps: checks
|
||||
@echo "Installing misspell" && go get -u github.com/client9/misspell/cmd/misspell
|
||||
@echo "Installing ineffassign" && go get -u github.com/gordonklaus/ineffassign
|
||||
|
||||
verifiers: vet fmt lint cyclo spelling
|
||||
verifiers: getdeps vet fmt lint cyclo deadcode spelling
|
||||
|
||||
vet:
|
||||
@echo "Running $@"
|
||||
@@ -95,29 +45,33 @@ cyclo:
|
||||
@${GOPATH}/bin/gocyclo -over 100 cmd
|
||||
@${GOPATH}/bin/gocyclo -over 100 pkg
|
||||
|
||||
build: getdeps verifiers $(UI_ASSETS)
|
||||
|
||||
deadcode:
|
||||
@${GOPATH}/bin/deadcode
|
||||
@echo "Running $@"
|
||||
@${GOPATH}/bin/deadcode -test $(shell go list ./...) || true
|
||||
|
||||
spelling:
|
||||
@${GOPATH}/bin/misspell -error `find cmd/`
|
||||
@${GOPATH}/bin/misspell -error `find pkg/`
|
||||
@${GOPATH}/bin/misspell -error `find docs/`
|
||||
|
||||
test: build
|
||||
@echo "Running all minio testing"
|
||||
# Builds minio, runs the verifiers then runs the tests.
|
||||
check: test
|
||||
test: verifiers build
|
||||
@echo "Running unit tests"
|
||||
@go test $(GOFLAGS) .
|
||||
@go test $(GOFLAGS) github.com/minio/minio/cmd...
|
||||
@go test $(GOFLAGS) github.com/minio/minio/pkg...
|
||||
@echo "Verifying build"
|
||||
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
||||
|
||||
coverage: build
|
||||
@echo "Running all coverage for minio"
|
||||
@./buildscripts/go-coverage.sh
|
||||
|
||||
gomake-all: build
|
||||
@echo "Installing minio at $(GOPATH)/bin/minio"
|
||||
@go build --ldflags $(BUILD_LDFLAGS) -o $(GOPATH)/bin/minio
|
||||
# Builds minio locally.
|
||||
build:
|
||||
@echo "Building minio binary: $(PWD)/minio"
|
||||
@CGO_ENABLED=0 go build --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
|
||||
|
||||
pkg-add:
|
||||
@echo "Adding new package $(PKG)"
|
||||
@@ -134,13 +88,11 @@ pkg-remove:
|
||||
pkg-list:
|
||||
@$(GOPATH)/bin/govendor list
|
||||
|
||||
install: gomake-all
|
||||
|
||||
release: verifiers
|
||||
@MINIO_RELEASE=RELEASE ./buildscripts/build.sh
|
||||
|
||||
experimental: verifiers
|
||||
@MINIO_RELEASE=EXPERIMENTAL ./buildscripts/build.sh
|
||||
# Builds minio and installs it to $GOPATH/bin.
|
||||
install: build
|
||||
@echo "Installing minio binary: $(GOPATH)/bin/minio"
|
||||
@cp $(PWD)/minio $(GOPATH)/bin/minio
|
||||
@echo "\nInstallation successful. To learn more, try \"minio --help\"."
|
||||
|
||||
clean:
|
||||
@echo "Cleaning up all the generated files"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Minio Quickstart Guide [](https://slack.minio.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/) [](https://codecov.io/gh/minio/minio)
|
||||
# Minio Quickstart Guide
|
||||
[](https://slack.minio.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/) [](https://codecov.io/gh/minio/minio) [](https://build.snapcraft.io/user/minio/minio)
|
||||
|
||||
Minio is an object storage server released under Apache License v2.0. It is compatible with Amazon S3 cloud storage service. It is best suited for storing unstructured data such as photos, videos, log files, backups and container / VM images. Size of an object can range from a few KBs to a maximum of 5TB.
|
||||
|
||||
@@ -8,38 +9,29 @@ Minio server is light enough to be bundled with the application stack, similar t
|
||||
### Stable
|
||||
```
|
||||
docker pull minio/minio
|
||||
docker run -p 9000:9000 minio/minio server /export
|
||||
docker run -p 9000:9000 minio/minio server /data
|
||||
```
|
||||
|
||||
### Edge
|
||||
```
|
||||
docker pull minio/minio:edge
|
||||
docker run -p 9000:9000 minio/minio:edge server /export
|
||||
docker run -p 9000:9000 minio/minio:edge server /data
|
||||
```
|
||||
Please visit Minio Docker quickstart guide for more [here](https://docs.minio.io/docs/minio-docker-quickstart-guide)
|
||||
|
||||
## macOS
|
||||
### Homebrew
|
||||
Install minio packages using [Homebrew](http://brew.sh/)
|
||||
|
||||
```sh
|
||||
brew install minio/stable/minio
|
||||
minio server ~/Photos
|
||||
minio server /data
|
||||
```
|
||||
#### Note
|
||||
If you previously installed minio using `brew install minio` then uninstall minio as shown below
|
||||
|
||||
```
|
||||
brew uninstall minio
|
||||
```
|
||||
|
||||
Then re-install the latest minio using:
|
||||
|
||||
```
|
||||
If you previously installed minio using `brew install minio` then it is recommended that you reinstall minio from `minio/stable/minio` official repo instead.
|
||||
```sh
|
||||
brew uninstall minio
|
||||
brew install minio/stable/minio
|
||||
```
|
||||
|
||||
>`brew install minio` and `brew upgrade minio` will no longer install/upgrade the latest minio binaries on macOS. Upstream bugs in golang 1.8 broke Minio brew installer. Use the updated `minio/stable/minio` in your brew paths.
|
||||
```
|
||||
|
||||
### Binary Download
|
||||
| Platform| Architecture | URL|
|
||||
@@ -47,7 +39,7 @@ brew install minio/stable/minio
|
||||
|Apple macOS|64-bit Intel|https://dl.minio.io/server/minio/release/darwin-amd64/minio |
|
||||
```sh
|
||||
chmod 755 minio
|
||||
./minio server ~/Photos
|
||||
./minio server /data
|
||||
```
|
||||
|
||||
## GNU/Linux
|
||||
@@ -55,13 +47,24 @@ chmod 755 minio
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio |
|
||||
| |32-bit Intel|https://dl.minio.io/server/minio/release/linux-386/minio |
|
||||
| |32-bit ARM|https://dl.minio.io/server/minio/release/linux-arm/minio |
|
||||
| |64-bit ARM|https://dl.minio.io/server/minio/release/linux-arm64/minio |
|
||||
| |32-bit ARMv6|https://dl.minio.io/server/minio/release/linux-arm6vl/minio |
|
||||
```sh
|
||||
chmod +x minio
|
||||
./minio server ~/Photos
|
||||
./minio server /data
|
||||
```
|
||||
|
||||
### Snap
|
||||
You can install the latest `minio` [snap](https://snapcraft.io), and help testing the most recent changes of the master branch in [all the supported Linux distros](https://snapcraft.io/docs/core/install) with:
|
||||
|
||||
```sh
|
||||
sudo snap install minio --edge
|
||||
```
|
||||
|
||||
Every time a new version of `minio` is pushed to the store, you will get it updated automatically.
|
||||
|
||||
You will need to allow the minio snap to observe mounts in the system:
|
||||
|
||||
```sh
|
||||
sudo snap connect minio:mount-observe
|
||||
```
|
||||
|
||||
## Microsoft Windows
|
||||
@@ -69,7 +72,6 @@ chmod +x minio
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|Microsoft Windows|64-bit|https://dl.minio.io/server/minio/release/windows-amd64/minio.exe |
|
||||
| |32-bit|https://dl.minio.io/server/minio/release/windows-386/minio.exe |
|
||||
```sh
|
||||
minio.exe server D:\Photos
|
||||
```
|
||||
@@ -85,15 +87,6 @@ sysrc minio_disks=/home/user/Photos
|
||||
service minio start
|
||||
```
|
||||
|
||||
### Binary Download
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|FreeBSD|64-bit|https://dl.minio.io/server/minio/release/freebsd-amd64/minio |
|
||||
```sh
|
||||
chmod 755 minio
|
||||
./minio server ~/Photos
|
||||
```
|
||||
|
||||
## Install from Source
|
||||
|
||||
Source installation is only intended for developers and advanced users. If you do not have a working Golang environment, please follow [How to install Golang](https://docs.minio.io/docs/how-to-install-golang).
|
||||
|
||||
+4
-9
@@ -1,4 +1,4 @@
|
||||
# Minio 快速入门 [](https://slack.minio.io) [](https://goreportcard.com/report/minio/minio) [](https://codecov.io/gh/minio/minio)
|
||||
# Minio 快速入门 [](https://slack.minio.io) [](https://goreportcard.com/report/minio/minio) [](https://codecov.io/gh/minio/minio) [](https://build.snapcraft.io/user/minio/minio)
|
||||
|
||||
Minio是一个对象存储服务,基于Apache License v2.0协议. 它完全兼容亚马逊的S3云储存服务,非常适合于存储很多非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
|
||||
|
||||
@@ -9,13 +9,8 @@ Minio是一个非常轻量的服务,可以很简单的和其他应用的结合
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio|
|
||||
||32-bit Intel|https://dl.minio.io/server/minio/release/linux-386/minio|
|
||||
||32-bit ARM|https://dl.minio.io/server/minio/release/linux-arm/minio|
|
||||
||64-bit ARM|https://dl.minio.io/server/minio/release/linux-arm64/minio|
|
||||
||32-bit ARMv6|https://dl.minio.io/server/minio/release/linux-arm6vl/minio|
|
||||
|Apple OS X|64-bit Intel|https://dl.minio.io/server/minio/release/darwin-amd64/minio|
|
||||
|Microsoft Windows|64-bit|https://dl.minio.io/server/minio/release/windows-amd64/minio.exe|
|
||||
||32-bit|https://dl.minio.io/server/minio/release/windows-386/minio.exe|
|
||||
|FreeBSD|64-bit|https://dl.minio.io/server/minio/release/freebsd-amd64/minio|
|
||||
|
||||
### Homebrew 安装
|
||||
@@ -47,7 +42,7 @@ $ go get -u github.com/minio/minio
|
||||
|
||||
$ chmod +x minio
|
||||
$ ./minio --help
|
||||
$ ./minio server ~/Photos
|
||||
$ ./minio server /data
|
||||
|
||||
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
@@ -75,7 +70,7 @@ SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
|
||||
$ chmod 755 minio
|
||||
$ ./minio --help
|
||||
$ ./minio server ~/Photos
|
||||
$ ./minio server /data
|
||||
|
||||
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
@@ -139,7 +134,7 @@ $ docker run -p 9000:9000 minio/minio
|
||||
|
||||
$ chmod 755 minio
|
||||
$ ./minio --help
|
||||
$ ./minio server ~/Photos
|
||||
$ ./minio server /data
|
||||
|
||||
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
|
||||
+13
-9
@@ -11,12 +11,12 @@ clone_folder: c:\gopath\src\github.com\minio\minio
|
||||
|
||||
# Environment variables
|
||||
environment:
|
||||
GOROOT: c:\go17
|
||||
GOPATH: c:\gopath
|
||||
GOROOT: c:\go
|
||||
|
||||
# scripts that run after cloning repository
|
||||
install:
|
||||
- set PATH=%GOPATH%\bin;c:\go17\bin;%PATH%
|
||||
- set PATH=%GOPATH%\bin;%GOROOT%\bin;%PATH%
|
||||
- go version
|
||||
- go env
|
||||
- python --version
|
||||
@@ -24,6 +24,8 @@ install:
|
||||
# To run your custom scripts instead of automatic MSBuild
|
||||
build_script:
|
||||
# Compile
|
||||
# We need to disable firewall - https://github.com/appveyor/ci/issues/1579#issuecomment-309830648
|
||||
- ps: Disable-NetFirewallRule -DisplayName 'File and Printer Sharing (SMB-Out)'
|
||||
- appveyor AddCompilationMessage "Starting Compile"
|
||||
- cd c:\gopath\src\github.com\minio\minio
|
||||
- go run buildscripts/gen-ldflags.go > temp.txt
|
||||
@@ -38,17 +40,19 @@ test_script:
|
||||
- mkdir build\coverage
|
||||
- go test -v -timeout 17m -race github.com/minio/minio/cmd...
|
||||
- go test -v -race github.com/minio/minio/pkg...
|
||||
- go test -v -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd
|
||||
# FIXME(aead): enable codecov after issue https://github.com/golang/go/issues/18468 is solved.
|
||||
# - go test -v -timeout 17m -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd
|
||||
- ps: Update-AppveyorTest "Unit Tests" -Outcome Passed
|
||||
|
||||
after_test:
|
||||
- go tool cover -html=build\coverage\coverage.txt -o build\coverage\coverage.html
|
||||
- ps: Push-AppveyorArtifact build\coverage\coverage.txt
|
||||
- ps: Push-AppveyorArtifact build\coverage\coverage.html
|
||||
# FIXME(aead): enable codecov after issue https://github.com/golang/go/issues/18468 is solved.
|
||||
# - go tool cover -html=build\coverage\coverage.txt -o build\coverage\coverage.html
|
||||
# - ps: Push-AppveyorArtifact build\coverage\coverage.txt
|
||||
# - ps: Push-AppveyorArtifact build\coverage\coverage.html
|
||||
# Upload coverage report.
|
||||
- "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%"
|
||||
- pip install codecov
|
||||
- codecov -X gcov -f "build\coverage\coverage.txt"
|
||||
# - "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%"
|
||||
# - pip install codecov
|
||||
# - codecov -X gcov -f "build\coverage\coverage.txt"
|
||||
|
||||
# to disable deployment
|
||||
deploy: off
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Minio Browser</title>
|
||||
<link rel="stylesheet" href="/minio/loader.css" type="text/css">
|
||||
<link rel="stylesheet" href="loader.css" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page-load">
|
||||
<div class="pl-inner">
|
||||
<img src="/minio/logo.svg" alt="">
|
||||
<img src="logo.svg" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div id="root"></div>
|
||||
@@ -27,19 +27,19 @@
|
||||
<ul>
|
||||
<li>
|
||||
<a href="http://www.google.com/chrome/">
|
||||
<img src="/minio/chrome.png" alt="">
|
||||
<img src="chrome.png" alt="">
|
||||
<div>Chrome</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.mozilla.org/en-US/firefox/new/">
|
||||
<img src="/minio/firefox.png" alt="">
|
||||
<img src="firefox.png" alt="">
|
||||
<div>Firefox</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.apple.com/safari/">
|
||||
<img src="/minio/safari.png" alt="">
|
||||
<img src="safari.png" alt="">
|
||||
<div>Safari</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -51,6 +51,6 @@
|
||||
<![endif]-->
|
||||
|
||||
<script>currentUiVersion = 'MINIO_UI_VERSION'</script>
|
||||
<script src="/minio/index_bundle.js"></script>
|
||||
<script src="index_bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -81,7 +81,7 @@ ReactDOM.render((
|
||||
<Provider store={ store } web={ web }>
|
||||
<Router history={ browserHistory }>
|
||||
<Route path='/' component={ App }>
|
||||
<Route path='minio' component={ App }>
|
||||
<Route path={ minioBrowserPrefix } component={ App }>
|
||||
<IndexRoute component={ Browse } onEnter={ authNeeded } />
|
||||
<Route path='login' component={ Login } onEnter={ authNotNeeded } />
|
||||
<Route path=':bucket' component={ Browse } onEnter={ authNeeded } />
|
||||
|
||||
@@ -465,7 +465,7 @@ export const uploadFile = (file, xhr) => {
|
||||
return (dispatch, getState) => {
|
||||
const {currentBucket, currentPath} = getState()
|
||||
const objectName = `${currentPath}${file.name}`
|
||||
const uploadUrl = `${window.location.origin}/minio/upload/${currentBucket}/${objectName}`
|
||||
const uploadUrl = `${window.location.origin}${minioBrowserPrefix}/upload/${currentBucket}/${objectName}`
|
||||
// The slug is a unique identifer for the file upload.
|
||||
const slug = `${currentBucket}-${currentPath}-${file.name}`
|
||||
|
||||
@@ -482,7 +482,7 @@ export const uploadFile = (file, xhr) => {
|
||||
}))
|
||||
|
||||
xhr.onload = function(event) {
|
||||
if (xhr.status == 401 || xhr.status == 403 || xhr.status == 500) {
|
||||
if (xhr.status == 401 || xhr.status == 403) {
|
||||
setShowAbortModal(false)
|
||||
dispatch(stopUpload({
|
||||
slug
|
||||
@@ -492,6 +492,16 @@ export const uploadFile = (file, xhr) => {
|
||||
message: 'Unauthorized request.'
|
||||
}))
|
||||
}
|
||||
if (xhr.status == 500) {
|
||||
setShowAbortModal(false)
|
||||
dispatch(stopUpload({
|
||||
slug
|
||||
}))
|
||||
dispatch(showAlert({
|
||||
type: 'danger',
|
||||
message: xhr.responseText
|
||||
}))
|
||||
}
|
||||
if (xhr.status == 200) {
|
||||
setShowAbortModal(false)
|
||||
dispatch(stopUpload({
|
||||
|
||||
@@ -150,7 +150,21 @@ export default class Browse extends React.Component {
|
||||
if (prefix === currentPath) return
|
||||
browserHistory.push(utils.pathJoin(currentBucket, encPrefix))
|
||||
} else {
|
||||
window.location = `${window.location.origin}/minio/download/${currentBucket}/${encPrefix}?token=${storage.getItem('token')}`
|
||||
if (!web.LoggedIn()) {
|
||||
let url = `${window.location.origin}/minio/download/${currentBucket}/${encPrefix}?token=''`
|
||||
window.location = url
|
||||
} else {
|
||||
// Download the selected file.
|
||||
web.CreateURLToken()
|
||||
.then(res => {
|
||||
let url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encPrefix}?token=${res.token}`
|
||||
window.location = url
|
||||
})
|
||||
.catch(err => dispatch(actions.showAlert({
|
||||
type: 'danger',
|
||||
message: err.message
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,8 +384,24 @@ export default class Browse extends React.Component {
|
||||
}
|
||||
|
||||
handleExpireValue(targetInput, inc, object) {
|
||||
inc === -1 ? this.refs[targetInput].stepDown(1) : this.refs[targetInput].stepUp(1)
|
||||
let value = this.refs[targetInput].value
|
||||
let maxValue = (targetInput == 'expireHours') ? 23 : (targetInput == 'expireMins') ? 59 : (targetInput == 'expireDays') ? 7 : 0
|
||||
value = isNaN(value) ? 0 : value
|
||||
|
||||
// Use custom step count to support browser Edge
|
||||
if((inc === -1)) {
|
||||
if(value != 0) {
|
||||
value--
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(value != maxValue) {
|
||||
value++
|
||||
}
|
||||
}
|
||||
this.refs[targetInput].value = value
|
||||
|
||||
// Reset hours and mins when days reaches it's max value
|
||||
if (this.refs.expireDays.value == 7) {
|
||||
this.refs.expireHours.value = 0
|
||||
this.refs.expireMins.value = 0
|
||||
@@ -379,6 +409,7 @@ export default class Browse extends React.Component {
|
||||
if (this.refs.expireDays.value + this.refs.expireHours.value + this.refs.expireMins.value == 0) {
|
||||
this.refs.expireDays.value = 7
|
||||
}
|
||||
|
||||
const {dispatch} = this.props
|
||||
dispatch(actions.shareObject(object, this.refs.expireDays.value, this.refs.expireHours.value, this.refs.expireMins.value))
|
||||
}
|
||||
@@ -389,16 +420,28 @@ export default class Browse extends React.Component {
|
||||
}
|
||||
|
||||
downloadSelected() {
|
||||
const {dispatch} = this.props
|
||||
const {dispatch, web} = this.props
|
||||
let req = {
|
||||
bucketName: this.props.currentBucket,
|
||||
objects: this.props.checkedObjects,
|
||||
prefix: this.props.currentPath
|
||||
}
|
||||
let requestUrl = location.origin + "/minio/zip?token=" + localStorage.token
|
||||
|
||||
this.xhr = new XMLHttpRequest()
|
||||
dispatch(actions.downloadSelected(requestUrl, req, this.xhr))
|
||||
if (!web.LoggedIn()) {
|
||||
let requestUrl = location.origin + "/minio/zip?token=''"
|
||||
this.xhr = new XMLHttpRequest()
|
||||
dispatch(actions.downloadSelected(requestUrl, req, this.xhr))
|
||||
} else {
|
||||
web.CreateURLToken()
|
||||
.then(res => {
|
||||
let requestUrl = location.origin + minioBrowserPrefix + "/zip?token=" + res.token
|
||||
this.xhr = new XMLHttpRequest()
|
||||
dispatch(actions.downloadSelected(requestUrl, req, this.xhr))
|
||||
})
|
||||
.catch(err => dispatch(actions.showAlert({
|
||||
type: 'danger',
|
||||
message: err.message
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
clearSelected() {
|
||||
@@ -459,7 +502,7 @@ export default class Browse extends React.Component {
|
||||
settingsFunc={ this.showSettings.bind(this) }
|
||||
logoutFunc={ this.logout.bind(this) } />
|
||||
} else {
|
||||
loginButton = <a className='btn btn-danger' href='/minio/login'>Login</a>
|
||||
loginButton = <a className='btn btn-danger' href={minioBrowserPrefix+'/login'}>Login</a>
|
||||
}
|
||||
|
||||
if (web.LoggedIn()) {
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
// File for all the browser constants.
|
||||
|
||||
// minioBrowserPrefix absolute path.
|
||||
export const minioBrowserPrefix = '/minio'
|
||||
var p = window.location.pathname
|
||||
export const minioBrowserPrefix = p.slice(0, p.indexOf("/", 1))
|
||||
|
||||
export const READ_ONLY = 'readonly'
|
||||
export const WRITE_ONLY = 'writeonly'
|
||||
export const READ_WRITE = 'readwrite'
|
||||
|
||||
@@ -112,6 +112,9 @@ export default class Web {
|
||||
return res
|
||||
})
|
||||
}
|
||||
CreateURLToken() {
|
||||
return this.makeCall('CreateURLToken')
|
||||
}
|
||||
GetBucketPolicy(args) {
|
||||
return this.makeCall('GetBucketPolicy', args)
|
||||
}
|
||||
|
||||
+13
-15
@@ -35,8 +35,6 @@
|
||||
"esformatter": "^0.10.0",
|
||||
"esformatter-jsx": "^7.4.1",
|
||||
"esformatter-jsx-ignore": "^1.0.6",
|
||||
"expect": "^1.20.2",
|
||||
"history": "^1.17.0",
|
||||
"html-webpack-plugin": "^2.22.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"karma": "^0.13.22",
|
||||
@@ -48,38 +46,38 @@
|
||||
"less": "^2.7.1",
|
||||
"less-loader": "^2.2.3",
|
||||
"mocha": "^2.5.3",
|
||||
"moment": "^2.15.1",
|
||||
"purifycss-webpack-plugin": "^2.0.3",
|
||||
"react": "^0.14.8",
|
||||
"react-addons-test-utils": "^0.14.8",
|
||||
"react-bootstrap": "^0.28.5",
|
||||
"react-custom-scrollbars": "^2.3.0",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^2.8.1",
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^1.0.3",
|
||||
"style-loader": "^0.13.1",
|
||||
"superagent": "^1.8.4",
|
||||
"superagent-es6-promise": "^1.0.0",
|
||||
"url-loader": "^0.5.7",
|
||||
"webpack": "^1.12.11",
|
||||
"webpack-dev-server": "^1.14.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^3.3.6",
|
||||
"classnames": "^2.2.3",
|
||||
"expect": "^1.20.2",
|
||||
"font-awesome": "^4.7.0",
|
||||
"history": "^1.17.0",
|
||||
"humanize": "0.0.9",
|
||||
"json-loader": "^0.5.4",
|
||||
"local-storage-fallback": "^1.3.0",
|
||||
"mime-db": "^1.25.0",
|
||||
"mime-types": "^2.1.13",
|
||||
"moment": "^2.15.1",
|
||||
"react": "^0.14.8",
|
||||
"react-addons-test-utils": "^0.14.8",
|
||||
"react-bootstrap": "^0.28.5",
|
||||
"react-copy-to-clipboard": "^4.2.3",
|
||||
"react-custom-scrollbars": "^2.2.2",
|
||||
"react-dom": "^0.14.6",
|
||||
"react-dropzone": "^3.5.3",
|
||||
"react-infinite-scroller": "^1.0.6",
|
||||
"react-onclickout": "2.0.4"
|
||||
"react-onclickout": "2.0.4",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^2.8.1",
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^1.0.3",
|
||||
"superagent": "^1.8.4",
|
||||
"superagent-es6-promise": "^1.0.0",
|
||||
"webpack": "^1.12.11"
|
||||
}
|
||||
}
|
||||
|
||||
+62
-59
File diff suppressed because one or more lines are too long
+1273
-953
File diff suppressed because it is too large
Load Diff
@@ -1,101 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
_init() {
|
||||
# Save release LDFLAGS
|
||||
LDFLAGS=$(go run buildscripts/gen-ldflags.go)
|
||||
|
||||
# Extract release tag
|
||||
release_tag=$(echo $LDFLAGS | awk {'print $6'} | cut -f2 -d=)
|
||||
|
||||
# Verify release tag.
|
||||
if [ -z "$release_tag" ]; then
|
||||
echo "Release tag cannot be empty. Please check return value of \`go run buildscripts/gen-ldflags.go\`"
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# Extract release string.
|
||||
release_str=$(echo $MINIO_RELEASE | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Verify release string.
|
||||
if [ -z "$release_str" ]; then
|
||||
echo "Release string cannot be empty. Please set \`MINIO_RELEASE\` env variable."
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# List of supported architectures
|
||||
SUPPORTED_OSARCH='linux/386 linux/amd64 linux/arm linux/arm64 windows/386 windows/amd64 darwin/amd64 freebsd/amd64'
|
||||
|
||||
## System binaries
|
||||
CP=`which cp`
|
||||
SHASUM=`which shasum`
|
||||
SHA256SUM="${SHASUM} -a 256"
|
||||
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 sha1sum name
|
||||
release_shasum="$release_str/$os-$arch/$(basename $package).${release_tag}.shasum"
|
||||
# Release sha1sum default
|
||||
release_shasum_default="$release_str/$os-$arch/$(basename $package).shasum"
|
||||
|
||||
# Release sha256sum name
|
||||
release_sha256sum="$release_str/$os-$arch/$(basename $package).${release_tag}.sha256sum"
|
||||
# Release sha256sum default
|
||||
release_sha256sum_default="$release_str/$os-$arch/$(basename $package).sha256sum"
|
||||
|
||||
# Go build to build the binary.
|
||||
CGO_ENABLED=0 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 sha1sum
|
||||
shasum_str=$(${SHASUM} ${release_bin})
|
||||
echo ${shasum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_shasum
|
||||
$CP -p $release_shasum $release_shasum_default
|
||||
|
||||
# Calculate sha256sum
|
||||
sha256sum_str=$(${SHA256SUM} ${release_bin})
|
||||
echo ${sha256sum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_sha256sum
|
||||
$CP -p $release_sha256sum $release_sha256sum_default
|
||||
}
|
||||
|
||||
main() {
|
||||
# Build releases.
|
||||
echo "Executing $release_str builds for OS: ${SUPPORTED_OSARCH}"
|
||||
echo "Choose an OS Arch from the below"
|
||||
for osarch in ${SUPPORTED_OSARCH}; do
|
||||
echo ${osarch}
|
||||
done
|
||||
|
||||
read -p "If you want to build for all, Just press Enter: " chosen_osarch
|
||||
if [ "$chosen_osarch" = "" ] || [ "$chosen_osarch" = "all" ]; then
|
||||
for each_osarch in ${SUPPORTED_OSARCH}; do
|
||||
go_build ${each_osarch}
|
||||
done
|
||||
else
|
||||
for each_osarch in $(echo $chosen_osarch | sed 's/,/ /g'); do
|
||||
go_build ${each_osarch}
|
||||
done
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
# Run main.
|
||||
_init && main
|
||||
@@ -21,7 +21,7 @@ _init() {
|
||||
|
||||
## Minimum required versions for build dependencies
|
||||
GIT_VERSION="1.0"
|
||||
GO_VERSION="1.7.1"
|
||||
GO_VERSION="1.9.1"
|
||||
OSX_VERSION="10.8"
|
||||
KNAME=$(uname -s)
|
||||
ARCH=$(uname -m)
|
||||
|
||||
Executable
+237
@@ -0,0 +1,237 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Minio Cloud Storage, (C) 2017 Minio, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
set -e
|
||||
set -E
|
||||
set -o pipefail
|
||||
|
||||
if [ ! -x "$PWD/minio" ]; then
|
||||
echo "minio executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||
|
||||
export MINT_MODE=core
|
||||
export MINT_DATA_DIR="$WORK_DIR/data"
|
||||
export SERVER_ENDPOINT="127.0.0.1:9000"
|
||||
export ACCESS_KEY="minio"
|
||||
export SECRET_KEY="minio123"
|
||||
export ENABLE_HTTPS=0
|
||||
|
||||
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
||||
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" )
|
||||
|
||||
FILE_1_MB="$MINT_DATA_DIR/datafile-1-MB"
|
||||
FILE_65_MB="$MINT_DATA_DIR/datafile-65-MB"
|
||||
|
||||
FUNCTIONAL_TESTS="$WORK_DIR/functional-tests.sh"
|
||||
|
||||
function start_minio_fs()
|
||||
{
|
||||
"${MINIO[@]}" server "${WORK_DIR}/fs-disk" >"$WORK_DIR/fs-minio.log" 2>&1 &
|
||||
minio_pid=$!
|
||||
sleep 3
|
||||
|
||||
echo "$minio_pid"
|
||||
}
|
||||
|
||||
function start_minio_xl()
|
||||
{
|
||||
"${MINIO[@]}" server "${WORK_DIR}/xl-disk1" "${WORK_DIR}/xl-disk2" "${WORK_DIR}/xl-disk3" "${WORK_DIR}/xl-disk4" >"$WORK_DIR/xl-minio.log" 2>&1 &
|
||||
minio_pid=$!
|
||||
sleep 3
|
||||
|
||||
echo "$minio_pid"
|
||||
}
|
||||
|
||||
function start_minio_dist()
|
||||
{
|
||||
declare -a minio_pids
|
||||
"${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9000.log" 2>&1 &
|
||||
minio_pids[0]=$!
|
||||
"${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9001.log" 2>&1 &
|
||||
minio_pids[1]=$!
|
||||
"${MINIO[@]}" server --address=:9002 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9002.log" 2>&1 &
|
||||
minio_pids[2]=$!
|
||||
"${MINIO[@]}" server --address=:9003 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9003.log" 2>&1 &
|
||||
minio_pids[3]=$!
|
||||
|
||||
sleep 30
|
||||
echo "${minio_pids[@]}"
|
||||
}
|
||||
|
||||
function start_minio_gateway_s3()
|
||||
{
|
||||
MINIO_ACCESS_KEY=Q3AM3UQ867SPQQA43P2F MINIO_SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG \
|
||||
"${MINIO[@]}" gateway s3 https://play.minio.io:9000 >"$WORK_DIR/minio-gateway-s3.log" 2>&1 &
|
||||
minio_pid=$!
|
||||
sleep 3
|
||||
|
||||
echo "$minio_pid"
|
||||
}
|
||||
|
||||
function run_test_fs()
|
||||
{
|
||||
minio_pid="$(start_minio_fs)"
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
kill "$minio_pid"
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
cat "$WORK_DIR/fs-minio.log"
|
||||
fi
|
||||
rm -f "$WORK_DIR/fs-minio.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_xl()
|
||||
{
|
||||
minio_pid="$(start_minio_xl)"
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
kill "$minio_pid"
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
cat "$WORK_DIR/xl-minio.log"
|
||||
fi
|
||||
rm -f "$WORK_DIR/xl-minio.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_dist()
|
||||
{
|
||||
minio_pids=( $(start_minio_dist) )
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
kill "$pid"
|
||||
done
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
echo "server1 log:"
|
||||
cat "$WORK_DIR/dist-minio-9000.log"
|
||||
echo "server2 log:"
|
||||
cat "$WORK_DIR/dist-minio-9001.log"
|
||||
echo "server3 log:"
|
||||
cat "$WORK_DIR/dist-minio-9002.log"
|
||||
echo "server4 log:"
|
||||
cat "$WORK_DIR/dist-minio-9003.log"
|
||||
fi
|
||||
|
||||
rm -f "$WORK_DIR/dist-minio-9000.log" "$WORK_DIR/dist-minio-9001.log" "$WORK_DIR/dist-minio-9002.log" "$WORK_DIR/dist-minio-9003.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_gateway_s3()
|
||||
{
|
||||
minio_pid="$(start_minio_gateway_s3)"
|
||||
|
||||
export ACCESS_KEY=Q3AM3UQ867SPQQA43P2F
|
||||
export SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
kill "$minio_pid"
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
cat "$WORK_DIR/minio-gateway-s3.log"
|
||||
fi
|
||||
rm -f "$WORK_DIR/minio-gateway-s3.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function __init__()
|
||||
{
|
||||
echo "Initializing environment"
|
||||
mkdir -p "$WORK_DIR"
|
||||
mkdir -p "$MINIO_CONFIG_DIR"
|
||||
mkdir -p "$MINT_DATA_DIR"
|
||||
|
||||
if ! go get -u github.com/minio/mc; then
|
||||
echo "failed to download https://github.com/minio/mc"
|
||||
exit 1
|
||||
fi
|
||||
/bin/cp -a "$(go env GOPATH)"/bin/mc "$WORK_DIR/mc"
|
||||
|
||||
chmod a+x "$WORK_DIR/mc"
|
||||
|
||||
shred -n 1 -s 1M - 1>"$FILE_1_MB" 2>/dev/null
|
||||
shred -n 1 -s 65M - 1>"$FILE_65_MB" 2>/dev/null
|
||||
|
||||
## version is purposefully set to '3' for minio to migrate configuration file
|
||||
echo '{"version": "3", "credential": {"accessKey": "minio", "secretKey": "minio123"}, "region": "us-east-1"}' > "$MINIO_CONFIG_DIR/config.json"
|
||||
|
||||
if ! wget -q -O "$FUNCTIONAL_TESTS" https://raw.githubusercontent.com/minio/mc/master/functional-tests.sh; then
|
||||
echo "failed to download https://raw.githubusercontent.com/minio/mc/master/functional-tests.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod a+x "$FUNCTIONAL_TESTS"
|
||||
}
|
||||
|
||||
function main()
|
||||
{
|
||||
echo "Testing in FS setup"
|
||||
if ! run_test_fs; then
|
||||
echo "FAILED"
|
||||
rm -fr "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in XL setup"
|
||||
if ! run_test_xl; then
|
||||
echo "FAILED"
|
||||
rm -fr "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Distribute XL setup"
|
||||
if ! run_test_dist; then
|
||||
echo "FAILED"
|
||||
rm -fr "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Gateway S3 setup"
|
||||
if ! run_test_gateway_s3; then
|
||||
echo "FAILED"
|
||||
rm -fr "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -fr "$WORK_DIR"
|
||||
}
|
||||
|
||||
( __init__ "$@" && main "$@" )
|
||||
rv=$?
|
||||
rm -fr "$WORK_DIR"
|
||||
exit "$rv"
|
||||
+38
-5
@@ -28,6 +28,8 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -168,7 +170,7 @@ func (adminAPI adminAPIHandlers) ServiceCredentialsHandler(w http.ResponseWriter
|
||||
return
|
||||
}
|
||||
|
||||
creds, err := createCredential(req.Username, req.Password)
|
||||
creds, err := auth.CreateCredentials(req.Username, req.Password)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -181,8 +183,14 @@ func (adminAPI adminAPIHandlers) ServiceCredentialsHandler(w http.ResponseWriter
|
||||
}
|
||||
|
||||
// Update local credentials in memory.
|
||||
serverConfig.SetCredential(creds)
|
||||
prevCred := serverConfig.SetCredential(creds)
|
||||
|
||||
// Save credentials to config file
|
||||
if err = serverConfig.Save(); err != nil {
|
||||
// Save the current creds when failed to update.
|
||||
serverConfig.SetCredential(prevCred)
|
||||
|
||||
errorIf(err, "Unable to update the config with new credentials.")
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -832,15 +840,19 @@ func (adminAPI adminAPIHandlers) HealFormatHandler(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
// Wrap into retrying disks
|
||||
retryingDisks := initRetryableStorageDisks(bootstrapDisks,
|
||||
time.Millisecond, time.Millisecond*5)
|
||||
|
||||
// Heal format.json on available storage.
|
||||
err = healFormatXL(bootstrapDisks)
|
||||
err = healFormatXL(retryingDisks)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Instantiate new object layer with newly formatted storage.
|
||||
newObjectAPI, err := newXLObjects(bootstrapDisks)
|
||||
newObjectAPI, err := newXLObjects(retryingDisks)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -973,6 +985,24 @@ func (adminAPI adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
|
||||
var config serverConfigV20
|
||||
err = json.Unmarshal(configBytes, &config)
|
||||
|
||||
if err != nil {
|
||||
errorIf(err, "Failed to unmarshal config from request body.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if globalIsEnvCreds {
|
||||
creds := serverConfig.GetCredential()
|
||||
if config.Credential.AccessKey != creds.AccessKey ||
|
||||
config.Credential.SecretKey != creds.SecretKey {
|
||||
writeErrorResponse(w, ErrAdminCredentialsMismatch, r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Write config received from request onto a temporary file on
|
||||
// all nodes.
|
||||
tmpFileName := fmt.Sprintf(minioConfigTmpFormat, mustGetUUID())
|
||||
@@ -989,7 +1019,10 @@ func (adminAPI adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http
|
||||
// bucket name and wouldn't conflict with normal object
|
||||
// operations.
|
||||
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
|
||||
configLock.Lock()
|
||||
if configLock.GetLock(globalObjectTimeout) != nil {
|
||||
writeErrorResponse(w, ErrOperationTimedOut, r.URL)
|
||||
return
|
||||
}
|
||||
defer configLock.Unlock()
|
||||
|
||||
// Rename the temporary config file to config.json
|
||||
|
||||
@@ -26,11 +26,13 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
router "github.com/gorilla/mux"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
var configJSON = []byte(`{
|
||||
@@ -182,7 +184,7 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
||||
// TearDown - method that resets the test bed for subsequent unit
|
||||
// tests to start afresh.
|
||||
func (atb *adminXLTestBed) TearDown() {
|
||||
removeAll(atb.configPath)
|
||||
os.RemoveAll(atb.configPath)
|
||||
removeRoots(atb.xlDirs)
|
||||
resetTestGlobals()
|
||||
}
|
||||
@@ -262,7 +264,7 @@ func testServiceSignalReceiver(cmd cmdType, t *testing.T) {
|
||||
|
||||
// getServiceCmdRequest - Constructs a management REST API request for service
|
||||
// subcommands for a given cmdType value.
|
||||
func getServiceCmdRequest(cmd cmdType, cred credential, body []byte) (*http.Request, error) {
|
||||
func getServiceCmdRequest(cmd cmdType, cred auth.Credentials, body []byte) (*http.Request, error) {
|
||||
req, err := newTestRequest(cmd.apiMethod(), "/?service", 0, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -942,7 +944,7 @@ func TestHealObjectHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
_, err = adminTestBed.objLayer.PutObject(bucketName, objName,
|
||||
int64(len("hello")), bytes.NewReader([]byte("hello")), nil, "")
|
||||
mustGetHashReader(t, bytes.NewReader([]byte("hello")), int64(len("hello")), "", ""), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create %s - %v", objName, err)
|
||||
}
|
||||
@@ -1082,7 +1084,7 @@ func TestHealUploadHandler(t *testing.T) {
|
||||
// Upload a part.
|
||||
partID := 1
|
||||
_, err = adminTestBed.objLayer.PutObjectPart(bucketName, objName, uploadID,
|
||||
partID, int64(len("hello")), bytes.NewReader([]byte("hello")), "", "")
|
||||
partID, mustGetHashReader(t, bytes.NewReader([]byte("hello")), int64(len("hello")), "", ""))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to upload part %d of %s/%s - %v", partID,
|
||||
bucketName, objName, err)
|
||||
@@ -1363,7 +1365,7 @@ func TestWriteSetConfigResponse(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
testCases := []struct {
|
||||
status bool
|
||||
errs []error
|
||||
|
||||
@@ -113,15 +113,15 @@ func (rc remoteAdminClient) ReInitDisks() error {
|
||||
}
|
||||
|
||||
// ServerInfoData - Returns the server info of this server.
|
||||
func (lc localAdminClient) ServerInfoData() (ServerInfoData, error) {
|
||||
func (lc localAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
|
||||
if globalBootTime.IsZero() {
|
||||
return ServerInfoData{}, errServerNotInitialized
|
||||
return sid, errServerNotInitialized
|
||||
}
|
||||
|
||||
// Build storage info
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
return ServerInfoData{}, errServerNotInitialized
|
||||
return sid, errServerNotInitialized
|
||||
}
|
||||
storage := objLayer.StorageInfo()
|
||||
|
||||
@@ -145,12 +145,12 @@ func (lc localAdminClient) ServerInfoData() (ServerInfoData, error) {
|
||||
}
|
||||
|
||||
// ServerInfo - returns the server info of the server to which the RPC call is made.
|
||||
func (rc remoteAdminClient) ServerInfoData() (ServerInfoData, error) {
|
||||
func (rc remoteAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
|
||||
args := AuthRPCArgs{}
|
||||
reply := ServerInfoDataReply{}
|
||||
err := rc.Call(serverInfoDataRPC, &args, &reply)
|
||||
if err != nil {
|
||||
return ServerInfoData{}, err
|
||||
return sid, err
|
||||
}
|
||||
|
||||
return reply.ServerInfoData, nil
|
||||
@@ -493,7 +493,7 @@ func getPeerConfig(peers adminPeers) ([]byte, error) {
|
||||
|
||||
// getValidServerConfig - finds the server config that is present in
|
||||
// quorum or more number of servers.
|
||||
func getValidServerConfig(serverConfigs []serverConfigV13, errs []error) (serverConfigV13, error) {
|
||||
func getValidServerConfig(serverConfigs []serverConfigV13, errs []error) (scv serverConfigV13, e error) {
|
||||
// majority-based quorum
|
||||
quorum := len(serverConfigs)/2 + 1
|
||||
|
||||
@@ -566,7 +566,7 @@ func getValidServerConfig(serverConfigs []serverConfigV13, errs []error) (server
|
||||
|
||||
// If quorum nodes don't agree.
|
||||
if maxOccurrence < quorum {
|
||||
return serverConfigV13{}, errXLWriteQuorum
|
||||
return scv, errXLWriteQuorum
|
||||
}
|
||||
|
||||
return configJSON, nil
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/rpc"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -236,7 +235,7 @@ func (s *adminCmd) CommitConfig(cArgs *CommitConfigArgs, cReply *CommitConfigRep
|
||||
// stop and restart commands.
|
||||
func registerAdminRPCRouter(mux *router.Router) error {
|
||||
adminRPCHandler := &adminCmd{}
|
||||
adminRPCServer := rpc.NewServer()
|
||||
adminRPCServer := newRPCServer()
|
||||
err := adminRPCServer.RegisterName("Admin", adminRPCHandler)
|
||||
if err != nil {
|
||||
return traceError(err)
|
||||
|
||||
@@ -18,6 +18,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -30,18 +31,21 @@ func testAdminCmd(cmd cmdType, t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test config - %v", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
creds := serverConfig.GetCredential()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
adminServer := adminCmd{}
|
||||
creds := serverConfig.GetCredential()
|
||||
args := LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
reply := LoginRPCReply{}
|
||||
err = adminServer.Login(&args, &reply)
|
||||
err = adminServer.Login(&args, &LoginRPCReply{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login to admin server - %v", err)
|
||||
}
|
||||
@@ -51,7 +55,7 @@ func testAdminCmd(cmd cmdType, t *testing.T) {
|
||||
<-globalServiceSignalCh
|
||||
}()
|
||||
|
||||
ga := AuthRPCArgs{AuthToken: reply.AuthToken}
|
||||
ga := AuthRPCArgs{AuthToken: token}
|
||||
genReply := AuthRPCReply{}
|
||||
switch cmd {
|
||||
case restartCmd:
|
||||
@@ -75,7 +79,7 @@ func TestReInitDisks(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize server config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// Initializing objectLayer for HealFormatHandler.
|
||||
_, xlDirs, xlErr := initTestXLObjLayer()
|
||||
@@ -90,21 +94,25 @@ func TestReInitDisks(t *testing.T) {
|
||||
// Setup admin rpc server for an XL backend.
|
||||
globalIsXL = true
|
||||
adminServer := adminCmd{}
|
||||
|
||||
creds := serverConfig.GetCredential()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
args := LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
reply := LoginRPCReply{}
|
||||
err = adminServer.Login(&args, &reply)
|
||||
err = adminServer.Login(&args, &LoginRPCReply{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login to admin server - %v", err)
|
||||
}
|
||||
|
||||
authArgs := AuthRPCArgs{
|
||||
AuthToken: reply.AuthToken,
|
||||
AuthToken: token,
|
||||
}
|
||||
authReply := AuthRPCReply{}
|
||||
|
||||
@@ -113,12 +121,15 @@ func TestReInitDisks(t *testing.T) {
|
||||
t.Errorf("Expected to pass, but failed with %v", err)
|
||||
}
|
||||
|
||||
token, err = authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Negative test case with admin rpc server setup for FS.
|
||||
globalIsXL = false
|
||||
fsAdminServer := adminCmd{}
|
||||
fsArgs := LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
@@ -129,7 +140,7 @@ func TestReInitDisks(t *testing.T) {
|
||||
}
|
||||
|
||||
authArgs = AuthRPCArgs{
|
||||
AuthToken: fsReply.AuthToken,
|
||||
AuthToken: token,
|
||||
}
|
||||
authReply = AuthRPCReply{}
|
||||
// Attempt ReInitDisks service on a FS backend.
|
||||
@@ -149,13 +160,18 @@ func TestGetConfig(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize server config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
adminServer := adminCmd{}
|
||||
creds := serverConfig.GetCredential()
|
||||
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
args := LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
@@ -166,7 +182,7 @@ func TestGetConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
authArgs := AuthRPCArgs{
|
||||
AuthToken: reply.AuthToken,
|
||||
AuthToken: token,
|
||||
}
|
||||
|
||||
configReply := ConfigReply{}
|
||||
@@ -193,13 +209,16 @@ func TestWriteAndCommitConfig(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize server config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
adminServer := adminCmd{}
|
||||
creds := serverConfig.GetCredential()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args := LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
@@ -214,7 +233,7 @@ func TestWriteAndCommitConfig(t *testing.T) {
|
||||
tmpFileName := mustGetUUID()
|
||||
wArgs := WriteConfigArgs{
|
||||
AuthRPCArgs: AuthRPCArgs{
|
||||
AuthToken: reply.AuthToken,
|
||||
AuthToken: token,
|
||||
},
|
||||
TmpFileName: tmpFileName,
|
||||
Buf: buf,
|
||||
@@ -231,7 +250,7 @@ func TestWriteAndCommitConfig(t *testing.T) {
|
||||
|
||||
cArgs := CommitConfigArgs{
|
||||
AuthRPCArgs: AuthRPCArgs{
|
||||
AuthToken: reply.AuthToken,
|
||||
AuthToken: token,
|
||||
},
|
||||
FileName: tmpFileName,
|
||||
}
|
||||
|
||||
+158
-11
@@ -19,6 +19,9 @@ package cmd
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
// APIError structure
|
||||
@@ -114,9 +117,23 @@ const (
|
||||
ErrInvalidQueryParams
|
||||
ErrBucketAlreadyOwnedByYou
|
||||
ErrInvalidDuration
|
||||
ErrNotSupported
|
||||
ErrBucketAlreadyExists
|
||||
ErrMetadataTooLarge
|
||||
ErrUnsupportedMetadata
|
||||
ErrMaximumExpires
|
||||
// Add new error codes here.
|
||||
|
||||
// Server-Side-Encryption (with Customer provided key) related API errors.
|
||||
|
||||
ErrInsecureSSECustomerRequest
|
||||
ErrSSEEncryptedObject
|
||||
ErrInvalidEncryptionParameters
|
||||
ErrInvalidSSECustomerAlgorithm
|
||||
ErrInvalidSSECustomerKey
|
||||
ErrMissingSSECustomerKey
|
||||
ErrMissingSSECustomerKeyMD5
|
||||
ErrSSECustomerKeyMD5Mismatch
|
||||
|
||||
// Bucket notification related errors.
|
||||
ErrEventNotification
|
||||
ErrARNNotification
|
||||
@@ -127,6 +144,7 @@ const (
|
||||
ErrFilterNameSuffix
|
||||
ErrFilterValueInvalid
|
||||
ErrOverlappingConfigs
|
||||
ErrUnsupportedNotification
|
||||
|
||||
// S3 extended errors.
|
||||
ErrContentSHA256Mismatch
|
||||
@@ -142,6 +160,9 @@ const (
|
||||
ErrInvalidObjectName
|
||||
ErrInvalidResourceName
|
||||
ErrServerNotInitialized
|
||||
ErrOperationTimedOut
|
||||
ErrPartsSizeUnequal
|
||||
ErrInvalidRequest
|
||||
// Add new extended error codes here.
|
||||
// Please open a https://github.com/minio/minio/issues before adding
|
||||
// new error codes here.
|
||||
@@ -149,7 +170,9 @@ const (
|
||||
ErrAdminInvalidAccessKey
|
||||
ErrAdminInvalidSecretKey
|
||||
ErrAdminConfigNoQuorum
|
||||
ErrAdminCredentialsMismatch
|
||||
ErrInsecureClientRequest
|
||||
ErrObjectTampered
|
||||
)
|
||||
|
||||
// error code to APIError structure, these fields carry respective
|
||||
@@ -317,7 +340,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
},
|
||||
ErrInvalidPart: {
|
||||
Code: "InvalidPart",
|
||||
Description: "One or more of the specified parts could not be found.",
|
||||
Description: "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidPartOrder: {
|
||||
@@ -355,6 +378,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "The bucket you tried to delete is not empty",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrBucketAlreadyExists: {
|
||||
Code: "BucketAlreadyExists",
|
||||
Description: "The requested bucket name is not available. The bucket namespace is shared by all users of the system. Please select a different name and try again.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrAllAccessDisabled: {
|
||||
Code: "AllAccessDisabled",
|
||||
Description: "All access to this bucket has been disabled.",
|
||||
@@ -545,6 +573,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Configurations overlap. Configurations on the same bucket cannot share a common event type.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrUnsupportedNotification: {
|
||||
Code: "UnsupportedNotification",
|
||||
Description: "Minio server does not support Topic or Cloud Function based notifications.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidCopyPartRange: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "The x-amz-copy-source-range value must be of the form bytes=first-last where first and last are the zero-based offsets of the first and last bytes to copy",
|
||||
@@ -555,6 +588,51 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Range specified is not valid for source object",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMetadataTooLarge: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Your metadata headers exceed the maximum allowed metadata size.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInsecureSSECustomerRequest: {
|
||||
Code: "InvalidRequest",
|
||||
Description: errInsecureSSERequest.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSSEEncryptedObject: {
|
||||
Code: "InvalidRequest",
|
||||
Description: errEncryptedObject.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidEncryptionParameters: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "The encryption parameters are not applicable to this object.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidSSECustomerAlgorithm: {
|
||||
Code: "InvalidArgument",
|
||||
Description: errInvalidSSEAlgorithm.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidSSECustomerKey: {
|
||||
Code: "InvalidArgument",
|
||||
Description: errInvalidSSEKey.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingSSECustomerKey: {
|
||||
Code: "InvalidArgument",
|
||||
Description: errMissingSSEKey.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingSSECustomerKeyMD5: {
|
||||
Code: "InvalidArgument",
|
||||
Description: errMissingSSEKeyMD5.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSSECustomerKeyMD5Mismatch: {
|
||||
Code: "InvalidArgument",
|
||||
Description: errSSEKeyMD5Mismatch.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
/// S3 extensions.
|
||||
ErrContentSHA256Mismatch: {
|
||||
@@ -566,7 +644,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
/// Minio extensions.
|
||||
ErrStorageFull: {
|
||||
Code: "XMinioStorageFull",
|
||||
Description: "Storage backend has reached its minimum free disk threshold. Please delete few objects to proceed.",
|
||||
Description: "Storage backend has reached its minimum free disk threshold. Please delete a few objects to proceed.",
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
ErrObjectExistsAsDirectory: {
|
||||
@@ -619,11 +697,49 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Configuration update failed because server quorum was not met",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrAdminCredentialsMismatch: {
|
||||
Code: "XMinioAdminCredentialsMismatch",
|
||||
Description: "Credentials in config mismatch with server environment variables",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrInsecureClientRequest: {
|
||||
Code: "XMinioInsecureClientRequest",
|
||||
Description: "Cannot respond to plain-text request from TLS-encrypted server",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrOperationTimedOut: {
|
||||
Code: "XMinioServerTimedOut",
|
||||
Description: "A timeout occurred while trying to lock a resource",
|
||||
HTTPStatusCode: http.StatusRequestTimeout,
|
||||
},
|
||||
ErrUnsupportedMetadata: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Your metadata headers are not supported.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrPartsSizeUnequal: {
|
||||
Code: "XMinioPartsSizeUnequal",
|
||||
Description: "All parts except the last part should be of the same size.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrObjectTampered: {
|
||||
Code: "XMinioObjectTampered",
|
||||
Description: errObjectTampered.Error(),
|
||||
HTTPStatusCode: http.StatusPartialContent,
|
||||
},
|
||||
ErrMaximumExpires: {
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
// Generic Invalid-Request error. Should be used for response errors only for unlikely
|
||||
// corner case errors for which introducing new APIErrorCode is not worth it. errorIf()
|
||||
// should be used to log the error at the source of the error for debugging purposes.
|
||||
ErrInvalidRequest: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "Invalid Request",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
// Add your error structure here.
|
||||
}
|
||||
@@ -641,15 +757,13 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
switch err {
|
||||
case errSignatureMismatch:
|
||||
apiErr = ErrSignatureDoesNotMatch
|
||||
case errContentSHA256Mismatch:
|
||||
apiErr = ErrContentSHA256Mismatch
|
||||
case errDataTooLarge:
|
||||
apiErr = ErrEntityTooLarge
|
||||
case errDataTooSmall:
|
||||
apiErr = ErrEntityTooSmall
|
||||
case errInvalidAccessKeyLength:
|
||||
case auth.ErrInvalidAccessKeyLength:
|
||||
apiErr = ErrAdminInvalidAccessKey
|
||||
case errInvalidSecretKeyLength:
|
||||
case auth.ErrInvalidSecretKeyLength:
|
||||
apiErr = ErrAdminInvalidSecretKey
|
||||
}
|
||||
|
||||
@@ -658,11 +772,34 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
return apiErr
|
||||
}
|
||||
|
||||
switch err { // SSE errors
|
||||
case errInsecureSSERequest:
|
||||
return ErrInsecureSSECustomerRequest
|
||||
case errInvalidSSEAlgorithm:
|
||||
return ErrInvalidSSECustomerAlgorithm
|
||||
case errInvalidSSEKey:
|
||||
return ErrInvalidSSECustomerKey
|
||||
case errMissingSSEKey:
|
||||
return ErrMissingSSECustomerKey
|
||||
case errMissingSSEKeyMD5:
|
||||
return ErrMissingSSECustomerKeyMD5
|
||||
case errSSEKeyMD5Mismatch:
|
||||
return ErrSSECustomerKeyMD5Mismatch
|
||||
case errObjectTampered:
|
||||
return ErrObjectTampered
|
||||
case errEncryptedObject:
|
||||
return ErrSSEEncryptedObject
|
||||
case errSSEKeyMismatch:
|
||||
return ErrAccessDenied // no access without correct key
|
||||
}
|
||||
|
||||
switch err.(type) {
|
||||
case StorageFull:
|
||||
apiErr = ErrStorageFull
|
||||
case BadDigest:
|
||||
case hash.BadDigest:
|
||||
apiErr = ErrBadDigest
|
||||
case AllAccessDisabled:
|
||||
apiErr = ErrAllAccessDisabled
|
||||
case IncompleteBody:
|
||||
apiErr = ErrIncompleteBody
|
||||
case ObjectExistsAsDirectory:
|
||||
@@ -677,6 +814,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrBucketAlreadyOwnedByYou
|
||||
case BucketNotEmpty:
|
||||
apiErr = ErrBucketNotEmpty
|
||||
case BucketAlreadyExists:
|
||||
apiErr = ErrBucketAlreadyExists
|
||||
case BucketExists:
|
||||
apiErr = ErrBucketAlreadyOwnedByYou
|
||||
case ObjectNotFound:
|
||||
@@ -701,18 +840,26 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrNoSuchUpload
|
||||
case PartTooSmall:
|
||||
apiErr = ErrEntityTooSmall
|
||||
case SHA256Mismatch:
|
||||
case SignatureDoesNotMatch:
|
||||
apiErr = ErrSignatureDoesNotMatch
|
||||
case hash.SHA256Mismatch:
|
||||
apiErr = ErrContentSHA256Mismatch
|
||||
case ObjectTooLarge:
|
||||
apiErr = ErrEntityTooLarge
|
||||
case ObjectTooSmall:
|
||||
apiErr = ErrEntityTooSmall
|
||||
case NotSupported:
|
||||
apiErr = ErrNotSupported
|
||||
case NotImplemented:
|
||||
apiErr = ErrNotImplemented
|
||||
case PolicyNotFound:
|
||||
apiErr = ErrNoSuchBucketPolicy
|
||||
case PartTooBig:
|
||||
apiErr = ErrEntityTooLarge
|
||||
case UnsupportedMetadata:
|
||||
apiErr = ErrUnsupportedMetadata
|
||||
case PartsSizeUnequal:
|
||||
apiErr = ErrPartsSizeUnequal
|
||||
case BucketPolicyNotFound:
|
||||
apiErr = ErrNoSuchBucketPolicy
|
||||
default:
|
||||
apiErr = ErrInternalError
|
||||
}
|
||||
|
||||
+43
-113
@@ -19,122 +19,52 @@ package cmd
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
var toAPIErrorCodeTests = []struct {
|
||||
err error
|
||||
errCode APIErrorCode
|
||||
}{
|
||||
{err: hash.BadDigest{}, errCode: ErrBadDigest},
|
||||
{err: hash.SHA256Mismatch{}, errCode: ErrContentSHA256Mismatch},
|
||||
{err: IncompleteBody{}, errCode: ErrIncompleteBody},
|
||||
{err: ObjectExistsAsDirectory{}, errCode: ErrObjectExistsAsDirectory},
|
||||
{err: BucketNameInvalid{}, errCode: ErrInvalidBucketName},
|
||||
{err: BucketExists{}, errCode: ErrBucketAlreadyOwnedByYou},
|
||||
{err: ObjectNotFound{}, errCode: ErrNoSuchKey},
|
||||
{err: ObjectNameInvalid{}, errCode: ErrInvalidObjectName},
|
||||
{err: InvalidUploadID{}, errCode: ErrNoSuchUpload},
|
||||
{err: InvalidPart{}, errCode: ErrInvalidPart},
|
||||
{err: InsufficientReadQuorum{}, errCode: ErrReadQuorum},
|
||||
{err: InsufficientWriteQuorum{}, errCode: ErrWriteQuorum},
|
||||
{err: UnsupportedDelimiter{}, errCode: ErrNotImplemented},
|
||||
{err: InvalidMarkerPrefixCombination{}, errCode: ErrNotImplemented},
|
||||
{err: InvalidUploadIDKeyCombination{}, errCode: ErrNotImplemented},
|
||||
{err: MalformedUploadID{}, errCode: ErrNoSuchUpload},
|
||||
{err: PartTooSmall{}, errCode: ErrEntityTooSmall},
|
||||
{err: BucketNotEmpty{}, errCode: ErrBucketNotEmpty},
|
||||
{err: BucketNotFound{}, errCode: ErrNoSuchBucket},
|
||||
{err: StorageFull{}, errCode: ErrStorageFull},
|
||||
{err: NotImplemented{}, errCode: ErrNotImplemented},
|
||||
{err: errSignatureMismatch, errCode: ErrSignatureDoesNotMatch},
|
||||
|
||||
// SSE-C errors
|
||||
{err: errInsecureSSERequest, errCode: ErrInsecureSSECustomerRequest},
|
||||
{err: errInvalidSSEAlgorithm, errCode: ErrInvalidSSECustomerAlgorithm},
|
||||
{err: errMissingSSEKey, errCode: ErrMissingSSECustomerKey},
|
||||
{err: errInvalidSSEKey, errCode: ErrInvalidSSECustomerKey},
|
||||
{err: errMissingSSEKeyMD5, errCode: ErrMissingSSECustomerKeyMD5},
|
||||
{err: errSSEKeyMD5Mismatch, errCode: ErrSSECustomerKeyMD5Mismatch},
|
||||
{err: errObjectTampered, errCode: ErrObjectTampered},
|
||||
|
||||
{err: nil, errCode: ErrNone},
|
||||
{err: errors.New("Custom error"), errCode: ErrInternalError}, // Case where err type is unknown.
|
||||
}
|
||||
|
||||
func TestAPIErrCode(t *testing.T) {
|
||||
testCases := []struct {
|
||||
err error
|
||||
errCode APIErrorCode
|
||||
}{
|
||||
// Valid cases.
|
||||
{
|
||||
BadDigest{},
|
||||
ErrBadDigest,
|
||||
},
|
||||
{
|
||||
IncompleteBody{},
|
||||
ErrIncompleteBody,
|
||||
},
|
||||
{
|
||||
ObjectExistsAsDirectory{},
|
||||
ErrObjectExistsAsDirectory,
|
||||
},
|
||||
{
|
||||
BucketNameInvalid{},
|
||||
ErrInvalidBucketName,
|
||||
},
|
||||
{
|
||||
BucketExists{},
|
||||
ErrBucketAlreadyOwnedByYou,
|
||||
},
|
||||
{
|
||||
ObjectNotFound{},
|
||||
ErrNoSuchKey,
|
||||
},
|
||||
{
|
||||
ObjectNameInvalid{},
|
||||
ErrInvalidObjectName,
|
||||
},
|
||||
{
|
||||
InvalidUploadID{},
|
||||
ErrNoSuchUpload,
|
||||
},
|
||||
{
|
||||
InvalidPart{},
|
||||
ErrInvalidPart,
|
||||
},
|
||||
{
|
||||
InsufficientReadQuorum{},
|
||||
ErrReadQuorum,
|
||||
},
|
||||
{
|
||||
InsufficientWriteQuorum{},
|
||||
ErrWriteQuorum,
|
||||
},
|
||||
{
|
||||
UnsupportedDelimiter{},
|
||||
ErrNotImplemented,
|
||||
},
|
||||
{
|
||||
InvalidMarkerPrefixCombination{},
|
||||
ErrNotImplemented,
|
||||
},
|
||||
{
|
||||
InvalidUploadIDKeyCombination{},
|
||||
ErrNotImplemented,
|
||||
},
|
||||
{
|
||||
MalformedUploadID{},
|
||||
ErrNoSuchUpload,
|
||||
},
|
||||
{
|
||||
PartTooSmall{},
|
||||
ErrEntityTooSmall,
|
||||
},
|
||||
{
|
||||
BucketNotEmpty{},
|
||||
ErrBucketNotEmpty,
|
||||
},
|
||||
{
|
||||
BucketNotFound{},
|
||||
ErrNoSuchBucket,
|
||||
},
|
||||
{
|
||||
StorageFull{},
|
||||
ErrStorageFull,
|
||||
},
|
||||
{
|
||||
NotSupported{},
|
||||
ErrNotSupported,
|
||||
},
|
||||
{
|
||||
NotImplemented{},
|
||||
ErrNotImplemented,
|
||||
},
|
||||
{
|
||||
errSignatureMismatch,
|
||||
ErrSignatureDoesNotMatch,
|
||||
},
|
||||
{
|
||||
errContentSHA256Mismatch,
|
||||
ErrContentSHA256Mismatch,
|
||||
}, // End of all valid cases.
|
||||
|
||||
// Case where err is nil.
|
||||
{
|
||||
nil,
|
||||
ErrNone,
|
||||
},
|
||||
|
||||
// Case where err type is unknown.
|
||||
{
|
||||
errors.New("Custom error"),
|
||||
ErrInternalError,
|
||||
},
|
||||
}
|
||||
|
||||
// Validate all the errors with their API error codes.
|
||||
for i, testCase := range testCases {
|
||||
for i, testCase := range toAPIErrorCodeTests {
|
||||
errCode := toAPIErrorCode(testCase.err)
|
||||
if errCode != testCase.errCode {
|
||||
t.Errorf("Test %d: Expected error code %d, got %d", i+1, testCase.errCode, errCode)
|
||||
|
||||
+13
-1
@@ -36,7 +36,11 @@ func setCommonHeaders(w http.ResponseWriter) {
|
||||
// Set unique request ID for each reply.
|
||||
w.Header().Set(responseRequestIDKey, mustGetRequestID(UTCNow()))
|
||||
w.Header().Set("Server", globalServerUserAgent)
|
||||
w.Header().Set("X-Amz-Bucket-Region", serverConfig.GetRegion())
|
||||
// Set `x-amz-bucket-region` only if region is set on the server
|
||||
// by default minio uses an empty region.
|
||||
if region := serverConfig.GetRegion(); region != "" {
|
||||
w.Header().Set("X-Amz-Bucket-Region", region)
|
||||
}
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
}
|
||||
|
||||
@@ -66,6 +70,14 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *h
|
||||
w.Header().Set("ETag", "\""+objInfo.ETag+"\"")
|
||||
}
|
||||
|
||||
if objInfo.ContentType != "" {
|
||||
w.Header().Set("Content-Type", objInfo.ContentType)
|
||||
}
|
||||
|
||||
if objInfo.ContentEncoding != "" {
|
||||
w.Header().Set("Content-Encoding", objInfo.ContentEncoding)
|
||||
}
|
||||
|
||||
// Set all other user defined metadata.
|
||||
for k, v := range objInfo.UserDefined {
|
||||
w.Header().Set(k, v)
|
||||
|
||||
+28
-19
@@ -275,9 +275,25 @@ func getLocation(r *http.Request) string {
|
||||
return path.Clean(r.URL.Path) // Clean any trailing slashes.
|
||||
}
|
||||
|
||||
// getObjectLocation gets the relative URL for an object
|
||||
func getObjectLocation(bucketName string, key string) string {
|
||||
return "/" + bucketName + "/" + key
|
||||
// returns "https" if the tls boolean is true, "http" otherwise.
|
||||
func getURLScheme(tls bool) string {
|
||||
if tls {
|
||||
return httpsScheme
|
||||
}
|
||||
return httpScheme
|
||||
}
|
||||
|
||||
// getObjectLocation gets the fully qualified URL of an object.
|
||||
func getObjectLocation(host, proto, bucket, object string) string {
|
||||
if proto == "" {
|
||||
proto = getURLScheme(globalIsSSL)
|
||||
}
|
||||
u := url.URL{
|
||||
Host: host,
|
||||
Path: path.Join(slashSeparator, bucket, object),
|
||||
Scheme: proto,
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// generates ListBucketsResponse from array of BucketInfo which can be
|
||||
@@ -288,12 +304,10 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
|
||||
var owner = Owner{}
|
||||
|
||||
owner.ID = globalMinioDefaultOwnerID
|
||||
owner.DisplayName = globalMinioDefaultOwnerID
|
||||
|
||||
for _, bucket := range buckets {
|
||||
var listbucket = Bucket{}
|
||||
listbucket.Name = bucket.Name
|
||||
listbucket.CreationDate = bucket.Created.Format(timeFormatAMZLong)
|
||||
listbucket.CreationDate = bucket.Created.UTC().Format(timeFormatAMZLong)
|
||||
listbucket.HealBucketInfo = bucket.HealBucketInfo
|
||||
listbuckets = append(listbuckets, listbucket)
|
||||
}
|
||||
@@ -312,8 +326,6 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max
|
||||
var data = ListObjectsResponse{}
|
||||
|
||||
owner.ID = globalMinioDefaultOwnerID
|
||||
owner.DisplayName = globalMinioDefaultOwnerID
|
||||
|
||||
for _, object := range resp.Objects {
|
||||
var content = Object{}
|
||||
if object.Name == "" {
|
||||
@@ -352,18 +364,17 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max
|
||||
}
|
||||
|
||||
// generates an ListObjectsV2 response for the said bucket with other enumerated options.
|
||||
func generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter string, fetchOwner bool, maxKeys int, resp ListObjectsInfo) ListObjectsV2Response {
|
||||
func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, delimiter string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string) ListObjectsV2Response {
|
||||
var contents []Object
|
||||
var prefixes []CommonPrefix
|
||||
var commonPrefixes []CommonPrefix
|
||||
var owner = Owner{}
|
||||
var data = ListObjectsV2Response{}
|
||||
|
||||
if fetchOwner {
|
||||
owner.ID = globalMinioDefaultOwnerID
|
||||
owner.DisplayName = globalMinioDefaultOwnerID
|
||||
}
|
||||
|
||||
for _, object := range resp.Objects {
|
||||
for _, object := range objects {
|
||||
var content = Object{}
|
||||
if object.Name == "" {
|
||||
continue
|
||||
@@ -387,14 +398,14 @@ func generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter
|
||||
data.Prefix = prefix
|
||||
data.MaxKeys = maxKeys
|
||||
data.ContinuationToken = token
|
||||
data.NextContinuationToken = resp.NextMarker
|
||||
data.IsTruncated = resp.IsTruncated
|
||||
for _, prefix := range resp.Prefixes {
|
||||
data.NextContinuationToken = nextToken
|
||||
data.IsTruncated = isTruncated
|
||||
for _, prefix := range prefixes {
|
||||
var prefixItem = CommonPrefix{}
|
||||
prefixItem.Prefix = prefix
|
||||
prefixes = append(prefixes, prefixItem)
|
||||
commonPrefixes = append(commonPrefixes, prefixItem)
|
||||
}
|
||||
data.CommonPrefixes = prefixes
|
||||
data.CommonPrefixes = commonPrefixes
|
||||
data.KeyCount = len(data.Contents) + len(data.CommonPrefixes)
|
||||
return data
|
||||
}
|
||||
@@ -443,9 +454,7 @@ func generateListPartsResponse(partsInfo ListPartsInfo) ListPartsResponse {
|
||||
listPartsResponse.UploadID = partsInfo.UploadID
|
||||
listPartsResponse.StorageClass = globalMinioDefaultStorageClass
|
||||
listPartsResponse.Initiator.ID = globalMinioDefaultOwnerID
|
||||
listPartsResponse.Initiator.DisplayName = globalMinioDefaultOwnerID
|
||||
listPartsResponse.Owner.ID = globalMinioDefaultOwnerID
|
||||
listPartsResponse.Owner.DisplayName = globalMinioDefaultOwnerID
|
||||
|
||||
listPartsResponse.MaxParts = partsInfo.MaxParts
|
||||
listPartsResponse.PartNumberMarker = partsInfo.PartNumberMarker
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests object location.
|
||||
func TestObjectLocation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
host, proto, bucket, object string
|
||||
expectedLocation string
|
||||
}{
|
||||
// Server binding to localhost IP with https.
|
||||
{
|
||||
host: "127.0.0.1:9000",
|
||||
proto: httpsScheme,
|
||||
bucket: "testbucket1",
|
||||
object: "test/1.txt",
|
||||
expectedLocation: "https://127.0.0.1:9000/testbucket1/test/1.txt",
|
||||
},
|
||||
// Server binding to fqdn.
|
||||
{
|
||||
host: "s3.mybucket.org",
|
||||
proto: httpScheme,
|
||||
bucket: "mybucket",
|
||||
object: "test/1.txt",
|
||||
expectedLocation: "http://s3.mybucket.org/mybucket/test/1.txt",
|
||||
},
|
||||
// Server binding to fqdn.
|
||||
{
|
||||
host: "mys3.mybucket.org",
|
||||
proto: "",
|
||||
bucket: "mybucket",
|
||||
object: "test/1.txt",
|
||||
expectedLocation: "http://mys3.mybucket.org/mybucket/test/1.txt",
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
gotLocation := getObjectLocation(testCase.host, testCase.proto, testCase.bucket, testCase.object)
|
||||
if testCase.expectedLocation != gotLocation {
|
||||
t.Errorf("Test %d: expected %s, got %s", i+1, testCase.expectedLocation, gotLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests getURLScheme function behavior.
|
||||
func TestGetURLScheme(t *testing.T) {
|
||||
tls := false
|
||||
gotScheme := getURLScheme(tls)
|
||||
if gotScheme != httpScheme {
|
||||
t.Errorf("Expected %s, got %s", httpScheme, gotScheme)
|
||||
}
|
||||
tls = true
|
||||
gotScheme = getURLScheme(tls)
|
||||
if gotScheme != httpsScheme {
|
||||
t.Errorf("Expected %s, got %s", httpsScheme, gotScheme)
|
||||
}
|
||||
}
|
||||
+66
-60
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import router "github.com/gorilla/mux"
|
||||
import "net/http"
|
||||
|
||||
// objectAPIHandler implements and provides http handlers for S3 API.
|
||||
type objectAPIHandlers struct {
|
||||
@@ -32,70 +33,75 @@ func registerAPIRouter(mux *router.Router) {
|
||||
|
||||
// API Router
|
||||
apiRouter := mux.NewRoute().PathPrefix("/").Subrouter()
|
||||
var routers []*router.Router
|
||||
if globalDomainName != "" {
|
||||
routers = append(routers, apiRouter.Host("{bucket:.+}."+globalDomainName).Subrouter())
|
||||
}
|
||||
routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter())
|
||||
|
||||
// Bucket router
|
||||
bucket := apiRouter.PathPrefix("/{bucket}").Subrouter()
|
||||
for _, bucket := range routers {
|
||||
// Object operations
|
||||
// HeadObject
|
||||
bucket.Methods("HEAD").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.HeadObjectHandler))
|
||||
// CopyObjectPart
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(httpTraceAll(api.CopyObjectPartHandler)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||
// PutObjectPart
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.PutObjectPartHandler)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||
// ListObjectPxarts
|
||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.ListObjectPartsHandler)).Queries("uploadId", "{uploadId:.*}")
|
||||
// CompleteMultipartUpload
|
||||
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.CompleteMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}")
|
||||
// NewMultipartUpload
|
||||
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.NewMultipartUploadHandler)).Queries("uploads", "")
|
||||
// AbortMultipartUpload
|
||||
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.AbortMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}")
|
||||
// GetObject
|
||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectHandler))
|
||||
// CopyObject
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(httpTraceAll(api.CopyObjectHandler))
|
||||
// PutObject
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.PutObjectHandler))
|
||||
// DeleteObject
|
||||
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.DeleteObjectHandler))
|
||||
|
||||
/// Object operations
|
||||
|
||||
// HeadObject
|
||||
bucket.Methods("HEAD").Path("/{object:.+}").HandlerFunc(api.HeadObjectHandler)
|
||||
// CopyObjectPart
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(api.CopyObjectPartHandler).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||
// PutObjectPart
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(api.PutObjectPartHandler).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||
// ListObjectPxarts
|
||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(api.ListObjectPartsHandler).Queries("uploadId", "{uploadId:.*}")
|
||||
// CompleteMultipartUpload
|
||||
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(api.CompleteMultipartUploadHandler).Queries("uploadId", "{uploadId:.*}")
|
||||
// NewMultipartUpload
|
||||
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(api.NewMultipartUploadHandler).Queries("uploads", "")
|
||||
// AbortMultipartUpload
|
||||
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(api.AbortMultipartUploadHandler).Queries("uploadId", "{uploadId:.*}")
|
||||
// GetObject
|
||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(api.GetObjectHandler)
|
||||
// CopyObject
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(api.CopyObjectHandler)
|
||||
// PutObject
|
||||
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(api.PutObjectHandler)
|
||||
// DeleteObject
|
||||
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(api.DeleteObjectHandler)
|
||||
|
||||
/// Bucket operations
|
||||
|
||||
// GetBucketLocation
|
||||
bucket.Methods("GET").HandlerFunc(api.GetBucketLocationHandler).Queries("location", "")
|
||||
// GetBucketPolicy
|
||||
bucket.Methods("GET").HandlerFunc(api.GetBucketPolicyHandler).Queries("policy", "")
|
||||
// GetBucketNotification
|
||||
bucket.Methods("GET").HandlerFunc(api.GetBucketNotificationHandler).Queries("notification", "")
|
||||
// ListenBucketNotification
|
||||
bucket.Methods("GET").HandlerFunc(api.ListenBucketNotificationHandler).Queries("events", "{events:.*}")
|
||||
// ListMultipartUploads
|
||||
bucket.Methods("GET").HandlerFunc(api.ListMultipartUploadsHandler).Queries("uploads", "")
|
||||
// ListObjectsV2
|
||||
bucket.Methods("GET").HandlerFunc(api.ListObjectsV2Handler).Queries("list-type", "2")
|
||||
// ListObjectsV1 (Legacy)
|
||||
bucket.Methods("GET").HandlerFunc(api.ListObjectsV1Handler)
|
||||
// PutBucketPolicy
|
||||
bucket.Methods("PUT").HandlerFunc(api.PutBucketPolicyHandler).Queries("policy", "")
|
||||
// PutBucketNotification
|
||||
bucket.Methods("PUT").HandlerFunc(api.PutBucketNotificationHandler).Queries("notification", "")
|
||||
// PutBucket
|
||||
bucket.Methods("PUT").HandlerFunc(api.PutBucketHandler)
|
||||
// HeadBucket
|
||||
bucket.Methods("HEAD").HandlerFunc(api.HeadBucketHandler)
|
||||
// PostPolicy
|
||||
bucket.Methods("POST").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(api.PostPolicyBucketHandler)
|
||||
// DeleteMultipleObjects
|
||||
bucket.Methods("POST").HandlerFunc(api.DeleteMultipleObjectsHandler)
|
||||
// DeleteBucketPolicy
|
||||
bucket.Methods("DELETE").HandlerFunc(api.DeleteBucketPolicyHandler).Queries("policy", "")
|
||||
// DeleteBucket
|
||||
bucket.Methods("DELETE").HandlerFunc(api.DeleteBucketHandler)
|
||||
/// Bucket operations
|
||||
// GetBucketLocation
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketLocationHandler)).Queries("location", "")
|
||||
// GetBucketPolicy
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketPolicyHandler)).Queries("policy", "")
|
||||
// GetBucketNotification
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.GetBucketNotificationHandler)).Queries("notification", "")
|
||||
// ListenBucketNotification
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListenBucketNotificationHandler)).Queries("events", "{events:.*}")
|
||||
// ListMultipartUploads
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListMultipartUploadsHandler)).Queries("uploads", "")
|
||||
// ListObjectsV2
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListObjectsV2Handler)).Queries("list-type", "2")
|
||||
// ListObjectsV1 (Legacy)
|
||||
bucket.Methods("GET").HandlerFunc(httpTraceAll(api.ListObjectsV1Handler))
|
||||
// PutBucketPolicy
|
||||
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketPolicyHandler)).Queries("policy", "")
|
||||
// PutBucketNotification
|
||||
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketNotificationHandler)).Queries("notification", "")
|
||||
// PutBucket
|
||||
bucket.Methods("PUT").HandlerFunc(httpTraceAll(api.PutBucketHandler))
|
||||
// HeadBucket
|
||||
bucket.Methods("HEAD").HandlerFunc(httpTraceAll(api.HeadBucketHandler))
|
||||
// PostPolicy
|
||||
bucket.Methods("POST").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(httpTraceAll(api.PostPolicyBucketHandler))
|
||||
// DeleteMultipleObjects
|
||||
bucket.Methods("POST").HandlerFunc(httpTraceAll(api.DeleteMultipleObjectsHandler)).Queries("delete", "")
|
||||
// DeleteBucketPolicy
|
||||
bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketPolicyHandler)).Queries("policy", "")
|
||||
// DeleteBucket
|
||||
bucket.Methods("DELETE").HandlerFunc(httpTraceAll(api.DeleteBucketHandler))
|
||||
}
|
||||
|
||||
/// Root operation
|
||||
|
||||
// ListBuckets
|
||||
apiRouter.Methods("GET").HandlerFunc(api.ListBucketsHandler)
|
||||
apiRouter.Methods("GET").Path("/").HandlerFunc(httpTraceAll(api.ListBucketsHandler))
|
||||
|
||||
// If none of the routes match.
|
||||
apiRouter.NotFoundHandler = http.HandlerFunc(httpTraceAll(notFoundHandler))
|
||||
}
|
||||
|
||||
+7
-2
@@ -126,8 +126,13 @@ func checkRequestAuthType(r *http.Request, bucket, policyAction, region string)
|
||||
|
||||
if reqAuthType == authTypeAnonymous && policyAction != "" {
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
||||
return enforceBucketPolicy(bucket, policyAction, r.URL.Path,
|
||||
r.Referer(), r.URL.Query())
|
||||
sourceIP := getSourceIPAddress(r)
|
||||
resource, err := getResource(r.URL.Path, r.Host, globalDomainName)
|
||||
if err != nil {
|
||||
return ErrInternalError
|
||||
}
|
||||
return enforceBucketPolicy(bucket, policyAction, resource,
|
||||
r.Referer(), sourceIP, r.URL.Query())
|
||||
}
|
||||
|
||||
// By default return ErrAccessDenied
|
||||
|
||||
@@ -21,7 +21,10 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Test get request auth type.
|
||||
@@ -324,9 +327,9 @@ func TestIsReqAuthenticated(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer removeAll(path)
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
creds, err := createCredential("myuser", "mypassword")
|
||||
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
||||
if err != nil {
|
||||
t.Fatalf("unable create credential, %s", err)
|
||||
}
|
||||
|
||||
@@ -99,21 +99,22 @@ func (authClient *AuthRPCClient) Login() (err error) {
|
||||
|
||||
// Attempt to login if not logged in already.
|
||||
if authClient.authToken == "" {
|
||||
// Login to authenticate and acquire a new auth token.
|
||||
authClient.authToken, err = authenticateNode(authClient.config.accessKey, authClient.config.secretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Login to authenticate your token.
|
||||
var (
|
||||
loginMethod = authClient.config.serviceName + loginMethodName
|
||||
loginArgs = LoginRPCArgs{
|
||||
Username: authClient.config.accessKey,
|
||||
Password: authClient.config.secretKey,
|
||||
AuthToken: authClient.authToken,
|
||||
Version: Version,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
loginReply = LoginRPCReply{}
|
||||
)
|
||||
if err = authClient.rpcClient.Call(loginMethod, &loginArgs, &loginReply); err != nil {
|
||||
if err = authClient.rpcClient.Call(loginMethod, &loginArgs, &LoginRPCReply{}); err != nil {
|
||||
return err
|
||||
}
|
||||
authClient.authToken = loginReply.AuthToken
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,8 +20,7 @@ package cmd
|
||||
const loginMethodName = ".Login"
|
||||
|
||||
// AuthRPCServer RPC server authenticates using JWT.
|
||||
type AuthRPCServer struct {
|
||||
}
|
||||
type AuthRPCServer struct{}
|
||||
|
||||
// Login - Handles JWT based RPC login.
|
||||
func (b AuthRPCServer) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
|
||||
@@ -30,14 +29,10 @@ func (b AuthRPCServer) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Authenticate using JWT.
|
||||
token, err := authenticateNode(args.Username, args.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
// Return an error if token is not valid.
|
||||
if !isAuthTokenValid(args.AuthToken) {
|
||||
return errAuthentication
|
||||
}
|
||||
|
||||
// Return the token.
|
||||
reply.AuthToken = token
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+16
-45
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -26,8 +27,12 @@ func TestLogin(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test config - %v", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
creds := serverConfig.GetCredential()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ls := AuthRPCServer{}
|
||||
testCases := []struct {
|
||||
args LoginRPCArgs
|
||||
@@ -37,9 +42,8 @@ func TestLogin(t *testing.T) {
|
||||
// Valid case.
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: nil,
|
||||
@@ -47,9 +51,8 @@ func TestLogin(t *testing.T) {
|
||||
// Valid username, password and request time, not version.
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: "INVALID-" + Version,
|
||||
AuthToken: token,
|
||||
Version: "INVALID-" + Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: errServerVersionMismatch,
|
||||
@@ -57,49 +60,17 @@ func TestLogin(t *testing.T) {
|
||||
// Valid username, password and version, not request time
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 20 * time.Minute,
|
||||
expectedErr: errServerTimeMismatch,
|
||||
},
|
||||
// Invalid username length
|
||||
// Invalid token, fails with authentication error
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: "aaa",
|
||||
Password: "minio123",
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: errInvalidAccessKeyLength,
|
||||
},
|
||||
// Invalid password length
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: globalMinioDefaultOwnerID,
|
||||
Password: "aaa",
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: errInvalidSecretKeyLength,
|
||||
},
|
||||
// Invalid username
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: "aaaaa",
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: errInvalidAccessKeyID,
|
||||
},
|
||||
// Invalid password
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: "aaaaaaaa",
|
||||
Version: Version,
|
||||
AuthToken: "",
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: errAuthentication,
|
||||
@@ -107,7 +78,7 @@ func TestLogin(t *testing.T) {
|
||||
}
|
||||
for i, test := range testCases {
|
||||
reply := LoginRPCReply{}
|
||||
test.args.RequestTime = time.Now().Add(test.skewTime).UTC()
|
||||
test.args.RequestTime = UTCNow().Add(test.skewTime)
|
||||
err := ls.Login(&test.args, &reply)
|
||||
if err != test.expectedErr {
|
||||
t.Errorf("Test %d: Expected error %v but received %v",
|
||||
|
||||
+62
-47
@@ -21,6 +21,7 @@ import (
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
@@ -49,20 +50,23 @@ func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for PutObject.
|
||||
metadata := make(map[string]string)
|
||||
metadata["etag"] = getMD5Hash(textData)
|
||||
sha256sum := ""
|
||||
|
||||
md5hex := getMD5Hash(textData)
|
||||
sha256hex := ""
|
||||
|
||||
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
|
||||
b.ReportAllocs()
|
||||
// the actual benchmark for PutObject starts here. Reset the benchmark timer.
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// insert the object.
|
||||
objInfo, err := obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata, sha256sum)
|
||||
objInfo, err := obj.PutObject(bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if objInfo.ETag != metadata["etag"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, metadata["etag"])
|
||||
if objInfo.ETag != md5hex {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, md5hex)
|
||||
}
|
||||
}
|
||||
// Benchmark ends here. Stop timer.
|
||||
@@ -93,13 +97,14 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for NewMultipartUpload.
|
||||
metadata := make(map[string]string)
|
||||
metadata["etag"] = getMD5Hash(textData)
|
||||
sha256sum := ""
|
||||
uploadID, err = obj.NewMultipartUpload(bucket, object, metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
md5hex := getMD5Hash(textData)
|
||||
sha256hex := ""
|
||||
|
||||
var textPartData []byte
|
||||
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
|
||||
b.ReportAllocs()
|
||||
@@ -114,15 +119,15 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
||||
} else {
|
||||
textPartData = textData[j*partSize:]
|
||||
}
|
||||
metadata := make(map[string]string)
|
||||
metadata["etag"] = getMD5Hash([]byte(textPartData))
|
||||
md5hex = getMD5Hash([]byte(textPartData))
|
||||
var partInfo PartInfo
|
||||
partInfo, err = obj.PutObjectPart(bucket, object, uploadID, j, int64(len(textPartData)), bytes.NewBuffer(textPartData), metadata["etag"], sha256sum)
|
||||
partInfo, err = obj.PutObjectPart(bucket, object, uploadID, j,
|
||||
mustGetHashReader(b, bytes.NewBuffer(textPartData), int64(len(textPartData)), md5hex, sha256hex))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if partInfo.ETag != metadata["etag"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, etag, metadata["etag"])
|
||||
if partInfo.ETag != md5hex {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, etag, md5hex)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,7 +141,7 @@ func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -155,7 +160,7 @@ func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -174,7 +179,7 @@ func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -194,7 +199,7 @@ func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
@@ -204,23 +209,27 @@ func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
sha256sum := ""
|
||||
textData := generateBytesData(objSize)
|
||||
|
||||
// generate etag for the generated data.
|
||||
// etag of the data to written is required as input for PutObject.
|
||||
// PutObject is the functions which writes the data onto the FS/XL backend.
|
||||
metadata := make(map[string]string)
|
||||
|
||||
// get text data generated for number of bytes equal to object size.
|
||||
md5hex := getMD5Hash(textData)
|
||||
sha256hex := ""
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
// get text data generated for number of bytes equal to object size.
|
||||
textData := generateBytesData(objSize)
|
||||
// generate etag for the generated data.
|
||||
// etag of the data to written is required as input for PutObject.
|
||||
// PutObject is the functions which writes the data onto the FS/XL backend.
|
||||
metadata := make(map[string]string)
|
||||
metadata["etag"] = getMD5Hash(textData)
|
||||
// insert the object.
|
||||
var objInfo ObjectInfo
|
||||
objInfo, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata, sha256sum)
|
||||
objInfo, err = obj.PutObject(bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if objInfo.ETag != metadata["etag"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, metadata["etag"])
|
||||
if objInfo.ETag != md5hex {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, md5hex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +272,7 @@ func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -282,7 +291,7 @@ func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -302,7 +311,7 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
@@ -317,8 +326,10 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for PutObject.
|
||||
metadata := make(map[string]string)
|
||||
metadata["etag"] = getMD5Hash([]byte(textData))
|
||||
sha256sum := ""
|
||||
|
||||
md5hex := getMD5Hash([]byte(textData))
|
||||
sha256hex := ""
|
||||
|
||||
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
|
||||
b.ReportAllocs()
|
||||
// the actual benchmark for PutObject starts here. Reset the benchmark timer.
|
||||
@@ -328,12 +339,13 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
// insert the object.
|
||||
objInfo, err := obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata, sha256sum)
|
||||
objInfo, err := obj.PutObject(bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if objInfo.ETag != metadata["etag"] {
|
||||
b.Fatalf("Write no: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", objInfo.ETag, metadata["etag"])
|
||||
if objInfo.ETag != md5hex {
|
||||
b.Fatalf("Write no: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", objInfo.ETag, md5hex)
|
||||
}
|
||||
i++
|
||||
}
|
||||
@@ -350,7 +362,7 @@ func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
@@ -360,23 +372,26 @@ func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// get text data generated for number of bytes equal to object size.
|
||||
textData := generateBytesData(objSize)
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for PutObject.
|
||||
// PutObject is the functions which writes the data onto the FS/XL backend.
|
||||
metadata := make(map[string]string)
|
||||
|
||||
md5hex := getMD5Hash([]byte(textData))
|
||||
sha256hex := ""
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
// get text data generated for number of bytes equal to object size.
|
||||
textData := generateBytesData(objSize)
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for PutObject.
|
||||
// PutObject is the functions which writes the data onto the FS/XL backend.
|
||||
metadata := make(map[string]string)
|
||||
metadata["etag"] = getMD5Hash([]byte(textData))
|
||||
sha256sum := ""
|
||||
// insert the object.
|
||||
var objInfo ObjectInfo
|
||||
objInfo, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata, sha256sum)
|
||||
objInfo, err = obj.PutObject(bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if objInfo.ETag != metadata["etag"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, metadata["etag"])
|
||||
if objInfo.ETag != md5hex {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, md5hex)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-24
@@ -21,35 +21,17 @@ import (
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Login handler implements JWT login token generator, which upon login request
|
||||
// along with username and password is generated.
|
||||
func (br *browserPeerAPIHandlers) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
|
||||
// Validate LoginRPCArgs
|
||||
if err := args.IsValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Authenticate using JWT.
|
||||
token, err := authenticateWeb(args.Username, args.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Return the token.
|
||||
reply.AuthToken = token
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAuthPeerArgs - Arguments collection for SetAuth RPC call
|
||||
type SetAuthPeerArgs struct {
|
||||
// For Auth
|
||||
AuthRPCArgs
|
||||
|
||||
// New credentials that receiving peer should update to.
|
||||
Creds credential
|
||||
Creds auth.Credentials
|
||||
}
|
||||
|
||||
// SetAuthPeer - Update to new credentials sent from a peer Minio
|
||||
@@ -69,11 +51,14 @@ func (br *browserPeerAPIHandlers) SetAuthPeer(args SetAuthPeerArgs, reply *AuthR
|
||||
}
|
||||
|
||||
// Update credentials in memory
|
||||
serverConfig.SetCredential(args.Creds)
|
||||
prevCred := serverConfig.SetCredential(args.Creds)
|
||||
|
||||
// Save credentials to config file
|
||||
if err := serverConfig.Save(); err != nil {
|
||||
errorIf(err, "Error updating config file with new credentials sent from browser RPC.")
|
||||
// Save the current creds when failed to update.
|
||||
serverConfig.SetCredential(prevCred)
|
||||
|
||||
errorIf(err, "Unable to update the config with new credentials sent from browser RPC.")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -81,7 +66,7 @@ func (br *browserPeerAPIHandlers) SetAuthPeer(args SetAuthPeerArgs, reply *AuthR
|
||||
}
|
||||
|
||||
// Sends SetAuthPeer RPCs to all peers in the Minio cluster
|
||||
func updateCredsOnPeers(creds credential) map[string]error {
|
||||
func updateCredsOnPeers(creds auth.Credentials) map[string]error {
|
||||
// Get list of peer addresses (from globalS3Peers)
|
||||
peers := []string{}
|
||||
for _, p := range globalS3Peers {
|
||||
|
||||
@@ -19,6 +19,8 @@ package cmd
|
||||
import (
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// API suite container common to both FS and XL.
|
||||
@@ -29,8 +31,8 @@ type TestRPCBrowserPeerSuite struct {
|
||||
}
|
||||
|
||||
// Setting up the test suite and starting the Test server.
|
||||
func (s *TestRPCBrowserPeerSuite) SetUpSuite(c *testing.T) {
|
||||
s.testServer = StartTestBrowserPeerRPCServer(c, s.serverType)
|
||||
func (s *TestRPCBrowserPeerSuite) SetUpSuite(t *testing.T) {
|
||||
s.testServer = StartTestBrowserPeerRPCServer(t, s.serverType)
|
||||
s.testAuthConf = authConfig{
|
||||
serverAddr: s.testServer.Server.Listener.Addr().String(),
|
||||
accessKey: s.testServer.AccessKey,
|
||||
@@ -40,10 +42,9 @@ func (s *TestRPCBrowserPeerSuite) SetUpSuite(c *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// No longer used with gocheck, but used in explicit teardown code in
|
||||
// each test function. // Called implicitly by "gopkg.in/check.v1"
|
||||
// after all tests are run.
|
||||
func (s *TestRPCBrowserPeerSuite) TearDownSuite(c *testing.T) {
|
||||
// TeatDownSuite - called implicitly by after all tests are run in
|
||||
// browser peer rpc suite.
|
||||
func (s *TestRPCBrowserPeerSuite) TearDownSuite(t *testing.T) {
|
||||
s.testServer.Stop()
|
||||
}
|
||||
|
||||
@@ -62,7 +63,7 @@ func TestBrowserPeerRPC(t *testing.T) {
|
||||
// Tests for browser peer rpc.
|
||||
func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
|
||||
// Construct RPC call arguments.
|
||||
creds, err := createCredential("abcd1", "abcd1234")
|
||||
creds, err := auth.CreateCredentials("abcd1", "abcd1234")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create credential. %v", err)
|
||||
}
|
||||
@@ -91,9 +92,12 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
|
||||
// Validate for failure in login handler with previous credentials.
|
||||
rclient = newRPCClient(s.testAuthConf.serverAddr, s.testAuthConf.serviceEndpoint, false)
|
||||
defer rclient.Close()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rargs := &LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
@@ -105,20 +109,18 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
token, err = authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Validate for success in loing handled with valid credetnails.
|
||||
rargs = &LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
AuthToken: token,
|
||||
Version: Version,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
rreply = &LoginRPCReply{}
|
||||
err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply)
|
||||
if err != nil {
|
||||
if err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Validate all the replied fields after successful login.
|
||||
if rreply.AuthToken == "" {
|
||||
t.Fatalf("Generated token cannot be empty %s", errInvalidToken)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/rpc"
|
||||
|
||||
router "github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@@ -37,9 +35,9 @@ type browserPeerAPIHandlers struct {
|
||||
|
||||
// Register RPC router
|
||||
func registerBrowserPeerRPCRouter(mux *router.Router) error {
|
||||
bpHandlers := &browserPeerAPIHandlers{}
|
||||
bpHandlers := &browserPeerAPIHandlers{AuthRPCServer{}}
|
||||
|
||||
bpRPCServer := rpc.NewServer()
|
||||
bpRPCServer := newRPCServer()
|
||||
err := bpRPCServer.RegisterName("BrowserPeer", bpHandlers)
|
||||
if err != nil {
|
||||
return traceError(err)
|
||||
|
||||
@@ -51,6 +51,26 @@ func validateListObjectsArgs(prefix, marker, delimiter string, maxKeys int) APIE
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Validate all the ListObjectsV2 query arguments, returns an APIErrorCode
|
||||
// if one of the args do not meet the required conditions.
|
||||
// Special conditions required by Minio server are as below
|
||||
// - delimiter if set should be equal to '/', otherwise the request is rejected.
|
||||
func validateGatewayListObjectsV2Args(prefix, marker, delimiter string, maxKeys int) APIErrorCode {
|
||||
// Max keys cannot be negative.
|
||||
if maxKeys < 0 {
|
||||
return ErrInvalidMaxKeys
|
||||
}
|
||||
|
||||
/// Minio special conditions for ListObjects.
|
||||
|
||||
// Verify if delimiter is anything other than '/', which we do not support.
|
||||
if delimiter != "" && delimiter != "/" {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// ListObjectsV2Handler - GET Bucket (List Objects) Version 2.
|
||||
// --------------------------
|
||||
// This implementation of the GET operation returns some or all (up to 1000)
|
||||
@@ -100,7 +120,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
|
||||
response := generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter, fetchOwner, maxKeys, listObjectsInfo)
|
||||
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsInfo.NextMarker, startAfter, delimiter, fetchOwner, listObjectsInfo.IsTruncated, maxKeys, listObjectsInfo.Objects, listObjectsInfo.Prefixes)
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, encodeResponse(response))
|
||||
|
||||
+96
-32
@@ -24,16 +24,20 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
mux "github.com/gorilla/mux"
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
||||
// Enforces bucket policies for a bucket for a given tatusaction.
|
||||
func enforceBucketPolicy(bucket, action, resource, referer string, queryParams url.Values) (s3Error APIErrorCode) {
|
||||
func enforceBucketPolicy(bucket, action, resource, referer, sourceIP string, queryParams url.Values) (s3Error APIErrorCode) {
|
||||
// Verify if bucket actually exists
|
||||
if err := checkBucketExist(bucket, newObjectLayerFn()); err != nil {
|
||||
err = errorCause(err)
|
||||
@@ -55,8 +59,8 @@ func enforceBucketPolicy(bucket, action, resource, referer string, queryParams u
|
||||
}
|
||||
|
||||
// Fetch bucket policy, if policy is not set return access denied.
|
||||
policy := globalBucketPolicies.GetBucketPolicy(bucket)
|
||||
if policy == nil {
|
||||
p := globalBucketPolicies.GetBucketPolicy(bucket)
|
||||
if reflect.DeepEqual(p, emptyBucketPolicy) {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
@@ -64,7 +68,7 @@ func enforceBucketPolicy(bucket, action, resource, referer string, queryParams u
|
||||
arn := bucketARNPrefix + strings.TrimSuffix(strings.TrimPrefix(resource, "/"), "/")
|
||||
|
||||
// Get conditions for policy verification.
|
||||
conditionKeyMap := make(map[string]set.StringSet)
|
||||
conditionKeyMap := make(policy.ConditionKeyMap)
|
||||
for queryParam := range queryParams {
|
||||
conditionKeyMap[queryParam] = set.CreateStringSet(queryParams.Get(queryParam))
|
||||
}
|
||||
@@ -73,9 +77,11 @@ func enforceBucketPolicy(bucket, action, resource, referer string, queryParams u
|
||||
if referer != "" {
|
||||
conditionKeyMap["referer"] = set.CreateStringSet(referer)
|
||||
}
|
||||
// Add request source Ip to conditionKeyMap.
|
||||
conditionKeyMap["ip"] = set.CreateStringSet(sourceIP)
|
||||
|
||||
// Validate action, resource and conditions with current policy statements.
|
||||
if !bucketPolicyEvalStatements(action, arn, conditionKeyMap, policy.Statements) {
|
||||
if !bucketPolicyEvalStatements(action, arn, conditionKeyMap, p.Statements) {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
return ErrNone
|
||||
@@ -87,14 +93,14 @@ func isBucketActionAllowed(action, bucket, prefix string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
policy := globalBucketPolicies.GetBucketPolicy(bucket)
|
||||
if policy == nil {
|
||||
bp := globalBucketPolicies.GetBucketPolicy(bucket)
|
||||
if reflect.DeepEqual(bp, emptyBucketPolicy) {
|
||||
return false
|
||||
}
|
||||
resource := bucketARNPrefix + path.Join(bucket, prefix)
|
||||
var conditionKeyMap map[string]set.StringSet
|
||||
// Validate action, resource and conditions with current policy statements.
|
||||
return bucketPolicyEvalStatements(action, resource, conditionKeyMap, policy.Statements)
|
||||
return bucketPolicyEvalStatements(action, resource, conditionKeyMap, bp.Statements)
|
||||
}
|
||||
|
||||
// GetBucketLocationHandler - GET Bucket location.
|
||||
@@ -238,9 +244,14 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(r, bucket, "s3:DeleteObject", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
var authError APIErrorCode
|
||||
if authError = checkRequestAuthType(r, bucket, "s3:DeleteObject", serverConfig.GetRegion()); authError != ErrNone {
|
||||
// In the event access is denied, a 200 response should still be returned
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||
if authError != ErrAccessDenied {
|
||||
writeErrorResponse(w, authError, r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Content-Length is required and should be non-zero
|
||||
@@ -282,14 +293,26 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
for index, object := range deleteObjects.Objects {
|
||||
wg.Add(1)
|
||||
go func(i int, obj ObjectIdentifier) {
|
||||
objectLock := globalNSMutex.NewNSLock(bucket, obj.ObjectName)
|
||||
objectLock.Lock()
|
||||
defer objectLock.Unlock()
|
||||
defer wg.Done()
|
||||
// If the request is denied access, each item
|
||||
// should be marked as 'AccessDenied'
|
||||
if authError == ErrAccessDenied {
|
||||
dErrs[i] = PrefixAccessDenied{
|
||||
Bucket: bucket,
|
||||
Object: obj.ObjectName,
|
||||
}
|
||||
return
|
||||
}
|
||||
objectLock := globalNSMutex.NewNSLock(bucket, obj.ObjectName)
|
||||
if timedOutErr := objectLock.GetLock(globalObjectTimeout); timedOutErr != nil {
|
||||
dErrs[i] = timedOutErr
|
||||
} else {
|
||||
defer objectLock.Unlock()
|
||||
|
||||
dErr := objectAPI.DeleteObject(bucket, obj.ObjectName)
|
||||
if dErr != nil {
|
||||
dErrs[i] = dErr
|
||||
dErr := objectAPI.DeleteObject(bucket, obj.ObjectName)
|
||||
if dErr != nil {
|
||||
dErrs[i] = dErr
|
||||
}
|
||||
}
|
||||
}(index, object)
|
||||
}
|
||||
@@ -361,11 +384,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
// PutBucket does not have any bucket action.
|
||||
s3Error := checkRequestAuthType(r, "", "", globalMinioDefaultRegion)
|
||||
if s3Error == ErrInvalidRegion {
|
||||
// Clients like boto3 send putBucket() call signed with region that is configured.
|
||||
s3Error = checkRequestAuthType(r, "", "", serverConfig.GetRegion())
|
||||
}
|
||||
s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion())
|
||||
if s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
@@ -389,7 +408,10 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.Lock()
|
||||
if bucketLock.GetLock(globalObjectTimeout) != nil {
|
||||
writeErrorResponse(w, ErrOperationTimedOut, r.URL)
|
||||
return
|
||||
}
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
// Proceed to creating a bucket.
|
||||
@@ -417,12 +439,24 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
bucket := mux.Vars(r)["bucket"]
|
||||
|
||||
// Require Content-Length to be set in the request
|
||||
size := r.ContentLength
|
||||
if size < 0 {
|
||||
writeErrorResponse(w, ErrMissingContentLength, r.URL)
|
||||
return
|
||||
}
|
||||
resource, err := getResource(r.URL.Path, r.Host, globalDomainName)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, ErrInvalidRequest, r.URL)
|
||||
return
|
||||
}
|
||||
// Make sure that the URL does not contain object name.
|
||||
if bucket != filepath.Clean(resource[1:]) {
|
||||
writeErrorResponse(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Here the parameter is the size of the form data that should
|
||||
// be loaded in memory, the remaining being put in temporary files.
|
||||
@@ -461,7 +495,6 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
// Close multipart file
|
||||
defer fileBody.Close()
|
||||
|
||||
bucket := mux.Vars(r)["bucket"]
|
||||
formValues.Set("Bucket", bucket)
|
||||
|
||||
if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") {
|
||||
@@ -525,22 +558,38 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
}
|
||||
|
||||
// Extract metadata to be saved from received Form.
|
||||
metadata := extractMetadataFromForm(formValues)
|
||||
sha256sum := ""
|
||||
metadata, err := extractMetadataFromHeader(formValues)
|
||||
if err != nil {
|
||||
errorIf(err, "found invalid http request header")
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
objectLock := globalNSMutex.NewNSLock(bucket, object)
|
||||
objectLock.Lock()
|
||||
if objectLock.GetLock(globalObjectTimeout) != nil {
|
||||
writeErrorResponse(w, ErrOperationTimedOut, r.URL)
|
||||
return
|
||||
}
|
||||
defer objectLock.Unlock()
|
||||
|
||||
objInfo, err := objectAPI.PutObject(bucket, object, fileSize, fileBody, metadata, sha256sum)
|
||||
hashReader, err := hash.NewReader(fileBody, fileSize, "", "")
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to initialize hashReader.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
objInfo, err := objectAPI.PutObject(bucket, object, hashReader, metadata)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to create object.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
port := r.Header.Get("X-Forward-Proto")
|
||||
location := getObjectLocation(r.Host, port, bucket, object)
|
||||
w.Header().Set("ETag", `"`+objInfo.ETag+`"`)
|
||||
w.Header().Set("Location", getObjectLocation(bucket, object))
|
||||
w.Header().Set("Location", location)
|
||||
|
||||
// Get host and port from Request.RemoteAddr.
|
||||
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||
@@ -573,7 +622,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
Bucket: objInfo.Bucket,
|
||||
Key: objInfo.Name,
|
||||
ETag: `"` + objInfo.ETag + `"`,
|
||||
Location: getObjectLocation(objInfo.Bucket, objInfo.Name),
|
||||
Location: location,
|
||||
})
|
||||
writeResponse(w, http.StatusCreated, resp, "application/xml")
|
||||
case "200":
|
||||
@@ -605,7 +654,10 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.RLock()
|
||||
if bucketLock.GetRLock(globalObjectTimeout) != nil {
|
||||
writeErrorResponseHeadersOnly(w, ErrOperationTimedOut)
|
||||
return
|
||||
}
|
||||
defer bucketLock.RUnlock()
|
||||
|
||||
if _, err := objectAPI.GetBucketInfo(bucket); err != nil {
|
||||
@@ -635,7 +687,10 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
bucket := vars["bucket"]
|
||||
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.Lock()
|
||||
if bucketLock.GetLock(globalObjectTimeout) != nil {
|
||||
writeErrorResponse(w, ErrOperationTimedOut, r.URL)
|
||||
return
|
||||
}
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
// Attempt to delete bucket.
|
||||
@@ -648,12 +703,21 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
// Delete bucket access policy, if present - ignore any errors.
|
||||
_ = removeBucketPolicy(bucket, objectAPI)
|
||||
|
||||
// Notify all peers (including self) to update in-memory state
|
||||
S3PeersUpdateBucketPolicy(bucket, policyChange{true, policy.BucketAccessPolicy{}})
|
||||
|
||||
// Delete notification config, if present - ignore any errors.
|
||||
_ = removeNotificationConfig(bucket, objectAPI)
|
||||
|
||||
// Notify all peers (including self) to update in-memory state
|
||||
S3PeersUpdateBucketNotification(bucket, nil)
|
||||
|
||||
// Delete listener config, if present - ignore any errors.
|
||||
_ = removeListenerConfig(bucket, objectAPI)
|
||||
|
||||
// Notify all peers (including self) to update in-memory state
|
||||
S3PeersUpdateBucketListener(bucket, []listenerConfig{})
|
||||
|
||||
// Write success response.
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
+44
-14
@@ -24,6 +24,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Wrapper for calling GetBucketPolicy HTTP handler tests for both XL multiple disks and single node setup.
|
||||
@@ -32,7 +34,7 @@ func TestGetBucketLocationHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// test cases with sample input and expected output.
|
||||
@@ -177,7 +179,7 @@ func TestHeadBucketHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// test cases with sample input and expected output.
|
||||
@@ -284,7 +286,7 @@ func TestListMultipartUploadsHandler(t *testing.T) {
|
||||
|
||||
// testListMultipartUploadsHandler - Tests validate listing of multipart uploads.
|
||||
func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// Collection of non-exhaustive ListMultipartUploads test cases, valid errors
|
||||
@@ -455,7 +457,6 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
|
||||
|
||||
// verify response for V2 signed HTTP request.
|
||||
reqV2, err := newTestSignedRequestV2("GET", u, 0, nil, testCase.accessKey, testCase.secretKey)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||
}
|
||||
@@ -523,7 +524,7 @@ func TestListBucketsHandler(t *testing.T) {
|
||||
|
||||
// testListBucketsHandler - Tests validate listing of buckets.
|
||||
func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
@@ -616,7 +617,7 @@ func TestAPIDeleteMultipleObjectsHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
var err error
|
||||
@@ -632,8 +633,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
for i := 0; i < 10; i++ {
|
||||
objectName := "test-object-" + strconv.Itoa(i)
|
||||
// uploading the object.
|
||||
_, err = obj.PutObject(bucketName, objectName, int64(len(contentBytes)), bytes.NewBuffer(contentBytes),
|
||||
make(map[string]string), sha256sum)
|
||||
_, err = obj.PutObject(bucketName, objectName, mustGetHashReader(t, bytes.NewBuffer(contentBytes), int64(len(contentBytes)), "", sha256sum), nil)
|
||||
// if object upload fails stop the test.
|
||||
if err != nil {
|
||||
t.Fatalf("Put Object %d: Error uploading object: <ERROR> %v", i, err)
|
||||
@@ -650,6 +650,17 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
|
||||
return objectIdentifierList
|
||||
}
|
||||
getDeleteErrorList := func(objects []ObjectIdentifier) (deleteErrorList []DeleteError) {
|
||||
for _, obj := range objects {
|
||||
deleteErrorList = append(deleteErrorList, DeleteError{
|
||||
Code: errorCodeResponse[ErrAccessDenied].Code,
|
||||
Message: errorCodeResponse[ErrAccessDenied].Description,
|
||||
Key: obj.ObjectName,
|
||||
})
|
||||
}
|
||||
|
||||
return deleteErrorList
|
||||
}
|
||||
|
||||
requestList := []DeleteObjectsRequest{
|
||||
{Quiet: false, Objects: getObjectIdentifierList(objectNames[:5])},
|
||||
@@ -670,6 +681,10 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
errorResponse := generateMultiDeleteResponse(requestList[1].Quiet, requestList[1].Objects, nil)
|
||||
encodedErrorResponse := encodeResponse(errorResponse)
|
||||
|
||||
anonRequest := encodeResponse(requestList[0])
|
||||
anonResponse := generateMultiDeleteResponse(requestList[0].Quiet, nil, getDeleteErrorList(requestList[0].Objects))
|
||||
encodedAnonResponse := encodeResponse(anonResponse)
|
||||
|
||||
testCases := []struct {
|
||||
bucket string
|
||||
objects []byte
|
||||
@@ -718,15 +733,32 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
expectedContent: encodedErrorResponse,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
// Test case - 5.
|
||||
// Anonymous user access denied response
|
||||
// Currently anonymous users cannot delete multiple objects in Minio server
|
||||
{
|
||||
bucket: bucketName,
|
||||
objects: anonRequest,
|
||||
accessKey: "",
|
||||
secretKey: "",
|
||||
expectedContent: encodedAnonResponse,
|
||||
expectedRespStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
var req *http.Request
|
||||
var actualContent []byte
|
||||
|
||||
// Indicating that all parts are uploaded and initiating completeMultipartUpload.
|
||||
req, err = newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", bucketName),
|
||||
int64(len(testCase.objects)), bytes.NewReader(testCase.objects), testCase.accessKey, testCase.secretKey)
|
||||
// Generate a signed or anonymous request based on the testCase
|
||||
if testCase.accessKey != "" {
|
||||
req, err = newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", bucketName),
|
||||
int64(len(testCase.objects)), bytes.NewReader(testCase.objects), testCase.accessKey, testCase.secretKey)
|
||||
} else {
|
||||
req, err = newTestRequest("POST", getDeleteMultipleObjectsURL("", bucketName),
|
||||
int64(len(testCase.objects)), bytes.NewReader(testCase.objects))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request for DeleteMultipleObjects: <ERROR> %v", err)
|
||||
}
|
||||
@@ -753,8 +785,6 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
}
|
||||
}
|
||||
|
||||
// Currently anonymous user cannot delete multiple objects in Minio server, hence no test case is required.
|
||||
|
||||
// HTTP request to test the case of `objectLayer` being set to `nil`.
|
||||
// There is no need to use an existing bucket or valid input for creating the request,
|
||||
// since the `objectLayer==nil` check is performed before any other checks inside the handlers.
|
||||
@@ -777,7 +807,7 @@ func TestIsBucketActionAllowed(t *testing.T) {
|
||||
}
|
||||
|
||||
func testIsBucketActionAllowedHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
// input.
|
||||
|
||||
@@ -67,6 +67,7 @@ type notificationConfig struct {
|
||||
XMLName xml.Name `xml:"NotificationConfiguration"`
|
||||
QueueConfigs []queueConfig `xml:"QueueConfiguration"`
|
||||
LambdaConfigs []lambdaConfig `xml:"CloudFunctionConfiguration"`
|
||||
TopicConfigs []topicConfig `xml:"TopicConfiguration"`
|
||||
}
|
||||
|
||||
// listenerConfig structure represents run-time notification
|
||||
@@ -136,13 +137,13 @@ type bucketMeta struct {
|
||||
|
||||
// Notification event object metadata.
|
||||
type objectMeta struct {
|
||||
Key string `json:"key"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
ETag string `json:"eTag,omitempty"`
|
||||
ContentType string `json:"contentType,omitempty"`
|
||||
UserDefined map[string]string `json:"userDefined,omitempty"`
|
||||
VersionID string `json:"versionId,omitempty"`
|
||||
Sequencer string `json:"sequencer"`
|
||||
Key string `json:"key"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
ETag string `json:"eTag,omitempty"`
|
||||
ContentType string `json:"contentType,omitempty"`
|
||||
UserMetadata map[string]string `json:"userMetadata,omitempty"`
|
||||
VersionID string `json:"versionId,omitempty"`
|
||||
Sequencer string `json:"sequencer"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -22,7 +22,9 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -63,13 +65,13 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
||||
|
||||
// Attempt to successfully load notification config.
|
||||
nConfig, err := loadNotificationConfig(bucket, objAPI)
|
||||
if err != nil && err != errNoSuchNotifications {
|
||||
if err != nil && errorCause(err) != errNoSuchNotifications {
|
||||
errorIf(err, "Unable to read notification configuration.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
// For no notifications we write a dummy XML.
|
||||
if err == errNoSuchNotifications {
|
||||
if errorCause(err) == errNoSuchNotifications {
|
||||
// Complies with the s3 behavior in this regard.
|
||||
nConfig = ¬ificationConfig{}
|
||||
}
|
||||
@@ -150,6 +152,12 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
// Convert the incoming ARNs properly to the GetRegion().
|
||||
for i, queueConfig := range notificationCfg.QueueConfigs {
|
||||
queueConfig.QueueARN = unmarshalSqsARN(queueConfig.QueueARN).String()
|
||||
notificationCfg.QueueConfigs[i] = queueConfig
|
||||
}
|
||||
|
||||
// Put bucket notification config.
|
||||
err = PutBucketNotificationConfig(bucket, ¬ificationCfg, objectAPI)
|
||||
if err != nil {
|
||||
@@ -173,7 +181,9 @@ func PutBucketNotificationConfig(bucket string, ncfg *notificationConfig, objAPI
|
||||
// Acquire a write lock on bucket before modifying its
|
||||
// configuration.
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.Lock()
|
||||
if err := bucketLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
// Release lock after notifying peers
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
@@ -204,6 +214,7 @@ func writeNotification(w http.ResponseWriter, notification map[string][]Notifica
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add additional CRLF characters for client to
|
||||
// differentiate the individual events properly.
|
||||
_, err = w.Write(append(notificationBytes, crlf...))
|
||||
@@ -215,25 +226,61 @@ func writeNotification(w http.ResponseWriter, notification map[string][]Notifica
|
||||
// CRLF character used for chunked transfer in accordance with HTTP standards.
|
||||
var crlf = []byte("\r\n")
|
||||
|
||||
// sendBucketNotification - writes notification back to client on the response writer
|
||||
// for each notification input, otherwise writes whitespace characters periodically
|
||||
// to keep the connection active. Each notification messages are terminated by CRLF
|
||||
// character. Upon any error received on response writer the for loop exits.
|
||||
func sendBucketNotification(w http.ResponseWriter, arnListenerCh <-chan []NotificationEvent) {
|
||||
var dummyEvents = map[string][]NotificationEvent{"Records": nil}
|
||||
// Continuously write to client either timely empty structures
|
||||
// every 5 seconds, or return back the notifications.
|
||||
// listenChan A `listenChan` provides a data channel to send event
|
||||
// notifications on and `doneCh` to signal that events are no longer
|
||||
// being received. It also sends empty events (whitespace) to keep the
|
||||
// underlying connection alive.
|
||||
type listenChan struct {
|
||||
doneCh chan struct{}
|
||||
dataCh chan []NotificationEvent
|
||||
}
|
||||
|
||||
// newListenChan returns a listenChan with properly initialized
|
||||
// unbuffered channels.
|
||||
func newListenChan() *listenChan {
|
||||
return &listenChan{
|
||||
doneCh: make(chan struct{}),
|
||||
dataCh: make(chan []NotificationEvent),
|
||||
}
|
||||
}
|
||||
|
||||
// sendNotificationEvent sends notification events on the data channel
|
||||
// unless doneCh is not closed
|
||||
func (l *listenChan) sendNotificationEvent(events []NotificationEvent) {
|
||||
select {
|
||||
// Returns immediately if receiver has quit.
|
||||
case <-l.doneCh:
|
||||
// Blocks until receiver is available.
|
||||
case l.dataCh <- events:
|
||||
}
|
||||
}
|
||||
|
||||
// waitForListener writes event notification OR whitespaces on
|
||||
// ResponseWriter until client closes connection
|
||||
func (l *listenChan) waitForListener(w http.ResponseWriter) {
|
||||
|
||||
// Logs errors other than EPIPE and ECONNRESET.
|
||||
// EPIPE and ECONNRESET indicate that the client stopped
|
||||
// listening to notification events.
|
||||
logClientError := func(err error, msg string) {
|
||||
if oe, ok := err.(*net.OpError); ok && (oe.Err == syscall.EPIPE || oe.Err ==
|
||||
syscall.ECONNRESET) {
|
||||
errorIf(err, msg)
|
||||
}
|
||||
}
|
||||
|
||||
emptyEvent := map[string][]NotificationEvent{"Records": nil}
|
||||
defer close(l.doneCh)
|
||||
for {
|
||||
select {
|
||||
case events := <-arnListenerCh:
|
||||
case events := <-l.dataCh:
|
||||
if err := writeNotification(w, map[string][]NotificationEvent{"Records": events}); err != nil {
|
||||
errorIf(err, "Unable to write notification to client.")
|
||||
logClientError(err, "Unable to write notification")
|
||||
return
|
||||
}
|
||||
case <-time.After(globalSNSConnAlive): // Wait for global conn active seconds.
|
||||
if err := writeNotification(w, dummyEvents); err != nil {
|
||||
// FIXME - do not log for all errors.
|
||||
errorIf(err, "Unable to write notification to client.")
|
||||
case <-time.After(globalSNSConnAlive):
|
||||
if err := writeNotification(w, emptyEvent); err != nil {
|
||||
logClientError(err, "Unable to write empty notification")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -285,6 +332,7 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
return
|
||||
}
|
||||
|
||||
targetServer := GetLocalPeer(globalEndpoints)
|
||||
accountID := fmt.Sprintf("%d", UTCNow().UnixNano())
|
||||
accountARN := fmt.Sprintf(
|
||||
"%s:%s:%s:%s-%s",
|
||||
@@ -292,8 +340,9 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
serverConfig.GetRegion(),
|
||||
accountID,
|
||||
snsTypeMinio,
|
||||
globalMinioAddr,
|
||||
targetServer,
|
||||
)
|
||||
|
||||
var filterRules []filterRule
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
@@ -327,12 +376,11 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
},
|
||||
}
|
||||
|
||||
// Setup a listening channel that will receive notifications
|
||||
// from the RPC handler.
|
||||
nEventCh := make(chan []NotificationEvent)
|
||||
defer close(nEventCh)
|
||||
// Setup a listen channel to receive notifications like
|
||||
// s3:ObjectCreated, s3:ObjectDeleted etc.
|
||||
nListenCh := newListenChan()
|
||||
// Add channel for listener events
|
||||
if err = globalEventNotifier.AddListenerChan(accountARN, nEventCh); err != nil {
|
||||
if err = globalEventNotifier.AddListenerChan(accountARN, nListenCh); err != nil {
|
||||
errorIf(err, "Error adding a listener!")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -342,11 +390,11 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
defer globalEventNotifier.RemoveListenerChan(accountARN)
|
||||
|
||||
// Update topic config to bucket config and persist - as soon
|
||||
// as this call compelets, events may start appearing in
|
||||
// nEventCh
|
||||
// as this call completes, events may start appearing in
|
||||
// nListenCh
|
||||
lc := listenerConfig{
|
||||
TopicConfig: *topicCfg,
|
||||
TargetServer: globalMinioAddr,
|
||||
TargetServer: targetServer,
|
||||
}
|
||||
|
||||
err = AddBucketListenerConfig(bucket, &lc, objAPI)
|
||||
@@ -359,8 +407,16 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
// Add all common headers.
|
||||
setCommonHeaders(w)
|
||||
|
||||
// Start sending bucket notifications.
|
||||
sendBucketNotification(w, nEventCh)
|
||||
// https://github.com/containous/traefik/issues/560
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
|
||||
//
|
||||
// Proxies might buffer the connection to avoid this we
|
||||
// need the proper MIME type before writing to client.
|
||||
// This MIME header tells the proxies to avoid buffering
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
|
||||
// Start writing bucket notifications to ResponseWriter.
|
||||
nListenCh.waitForListener(w)
|
||||
}
|
||||
|
||||
// AddBucketListenerConfig - Updates on disk state of listeners, and
|
||||
@@ -377,7 +433,9 @@ func AddBucketListenerConfig(bucket string, lcfg *listenerConfig, objAPI ObjectL
|
||||
// Acquire a write lock on bucket before modifying its
|
||||
// configuration.
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.Lock()
|
||||
if err := bucketLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
// Release lock after notifying peers
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
@@ -418,7 +476,9 @@ func RemoveBucketListenerConfig(bucket string, lcfg *listenerConfig, objAPI Obje
|
||||
// Acquire a write lock on bucket before modifying its
|
||||
// configuration.
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.Lock()
|
||||
if bucketLock.GetLock(globalOperationTimeout) != nil {
|
||||
return
|
||||
}
|
||||
// Release lock after notifying peers
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
|
||||
@@ -21,12 +21,19 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Implement a dummy flush writer.
|
||||
@@ -51,7 +58,7 @@ func TestWriteNotification(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize test config %s", err)
|
||||
}
|
||||
defer removeAll(root)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
// Collection of test cases for each event writer.
|
||||
@@ -110,26 +117,56 @@ func TestWriteNotification(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendBucketNotification(t *testing.T) {
|
||||
// testResponseWriter implements `http.ResponseWriter` that buffers
|
||||
// response body in a `bytes.Buffer` and returns error after `failCount`
|
||||
// calls to `Write` method
|
||||
type testResponseWriter struct {
|
||||
mu sync.Mutex
|
||||
failCount int
|
||||
buf *bytes.Buffer
|
||||
m http.Header
|
||||
}
|
||||
|
||||
func newTestResponseWriter(failAt int) *testResponseWriter {
|
||||
return &testResponseWriter{
|
||||
buf: new(bytes.Buffer),
|
||||
m: make(http.Header),
|
||||
failCount: failAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (trw *testResponseWriter) Flush() {
|
||||
}
|
||||
|
||||
func (trw *testResponseWriter) Write(p []byte) (int, error) {
|
||||
trw.mu.Lock()
|
||||
defer trw.mu.Unlock()
|
||||
|
||||
if trw.failCount == 0 {
|
||||
return 0, errors.New("Custom error")
|
||||
}
|
||||
trw.failCount--
|
||||
|
||||
return trw.buf.Write(p)
|
||||
}
|
||||
|
||||
func (trw *testResponseWriter) Header() http.Header {
|
||||
return trw.m
|
||||
}
|
||||
|
||||
func (trw *testResponseWriter) WriteHeader(i int) {
|
||||
}
|
||||
|
||||
func TestListenChan(t *testing.T) {
|
||||
// Initialize a new test config.
|
||||
root, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize test config %s", err)
|
||||
}
|
||||
defer removeAll(root)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
eventCh := make(chan []NotificationEvent)
|
||||
|
||||
// Create a Pipe with FlushWriter on the write-side and bufio.Scanner
|
||||
// on the reader-side to receive notification over the listen channel in a
|
||||
// synchronized manner.
|
||||
pr, pw := io.Pipe()
|
||||
fw := newFlushWriter(pw)
|
||||
scanner := bufio.NewScanner(pr)
|
||||
// Start a go-routine to wait for notification events.
|
||||
go func(listenerCh <-chan []NotificationEvent) {
|
||||
sendBucketNotification(fw, listenerCh)
|
||||
}(eventCh)
|
||||
// Create a listen channel to manage notifications
|
||||
nListenCh := newListenChan()
|
||||
|
||||
// Construct notification events to be passed on the events channel.
|
||||
var events []NotificationEvent
|
||||
@@ -139,37 +176,68 @@ func TestSendBucketNotification(t *testing.T) {
|
||||
ObjectCreatedCopy,
|
||||
ObjectCreatedCompleteMultipartUpload,
|
||||
}
|
||||
|
||||
for _, evType := range evTypes {
|
||||
events = append(events, newNotificationEvent(eventData{
|
||||
Type: evType,
|
||||
}))
|
||||
}
|
||||
// Send notification events to the channel on which sendBucketNotification
|
||||
// is waiting on.
|
||||
eventCh <- events
|
||||
|
||||
// Read from the pipe connected to the ResponseWriter.
|
||||
scanner.Scan()
|
||||
notificationBytes := scanner.Bytes()
|
||||
|
||||
// Close the read-end and send an empty notification event on the channel
|
||||
// to signal sendBucketNotification to terminate.
|
||||
pr.Close()
|
||||
eventCh <- []NotificationEvent{}
|
||||
close(eventCh)
|
||||
|
||||
// Checking if the notification are the same as those sent over the channel.
|
||||
var notifications map[string][]NotificationEvent
|
||||
err = json.Unmarshal(notificationBytes, ¬ifications)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to Unmarshal notification")
|
||||
}
|
||||
records := notifications["Records"]
|
||||
for i, rec := range records {
|
||||
if rec.EventName == evTypes[i].String() {
|
||||
continue
|
||||
// Send notification events one-by-one
|
||||
go func() {
|
||||
for _, event := range events {
|
||||
nListenCh.sendNotificationEvent([]NotificationEvent{event})
|
||||
}
|
||||
t.Errorf("Failed to receive %d event %s", i, evTypes[i].String())
|
||||
}()
|
||||
|
||||
// Create a http.ResponseWriter that fails after len(events)
|
||||
// number of times
|
||||
trw := newTestResponseWriter(len(events))
|
||||
|
||||
// Wait for all (4) notification events to be received
|
||||
nListenCh.waitForListener(trw)
|
||||
|
||||
// Used to read JSON-formatted event stream line-by-line
|
||||
scanner := bufio.NewScanner(trw.buf)
|
||||
var records map[string][]NotificationEvent
|
||||
for i := 0; scanner.Scan(); i++ {
|
||||
err = json.Unmarshal(scanner.Bytes(), &records)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to unmarshal json %v", err)
|
||||
}
|
||||
|
||||
nEvent := records["Records"][0]
|
||||
if nEvent.EventName != evTypes[i].String() {
|
||||
t.Errorf("notification event name mismatch, expected %s but got %s", evTypes[i], nEvent.EventName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendNotificationEvent(t *testing.T) {
|
||||
// This test verifies that sendNotificationEvent function
|
||||
// returns once listenChan.doneCh is closed
|
||||
|
||||
l := newListenChan()
|
||||
testCh := make(chan struct{})
|
||||
timeout := 5 * time.Second
|
||||
|
||||
go func() {
|
||||
// Send one empty notification event on listenChan
|
||||
events := []NotificationEvent{NotificationEvent{}}
|
||||
l.sendNotificationEvent(events)
|
||||
testCh <- struct{}{}
|
||||
}()
|
||||
|
||||
// close l.doneCh to signal client exiting from
|
||||
// ListenBucketNotification API call
|
||||
close(l.doneCh)
|
||||
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
t.Fatalf("sendNotificationEvent didn't return after %v seconds", timeout)
|
||||
case <-testCh:
|
||||
// If we reach this case, sendNotificationEvent
|
||||
// returned on closing l.doneCh
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,12 +248,12 @@ func TestGetBucketNotificationHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testGetBucketNotificationHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
// declare sample configs
|
||||
filterRules := []filterRule{
|
||||
{
|
||||
Name: "prefix",
|
||||
Value: globalMinioDefaultOwnerID,
|
||||
Value: "minio",
|
||||
},
|
||||
{
|
||||
Name: "suffix",
|
||||
@@ -246,6 +314,95 @@ func testGetBucketNotificationHandler(obj ObjectLayer, instanceType, bucketName
|
||||
}
|
||||
}
|
||||
|
||||
func TestPutBucketNotificationHandler(t *testing.T) {
|
||||
ExecObjectLayerAPITest(t, testPutBucketNotificationHandler, []string{
|
||||
"PutBucketNotification",
|
||||
})
|
||||
}
|
||||
|
||||
func testPutBucketNotificationHandler(obj ObjectLayer, instanceType,
|
||||
bucketName string, apiRouter http.Handler, credentials auth.Credentials,
|
||||
t *testing.T) {
|
||||
|
||||
// declare sample configs
|
||||
filterRules := []filterRule{
|
||||
{
|
||||
Name: "prefix",
|
||||
Value: "minio",
|
||||
},
|
||||
{
|
||||
Name: "suffix",
|
||||
Value: "*.jpg",
|
||||
},
|
||||
}
|
||||
sampleSvcCfg := ServiceConfig{
|
||||
[]string{"s3:ObjectRemoved:*", "s3:ObjectCreated:*"},
|
||||
filterStruct{
|
||||
keyFilter{filterRules},
|
||||
},
|
||||
"1",
|
||||
}
|
||||
sampleNotifCfg := notificationConfig{
|
||||
QueueConfigs: []queueConfig{
|
||||
{
|
||||
ServiceConfig: sampleSvcCfg,
|
||||
QueueARN: "testqARN",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
{
|
||||
sampleNotifCfg.LambdaConfigs = []lambdaConfig{
|
||||
{
|
||||
sampleSvcCfg, "testLARN",
|
||||
},
|
||||
}
|
||||
xmlBytes, err := xml.Marshal(sampleNotifCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Unexpected err: %#v", instanceType, err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
req, err := newTestSignedRequestV4("PUT",
|
||||
getPutBucketNotificationURL("", bucketName),
|
||||
int64(len(xmlBytes)), bytes.NewReader(xmlBytes),
|
||||
credentials.AccessKey, credentials.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Failed to create HTTP testRequest for PutBucketNotification: <ERROR> %v",
|
||||
instanceType, err)
|
||||
}
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("Unexpected http response %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
sampleNotifCfg.LambdaConfigs = nil
|
||||
sampleNotifCfg.TopicConfigs = []topicConfig{
|
||||
{
|
||||
sampleSvcCfg, "testTARN",
|
||||
},
|
||||
}
|
||||
xmlBytes, err := xml.Marshal(sampleNotifCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Unexpected err: %#v", instanceType, err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
req, err := newTestSignedRequestV4("PUT",
|
||||
getPutBucketNotificationURL("", bucketName),
|
||||
int64(len(xmlBytes)), bytes.NewReader(xmlBytes),
|
||||
credentials.AccessKey, credentials.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Failed to create HTTP testRequest for PutBucketNotification: <ERROR> %v",
|
||||
instanceType, err)
|
||||
}
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("Unexpected http response %d", rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenBucketNotificationNilHandler(t *testing.T) {
|
||||
ExecObjectLayerAPITest(t, testListenBucketNotificationNilHandler, []string{
|
||||
"ListenBucketNotification",
|
||||
@@ -254,7 +411,7 @@ func TestListenBucketNotificationNilHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testListenBucketNotificationNilHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
// get random bucket name.
|
||||
randBucket := getRandomBucketName()
|
||||
|
||||
@@ -280,26 +437,28 @@ func testListenBucketNotificationNilHandler(obj ObjectLayer, instanceType, bucke
|
||||
}
|
||||
}
|
||||
|
||||
func testRemoveNotificationConfig(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
func testRemoveNotificationConfig(obj ObjectLayer, instanceType,
|
||||
bucketName string, apiRouter http.Handler, credentials auth.Credentials,
|
||||
t *testing.T) {
|
||||
|
||||
invalidBucket := "Invalid\\Bucket"
|
||||
// get random bucket name.
|
||||
randBucket := bucketName
|
||||
|
||||
sampleNotificationBytes := []byte("<NotificationConfiguration><TopicConfiguration>" +
|
||||
"<Event>s3:ObjectCreated:*</Event><Event>s3:ObjectRemoved:*</Event><Filter>" +
|
||||
"<S3Key></S3Key></Filter><Id></Id><Topic>arn:minio:sns:us-east-1:1474332374:listen</Topic>" +
|
||||
"</TopicConfiguration></NotificationConfiguration>")
|
||||
|
||||
// Set sample bucket notification on randBucket.
|
||||
testRec := httptest.NewRecorder()
|
||||
testReq, tErr := newTestSignedRequestV4("PUT", getPutBucketNotificationURL("", randBucket),
|
||||
int64(len(sampleNotificationBytes)), bytes.NewReader(sampleNotificationBytes),
|
||||
credentials.AccessKey, credentials.SecretKey)
|
||||
if tErr != nil {
|
||||
t.Fatalf("%s: Failed to create HTTP testRequest for PutBucketNotification: <ERROR> %v", instanceType, tErr)
|
||||
nCfg := notificationConfig{
|
||||
QueueConfigs: []queueConfig{
|
||||
{
|
||||
ServiceConfig: ServiceConfig{
|
||||
Events: []string{"s3:ObjectRemoved:*",
|
||||
"s3:ObjectCreated:*"},
|
||||
},
|
||||
QueueARN: "testqARN",
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := persistNotificationConfig(randBucket, &nCfg, obj); err != nil {
|
||||
t.Fatalf("Unexpected error: %#v", err)
|
||||
}
|
||||
apiRouter.ServeHTTP(testRec, testReq)
|
||||
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
|
||||
@@ -116,9 +116,13 @@ func checkARN(arn, arnType string) APIErrorCode {
|
||||
if len(strs) != 6 {
|
||||
return ErrARNNotification
|
||||
}
|
||||
if serverConfig.GetRegion() != "" {
|
||||
|
||||
// Server region is allowed to be empty by default,
|
||||
// in such a scenario ARN region is not validating
|
||||
// allowing all regions.
|
||||
if sregion := serverConfig.GetRegion(); sregion != "" {
|
||||
region := strs[3]
|
||||
if region != serverConfig.GetRegion() {
|
||||
if region != sregion {
|
||||
return ErrRegionNotification
|
||||
}
|
||||
}
|
||||
@@ -144,6 +148,9 @@ func isValidQueueID(queueARN string) bool {
|
||||
if isAMQPQueue(sqsARN) { // AMQP eueue.
|
||||
amqpN := serverConfig.Notify.GetAMQPByID(sqsARN.AccountID)
|
||||
return amqpN.Enable && amqpN.URL != ""
|
||||
} else if isMQTTQueue(sqsARN) {
|
||||
mqttN := serverConfig.Notify.GetMQTTByID(sqsARN.AccountID)
|
||||
return mqttN.Enable && mqttN.Broker != ""
|
||||
} else if isNATSQueue(sqsARN) {
|
||||
natsN := serverConfig.Notify.GetNATSByID(sqsARN.AccountID)
|
||||
return natsN.Enable && natsN.Address != ""
|
||||
@@ -232,6 +239,12 @@ func checkDuplicateQueueConfigs(configs []queueConfig) APIErrorCode {
|
||||
// if one of the config is malformed or has invalid data it is rejected.
|
||||
// Configuration is never applied partially.
|
||||
func validateNotificationConfig(nConfig notificationConfig) APIErrorCode {
|
||||
// Minio server does not support lambda/topic configurations
|
||||
// currently. Such configuration is rejected.
|
||||
if len(nConfig.LambdaConfigs) > 0 || len(nConfig.TopicConfigs) > 0 {
|
||||
return ErrUnsupportedNotification
|
||||
}
|
||||
|
||||
// Validate all queue configs.
|
||||
if s3Error := validateQueueConfigs(nConfig.QueueConfigs); s3Error != ErrNone {
|
||||
return s3Error
|
||||
@@ -251,6 +264,7 @@ func validateNotificationConfig(nConfig notificationConfig) APIErrorCode {
|
||||
// Unmarshals input value of AWS ARN format into minioSqs object.
|
||||
// Returned value represents minio sqs types, currently supported are
|
||||
// - amqp
|
||||
// - mqtt
|
||||
// - nats
|
||||
// - elasticsearch
|
||||
// - redis
|
||||
@@ -263,9 +277,13 @@ func unmarshalSqsARN(queueARN string) (mSqs arnSQS) {
|
||||
if len(strs) != 6 {
|
||||
return
|
||||
}
|
||||
if serverConfig.GetRegion() != "" {
|
||||
|
||||
// Server region is allowed to be empty by default,
|
||||
// in such a scenario ARN region is not validating
|
||||
// allowing all regions.
|
||||
if sregion := serverConfig.GetRegion(); sregion != "" {
|
||||
region := strs[3]
|
||||
if region != serverConfig.GetRegion() {
|
||||
if region != sregion {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -273,6 +291,8 @@ func unmarshalSqsARN(queueARN string) (mSqs arnSQS) {
|
||||
switch sqsType {
|
||||
case queueTypeAMQP:
|
||||
mSqs.Type = queueTypeAMQP
|
||||
case queueTypeMQTT:
|
||||
mSqs.Type = queueTypeMQTT
|
||||
case queueTypeNATS:
|
||||
mSqs.Type = queueTypeNATS
|
||||
case queueTypeElastic:
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -222,7 +223,7 @@ func TestQueueARN(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
testCases := []struct {
|
||||
queueARN string
|
||||
@@ -299,7 +300,7 @@ func TestQueueARN(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
testCases = []struct {
|
||||
queueARN string
|
||||
@@ -332,7 +333,7 @@ func TestUnmarshalSQSARN(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
testCases := []struct {
|
||||
queueARN string
|
||||
@@ -358,6 +359,11 @@ func TestUnmarshalSQSARN(t *testing.T) {
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:amqp",
|
||||
Type: "amqp",
|
||||
},
|
||||
// Valid mqtt queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:mqtt",
|
||||
Type: "mqtt",
|
||||
},
|
||||
// Invalid empty queue arn.
|
||||
{
|
||||
queueARN: "",
|
||||
@@ -387,7 +393,7 @@ func TestUnmarshalSQSARN(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
testCases = []struct {
|
||||
queueARN string
|
||||
|
||||
@@ -17,16 +17,17 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
mux "github.com/gorilla/mux"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,8 @@ const maxAccessPolicySize = 20 * humanize.KiByte
|
||||
|
||||
// Verify if a given action is valid for the url path based on the
|
||||
// existing bucket access policy.
|
||||
func bucketPolicyEvalStatements(action string, resource string, conditions map[string]set.StringSet, statements []policyStatement) bool {
|
||||
func bucketPolicyEvalStatements(action string, resource string, conditions policy.ConditionKeyMap,
|
||||
statements []policy.Statement) bool {
|
||||
for _, statement := range statements {
|
||||
if bucketPolicyMatchStatement(action, resource, conditions, statement) {
|
||||
if statement.Effect == "Allow" {
|
||||
@@ -51,7 +53,8 @@ func bucketPolicyEvalStatements(action string, resource string, conditions map[s
|
||||
}
|
||||
|
||||
// Verify if action, resource and conditions match input policy statement.
|
||||
func bucketPolicyMatchStatement(action string, resource string, conditions map[string]set.StringSet, statement policyStatement) bool {
|
||||
func bucketPolicyMatchStatement(action string, resource string, conditions policy.ConditionKeyMap,
|
||||
statement policy.Statement) bool {
|
||||
// Verify if action, resource and condition match in given statement.
|
||||
return (bucketPolicyActionMatch(action, statement) &&
|
||||
bucketPolicyResourceMatch(resource, statement) &&
|
||||
@@ -59,7 +62,7 @@ func bucketPolicyMatchStatement(action string, resource string, conditions map[s
|
||||
}
|
||||
|
||||
// Verify if given action matches with policy statement.
|
||||
func bucketPolicyActionMatch(action string, statement policyStatement) bool {
|
||||
func bucketPolicyActionMatch(action string, statement policy.Statement) bool {
|
||||
return !statement.Actions.FuncMatch(actionMatch, action).IsEmpty()
|
||||
}
|
||||
|
||||
@@ -81,8 +84,20 @@ func refererMatch(pattern, referer string) bool {
|
||||
return wildcard.MatchSimple(pattern, referer)
|
||||
}
|
||||
|
||||
// isIPInCIDR - checks if a given a IP address is a member of the given subnet.
|
||||
func isIPInCIDR(cidr, ip string) bool {
|
||||
// AWS S3 spec says IPs must use standard CIDR notation.
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html#example-bucket-policies-use-case-3.
|
||||
_, cidrNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return false // If provided CIDR can't be parsed no IP will be in the subnet.
|
||||
}
|
||||
addr := net.ParseIP(ip)
|
||||
return cidrNet.Contains(addr)
|
||||
}
|
||||
|
||||
// Verify if given resource matches with policy statement.
|
||||
func bucketPolicyResourceMatch(resource string, statement policyStatement) bool {
|
||||
func bucketPolicyResourceMatch(resource string, statement policy.Statement) bool {
|
||||
// the resource rule for object could contain "*" wild card.
|
||||
// the requested object can be given access based on the already set bucket policy if
|
||||
// the match is successful.
|
||||
@@ -91,17 +106,20 @@ func bucketPolicyResourceMatch(resource string, statement policyStatement) bool
|
||||
}
|
||||
|
||||
// Verify if given condition matches with policy statement.
|
||||
func bucketPolicyConditionMatch(conditions map[string]set.StringSet, statement policyStatement) bool {
|
||||
func bucketPolicyConditionMatch(conditions policy.ConditionKeyMap, statement policy.Statement) bool {
|
||||
// Supports following conditions.
|
||||
// - StringEquals
|
||||
// - StringNotEquals
|
||||
// - StringLike
|
||||
// - StringNotLike
|
||||
// - IpAddress
|
||||
// - NotIpAddress
|
||||
//
|
||||
// Supported applicable condition keys for each conditions.
|
||||
// - s3:prefix
|
||||
// - s3:max-keys
|
||||
// - s3:aws-Referer
|
||||
// - s3:aws-SourceIp
|
||||
|
||||
// The following loop evaluates the logical AND of all the
|
||||
// conditions in the statement. Note: we can break out of the
|
||||
@@ -159,6 +177,37 @@ func bucketPolicyConditionMatch(conditions map[string]set.StringSet, statement p
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else if condition == "IpAddress" {
|
||||
awsIps := conditionKeyVal["aws:SourceIp"]
|
||||
// Skip empty condition, it is trivially satisfied.
|
||||
if awsIps.IsEmpty() {
|
||||
continue
|
||||
}
|
||||
// wildcard match of ip if statement was not empty.
|
||||
// Find a valid ip.
|
||||
ipFound := false
|
||||
for ip := range conditions["ip"] {
|
||||
if !awsIps.FuncMatch(isIPInCIDR, ip).IsEmpty() {
|
||||
ipFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ipFound {
|
||||
return false
|
||||
}
|
||||
} else if condition == "NotIpAddress" {
|
||||
awsIps := conditionKeyVal["aws:SourceIp"]
|
||||
// Skip empty condition, it is trivially satisfied.
|
||||
if awsIps.IsEmpty() {
|
||||
continue
|
||||
}
|
||||
// wildcard match of ip if statement was not empty.
|
||||
// Find if nothing matches.
|
||||
for ip := range conditions["ip"] {
|
||||
if !awsIps.FuncMatch(isIPInCIDR, ip).IsEmpty() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,13 +302,11 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
||||
|
||||
// Delete bucket access policy, by passing an empty policy
|
||||
// struct.
|
||||
if err := persistAndNotifyBucketPolicyChange(bucket, policyChange{true, nil}, objAPI); err != nil {
|
||||
switch err.(type) {
|
||||
case BucketPolicyNotFound:
|
||||
writeErrorResponse(w, ErrNoSuchBucketPolicy, r.URL)
|
||||
default:
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
}
|
||||
err = persistAndNotifyBucketPolicyChange(bucket, policyChange{
|
||||
true, policy.BucketAccessPolicy{},
|
||||
}, objAPI)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -298,15 +345,17 @@ func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
policy, err := readBucketPolicy(bucket, objAPI)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to read bucket policy.")
|
||||
switch err.(type) {
|
||||
case BucketPolicyNotFound:
|
||||
writeErrorResponse(w, ErrNoSuchBucketPolicy, r.URL)
|
||||
default:
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
}
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
policyBytes, err := json.Marshal(&policy)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to marshal bucket policy.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write to client.
|
||||
fmt.Fprint(w, policy)
|
||||
w.Write(policyBytes)
|
||||
}
|
||||
|
||||
@@ -25,15 +25,17 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Tests validate Bucket policy resource matcher.
|
||||
func TestBucketPolicyResourceMatch(t *testing.T) {
|
||||
|
||||
// generates statement with given resource..
|
||||
generateStatement := func(resource string) policyStatement {
|
||||
statement := policyStatement{}
|
||||
generateStatement := func(resource string) policy.Statement {
|
||||
statement := policy.Statement{}
|
||||
statement.Resources = set.CreateStringSet([]string{resource}...)
|
||||
return statement
|
||||
}
|
||||
@@ -45,7 +47,7 @@ func TestBucketPolicyResourceMatch(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
resourceToMatch string
|
||||
statement policyStatement
|
||||
statement policy.Statement
|
||||
expectedResourceMatch bool
|
||||
}{
|
||||
// Test case 1-4.
|
||||
@@ -85,7 +87,7 @@ func TestBucketPolicyResourceMatch(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestBucketPolicyActionMatch - Test validates whether given action on the
|
||||
// bucket/object matches the allowed actions in policyStatement.
|
||||
// bucket/object matches the allowed actions in policy.Statement.
|
||||
// This test preserves the allowed actions for all 3 sets of policies, that is read-write,read-only, write-only.
|
||||
// The intention of the test is to catch any changes made to allowed action for on eof the above 3 major policy groups mentioned.
|
||||
func TestBucketPolicyActionMatch(t *testing.T) {
|
||||
@@ -94,7 +96,7 @@ func TestBucketPolicyActionMatch(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
action string
|
||||
statement policyStatement
|
||||
statement policy.Statement
|
||||
expectedResult bool
|
||||
}{
|
||||
// s3:GetBucketLocation is the action necessary to be present in the bucket policy to allow
|
||||
@@ -247,7 +249,7 @@ func TestPutBucketPolicyHandler(t *testing.T) {
|
||||
|
||||
// testPutBucketPolicyHandler - Test for Bucket policy end point.
|
||||
func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
bucketName1 := fmt.Sprintf("%s-1", bucketName)
|
||||
@@ -454,7 +456,7 @@ func TestGetBucketPolicyHandler(t *testing.T) {
|
||||
|
||||
// testGetBucketPolicyHandler - Test for end point which fetches the access policy json of the given bucket.
|
||||
func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
// initialize bucket policy.
|
||||
initBucketPolicies(obj)
|
||||
|
||||
@@ -643,7 +645,7 @@ func TestDeleteBucketPolicyHandler(t *testing.T) {
|
||||
|
||||
// testDeleteBucketPolicyHandler - Test for Delete bucket policy end point.
|
||||
func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
// initialize bucket policy.
|
||||
initBucketPolicies(obj)
|
||||
|
||||
@@ -843,28 +845,28 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
||||
|
||||
// TestBucketPolicyConditionMatch - Tests to validate whether bucket policy conditions match.
|
||||
func TestBucketPolicyConditionMatch(t *testing.T) {
|
||||
// obtain the inner map[string]set.StringSet for policyStatement.Conditions .
|
||||
// obtain the inner map[string]set.StringSet for policy.Statement.Conditions.
|
||||
getInnerMap := func(key2, value string) map[string]set.StringSet {
|
||||
innerMap := make(map[string]set.StringSet)
|
||||
innerMap[key2] = set.CreateStringSet(value)
|
||||
return innerMap
|
||||
}
|
||||
|
||||
// obtain policyStatement with Conditions set.
|
||||
getStatementWithCondition := func(key1, key2, value string) policyStatement {
|
||||
// obtain policy.Statement with Conditions set.
|
||||
getStatementWithCondition := func(key1, key2, value string) policy.Statement {
|
||||
innerMap := getInnerMap(key2, value)
|
||||
// to set policyStatment.Conditions .
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions[key1] = innerMap
|
||||
// new policy statement.
|
||||
statement := policyStatement{}
|
||||
statement := policy.Statement{}
|
||||
// set the condition.
|
||||
statement.Conditions = conditions
|
||||
return statement
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
statementCondition policyStatement
|
||||
statementCondition policy.Statement
|
||||
condition map[string]set.StringSet
|
||||
|
||||
expectedMatch bool
|
||||
@@ -970,6 +972,34 @@ func TestBucketPolicyConditionMatch(t *testing.T) {
|
||||
condition: getInnerMap("referer", "http://somethingelse.com/"),
|
||||
expectedMatch: true,
|
||||
},
|
||||
// Test case 13.
|
||||
// IpAddress condition evaluates to true.
|
||||
{
|
||||
statementCondition: getStatementWithCondition("IpAddress", "aws:SourceIp", "54.240.143.0/24"),
|
||||
condition: getInnerMap("ip", "54.240.143.2"),
|
||||
expectedMatch: true,
|
||||
},
|
||||
// Test case 14.
|
||||
// IpAddress condition evaluates to false.
|
||||
{
|
||||
statementCondition: getStatementWithCondition("IpAddress", "aws:SourceIp", "54.240.143.0/24"),
|
||||
condition: getInnerMap("ip", "127.240.143.224"),
|
||||
expectedMatch: false,
|
||||
},
|
||||
// Test case 15.
|
||||
// NotIpAddress condition evaluates to true.
|
||||
{
|
||||
statementCondition: getStatementWithCondition("NotIpAddress", "aws:SourceIp", "54.240.143.0/24"),
|
||||
condition: getInnerMap("ip", "54.240.144.188"),
|
||||
expectedMatch: true,
|
||||
},
|
||||
// Test case 16.
|
||||
// NotIpAddress condition evaluates to false.
|
||||
{
|
||||
statementCondition: getStatementWithCondition("NotIpAddress", "aws:SourceIp", "54.240.143.0/24"),
|
||||
condition: getInnerMap("ip", "54.240.143.243"),
|
||||
expectedMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
|
||||
+34
-97
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -26,12 +26,16 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
)
|
||||
|
||||
var conditionKeyActionMap = map[string]set.StringSet{
|
||||
"s3:prefix": set.CreateStringSet("s3:ListBucket"),
|
||||
"s3:max-keys": set.CreateStringSet("s3:ListBucket"),
|
||||
var emptyBucketPolicy = policy.BucketAccessPolicy{}
|
||||
|
||||
var conditionKeyActionMap = policy.ConditionKeyMap{
|
||||
"s3:prefix": set.CreateStringSet("s3:ListBucket", "s3:ListBucketMultipartUploads"),
|
||||
"s3:max-keys": set.CreateStringSet("s3:ListBucket", "s3:ListBucketMultipartUploads",
|
||||
"s3:ListMultipartUploadParts"),
|
||||
}
|
||||
|
||||
// supportedActionMap - lists all the actions supported by minio.
|
||||
@@ -40,50 +44,25 @@ var supportedActionMap = set.CreateStringSet("*", "s3:*", "s3:GetObject",
|
||||
"s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts")
|
||||
|
||||
// supported Conditions type.
|
||||
var supportedConditionsType = set.CreateStringSet("StringEquals", "StringNotEquals", "StringLike", "StringNotLike")
|
||||
var supportedConditionsType = set.CreateStringSet("StringEquals", "StringNotEquals", "StringLike", "StringNotLike", "IpAddress", "NotIpAddress")
|
||||
|
||||
// Validate s3:prefix, s3:max-keys are present if not
|
||||
// supported keys for the conditions.
|
||||
var supportedConditionsKey = set.CreateStringSet("s3:prefix", "s3:max-keys", "aws:Referer")
|
||||
var supportedConditionsKey = set.CreateStringSet("s3:prefix", "s3:max-keys", "aws:Referer", "aws:SourceIp")
|
||||
|
||||
// supportedEffectMap - supported effects.
|
||||
var supportedEffectMap = set.CreateStringSet("Allow", "Deny")
|
||||
|
||||
// Statement - minio policy statement
|
||||
type policyStatement struct {
|
||||
Actions set.StringSet `json:"Action"`
|
||||
Conditions map[string]map[string]set.StringSet `json:"Condition,omitempty"`
|
||||
Effect string
|
||||
Principal interface{} `json:"Principal"`
|
||||
Resources set.StringSet `json:"Resource"`
|
||||
Sid string
|
||||
}
|
||||
|
||||
// bucketPolicy - collection of various bucket policy statements.
|
||||
type bucketPolicy struct {
|
||||
Version string // date in YYYY-MM-DD format
|
||||
Statements []policyStatement `json:"Statement"`
|
||||
}
|
||||
|
||||
// Stringer implementation for the bucket policies.
|
||||
func (b bucketPolicy) String() string {
|
||||
bbytes, err := json.Marshal(&b)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to marshal bucket policy into JSON %#v", b)
|
||||
return ""
|
||||
}
|
||||
return string(bbytes)
|
||||
}
|
||||
|
||||
// isValidActions - are actions valid.
|
||||
func isValidActions(actions set.StringSet) (err error) {
|
||||
// Statement actions cannot be empty.
|
||||
if len(actions) == 0 {
|
||||
if actions.IsEmpty() {
|
||||
err = errors.New("Action list cannot be empty")
|
||||
return err
|
||||
}
|
||||
if unsupportedActions := actions.Difference(supportedActionMap); !unsupportedActions.IsEmpty() {
|
||||
err = fmt.Errorf("Unsupported actions found: ‘%#v’, please validate your policy document", unsupportedActions)
|
||||
err = fmt.Errorf("Unsupported actions found: ‘%#v’, please validate your policy document",
|
||||
unsupportedActions)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -106,7 +85,7 @@ func isValidEffect(effect string) (err error) {
|
||||
// isValidResources - are valid resources.
|
||||
func isValidResources(resources set.StringSet) (err error) {
|
||||
// Statement resources cannot be empty.
|
||||
if len(resources) == 0 {
|
||||
if resources.IsEmpty() {
|
||||
err = errors.New("Resource list cannot be empty")
|
||||
return err
|
||||
}
|
||||
@@ -124,60 +103,17 @@ func isValidResources(resources set.StringSet) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse principals parses a incoming json. Handles cases for
|
||||
// these three combinations.
|
||||
// - "Principal": "*",
|
||||
// - "Principal": { "AWS" : "*" }
|
||||
// - "Principal": { "AWS" : [ "*" ]}
|
||||
func parsePrincipals(principal interface{}) set.StringSet {
|
||||
principals, ok := principal.(map[string]interface{})
|
||||
if !ok {
|
||||
var principalStr string
|
||||
principalStr, ok = principal.(string)
|
||||
if ok {
|
||||
return set.CreateStringSet(principalStr)
|
||||
}
|
||||
} // else {
|
||||
var principalStrs []string
|
||||
for _, p := range principals {
|
||||
principalStr, isStr := p.(string)
|
||||
if !isStr {
|
||||
principalsAdd, isInterface := p.([]interface{})
|
||||
if !isInterface {
|
||||
principalStrsAddr, isStrs := p.([]string)
|
||||
if !isStrs {
|
||||
continue
|
||||
}
|
||||
principalStrs = append(principalStrs, principalStrsAddr...)
|
||||
} else {
|
||||
for _, pa := range principalsAdd {
|
||||
var pstr string
|
||||
pstr, isStr = pa.(string)
|
||||
if !isStr {
|
||||
continue
|
||||
}
|
||||
principalStrs = append(principalStrs, pstr)
|
||||
}
|
||||
}
|
||||
continue
|
||||
} // else {
|
||||
principalStrs = append(principalStrs, principalStr)
|
||||
}
|
||||
return set.CreateStringSet(principalStrs...)
|
||||
}
|
||||
|
||||
// isValidPrincipals - are valid principals.
|
||||
func isValidPrincipals(principal interface{}) (err error) {
|
||||
principals := parsePrincipals(principal)
|
||||
// Statement principal should have a value.
|
||||
if len(principals) == 0 {
|
||||
err = errors.New("Principal cannot be empty")
|
||||
return err
|
||||
func isValidPrincipals(principal policy.User) (err error) {
|
||||
if principal.AWS.IsEmpty() {
|
||||
return errors.New("Principal cannot be empty")
|
||||
}
|
||||
if unsuppPrincipals := principals.Difference(set.CreateStringSet([]string{"*"}...)); !unsuppPrincipals.IsEmpty() {
|
||||
if diff := principal.AWS.Difference(set.CreateStringSet("*")); !diff.IsEmpty() {
|
||||
// Minio does not support or implement IAM, "*" is the only valid value.
|
||||
// Amazon s3 doc on principals: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Principal
|
||||
err = fmt.Errorf("Unsupported principals found: ‘%#v’, please validate your policy document", unsuppPrincipals)
|
||||
// Amazon s3 doc on principal:
|
||||
// http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Principal
|
||||
err = fmt.Errorf("Unsupported principals found: ‘%#v’, please validate your policy document",
|
||||
diff)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -185,7 +121,7 @@ func isValidPrincipals(principal interface{}) (err error) {
|
||||
|
||||
// isValidConditions - returns nil if the given conditions valid and
|
||||
// corresponding error otherwise.
|
||||
func isValidConditions(actions set.StringSet, conditions map[string]map[string]set.StringSet) (err error) {
|
||||
func isValidConditions(actions set.StringSet, conditions policy.ConditionMap) (err error) {
|
||||
// Verify conditions should be valid. Validate if only
|
||||
// supported condition keys are present and return error
|
||||
// otherwise.
|
||||
@@ -239,7 +175,7 @@ func resourcePrefix(resource string) string {
|
||||
// checkBucketPolicyResources validates Resources in unmarshalled bucket policy structure.
|
||||
// - Resources are validated against the given set of Actions.
|
||||
// -
|
||||
func checkBucketPolicyResources(bucket string, bucketPolicy *bucketPolicy) APIErrorCode {
|
||||
func checkBucketPolicyResources(bucket string, bucketPolicy policy.BucketAccessPolicy) APIErrorCode {
|
||||
// Validate statements for special actions and collect resources
|
||||
// for others to validate nesting.
|
||||
var resourceMap = set.NewStringSet()
|
||||
@@ -294,27 +230,27 @@ func checkBucketPolicyResources(bucket string, bucketPolicy *bucketPolicy) APIEr
|
||||
|
||||
// parseBucketPolicy - parses and validates if bucket policy is of
|
||||
// proper JSON and follows allowed restrictions with policy standards.
|
||||
func parseBucketPolicy(bucketPolicyReader io.Reader, policy *bucketPolicy) (err error) {
|
||||
func parseBucketPolicy(bucketPolicyReader io.Reader, bktPolicy *policy.BucketAccessPolicy) (err error) {
|
||||
// Parse bucket policy reader.
|
||||
decoder := json.NewDecoder(bucketPolicyReader)
|
||||
if err = decoder.Decode(&policy); err != nil {
|
||||
if err = decoder.Decode(bktPolicy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Policy version cannot be empty.
|
||||
if len(policy.Version) == 0 {
|
||||
if len(bktPolicy.Version) == 0 {
|
||||
err = errors.New("Policy version cannot be empty")
|
||||
return err
|
||||
}
|
||||
|
||||
// Policy statements cannot be empty.
|
||||
if len(policy.Statements) == 0 {
|
||||
if len(bktPolicy.Statements) == 0 {
|
||||
err = errors.New("Policy statement cannot be empty")
|
||||
return err
|
||||
}
|
||||
|
||||
// Loop through all policy statements and validate entries.
|
||||
for _, statement := range policy.Statements {
|
||||
for _, statement := range bktPolicy.Statements {
|
||||
// Statement effect should be valid.
|
||||
if err := isValidEffect(statement.Effect); err != nil {
|
||||
return err
|
||||
@@ -339,19 +275,20 @@ func parseBucketPolicy(bucketPolicyReader io.Reader, policy *bucketPolicy) (err
|
||||
|
||||
// Separate deny and allow statements, so that we can apply deny
|
||||
// statements in the beginning followed by Allow statements.
|
||||
var denyStatements []policyStatement
|
||||
var allowStatements []policyStatement
|
||||
for _, statement := range policy.Statements {
|
||||
var denyStatements []policy.Statement
|
||||
var allowStatements []policy.Statement
|
||||
for _, statement := range bktPolicy.Statements {
|
||||
if statement.Effect == "Deny" {
|
||||
denyStatements = append(denyStatements, statement)
|
||||
continue
|
||||
}
|
||||
|
||||
// else if statement.Effect == "Allow"
|
||||
allowStatements = append(allowStatements, statement)
|
||||
}
|
||||
|
||||
// Deny statements are enforced first once matched.
|
||||
policy.Statements = append(denyStatements, allowStatements...)
|
||||
bktPolicy.Statements = append(denyStatements, allowStatements...)
|
||||
|
||||
// Return successfully parsed policy structure.
|
||||
return nil
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
@@ -74,11 +75,11 @@ var (
|
||||
)
|
||||
|
||||
// Obtain bucket statement for read-write bucketPolicy.
|
||||
func getReadWriteObjectStatement(bucketName, objectPrefix string) policyStatement {
|
||||
objectResourceStatement := policyStatement{}
|
||||
func getReadWriteObjectStatement(bucketName, objectPrefix string) policy.Statement {
|
||||
objectResourceStatement := policy.Statement{}
|
||||
objectResourceStatement.Effect = "Allow"
|
||||
objectResourceStatement.Principal = map[string]interface{}{
|
||||
"AWS": "*",
|
||||
objectResourceStatement.Principal = policy.User{
|
||||
AWS: set.StringSet{"*": struct{}{}},
|
||||
}
|
||||
objectResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName+"/"+objectPrefix+"*")}...)
|
||||
objectResourceStatement.Actions = set.CreateStringSet(readWriteObjectActions...)
|
||||
@@ -86,11 +87,11 @@ func getReadWriteObjectStatement(bucketName, objectPrefix string) policyStatemen
|
||||
}
|
||||
|
||||
// Obtain object statement for read-write bucketPolicy.
|
||||
func getReadWriteBucketStatement(bucketName, objectPrefix string) policyStatement {
|
||||
bucketResourceStatement := policyStatement{}
|
||||
func getReadWriteBucketStatement(bucketName, objectPrefix string) policy.Statement {
|
||||
bucketResourceStatement := policy.Statement{}
|
||||
bucketResourceStatement.Effect = "Allow"
|
||||
bucketResourceStatement.Principal = map[string]interface{}{
|
||||
"AWS": "*",
|
||||
bucketResourceStatement.Principal = policy.User{
|
||||
AWS: set.StringSet{"*": struct{}{}},
|
||||
}
|
||||
bucketResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName)}...)
|
||||
bucketResourceStatement.Actions = set.CreateStringSet(readWriteBucketActions...)
|
||||
@@ -98,19 +99,19 @@ func getReadWriteBucketStatement(bucketName, objectPrefix string) policyStatemen
|
||||
}
|
||||
|
||||
// Obtain statements for read-write bucketPolicy.
|
||||
func getReadWriteStatement(bucketName, objectPrefix string) []policyStatement {
|
||||
statements := []policyStatement{}
|
||||
func getReadWriteStatement(bucketName, objectPrefix string) []policy.Statement {
|
||||
statements := []policy.Statement{}
|
||||
// Save the read write policy.
|
||||
statements = append(statements, getReadWriteBucketStatement(bucketName, objectPrefix), getReadWriteObjectStatement(bucketName, objectPrefix))
|
||||
return statements
|
||||
}
|
||||
|
||||
// Obtain bucket statement for read only bucketPolicy.
|
||||
func getReadOnlyBucketStatement(bucketName, objectPrefix string) policyStatement {
|
||||
bucketResourceStatement := policyStatement{}
|
||||
func getReadOnlyBucketStatement(bucketName, objectPrefix string) policy.Statement {
|
||||
bucketResourceStatement := policy.Statement{}
|
||||
bucketResourceStatement.Effect = "Allow"
|
||||
bucketResourceStatement.Principal = map[string]interface{}{
|
||||
"AWS": "*",
|
||||
bucketResourceStatement.Principal = policy.User{
|
||||
AWS: set.StringSet{"*": struct{}{}},
|
||||
}
|
||||
bucketResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName)}...)
|
||||
bucketResourceStatement.Actions = set.CreateStringSet(readOnlyBucketActions...)
|
||||
@@ -118,11 +119,11 @@ func getReadOnlyBucketStatement(bucketName, objectPrefix string) policyStatement
|
||||
}
|
||||
|
||||
// Obtain object statement for read only bucketPolicy.
|
||||
func getReadOnlyObjectStatement(bucketName, objectPrefix string) policyStatement {
|
||||
objectResourceStatement := policyStatement{}
|
||||
func getReadOnlyObjectStatement(bucketName, objectPrefix string) policy.Statement {
|
||||
objectResourceStatement := policy.Statement{}
|
||||
objectResourceStatement.Effect = "Allow"
|
||||
objectResourceStatement.Principal = map[string]interface{}{
|
||||
"AWS": "*",
|
||||
objectResourceStatement.Principal = policy.User{
|
||||
AWS: set.StringSet{"*": struct{}{}},
|
||||
}
|
||||
objectResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName+"/"+objectPrefix+"*")}...)
|
||||
objectResourceStatement.Actions = set.CreateStringSet(readOnlyObjectActions...)
|
||||
@@ -130,20 +131,20 @@ func getReadOnlyObjectStatement(bucketName, objectPrefix string) policyStatement
|
||||
}
|
||||
|
||||
// Obtain statements for read only bucketPolicy.
|
||||
func getReadOnlyStatement(bucketName, objectPrefix string) []policyStatement {
|
||||
statements := []policyStatement{}
|
||||
func getReadOnlyStatement(bucketName, objectPrefix string) []policy.Statement {
|
||||
statements := []policy.Statement{}
|
||||
// Save the read only policy.
|
||||
statements = append(statements, getReadOnlyBucketStatement(bucketName, objectPrefix), getReadOnlyObjectStatement(bucketName, objectPrefix))
|
||||
return statements
|
||||
}
|
||||
|
||||
// Obtain bucket statements for write only bucketPolicy.
|
||||
func getWriteOnlyBucketStatement(bucketName, objectPrefix string) policyStatement {
|
||||
func getWriteOnlyBucketStatement(bucketName, objectPrefix string) policy.Statement {
|
||||
|
||||
bucketResourceStatement := policyStatement{}
|
||||
bucketResourceStatement := policy.Statement{}
|
||||
bucketResourceStatement.Effect = "Allow"
|
||||
bucketResourceStatement.Principal = map[string]interface{}{
|
||||
"AWS": "*",
|
||||
bucketResourceStatement.Principal = policy.User{
|
||||
AWS: set.StringSet{"*": struct{}{}},
|
||||
}
|
||||
bucketResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName)}...)
|
||||
bucketResourceStatement.Actions = set.CreateStringSet(writeOnlyBucketActions...)
|
||||
@@ -151,11 +152,11 @@ func getWriteOnlyBucketStatement(bucketName, objectPrefix string) policyStatemen
|
||||
}
|
||||
|
||||
// Obtain object statements for write only bucketPolicy.
|
||||
func getWriteOnlyObjectStatement(bucketName, objectPrefix string) policyStatement {
|
||||
objectResourceStatement := policyStatement{}
|
||||
func getWriteOnlyObjectStatement(bucketName, objectPrefix string) policy.Statement {
|
||||
objectResourceStatement := policy.Statement{}
|
||||
objectResourceStatement.Effect = "Allow"
|
||||
objectResourceStatement.Principal = map[string]interface{}{
|
||||
"AWS": "*",
|
||||
objectResourceStatement.Principal = policy.User{
|
||||
AWS: set.StringSet{"*": struct{}{}},
|
||||
}
|
||||
objectResourceStatement.Resources = set.CreateStringSet([]string{fmt.Sprintf("%s%s", bucketARNPrefix, bucketName+"/"+objectPrefix+"*")}...)
|
||||
objectResourceStatement.Actions = set.CreateStringSet(writeOnlyObjectActions...)
|
||||
@@ -163,8 +164,8 @@ func getWriteOnlyObjectStatement(bucketName, objectPrefix string) policyStatemen
|
||||
}
|
||||
|
||||
// Obtain statements for write only bucketPolicy.
|
||||
func getWriteOnlyStatement(bucketName, objectPrefix string) []policyStatement {
|
||||
statements := []policyStatement{}
|
||||
func getWriteOnlyStatement(bucketName, objectPrefix string) []policy.Statement {
|
||||
statements := []policy.Statement{}
|
||||
// Write only policy.
|
||||
// Save the write only policy.
|
||||
statements = append(statements, getWriteOnlyBucketStatement(bucketName, objectPrefix), getWriteOnlyBucketStatement(bucketName, objectPrefix))
|
||||
@@ -325,9 +326,10 @@ func TestIsValidPrincipals(t *testing.T) {
|
||||
{[]string{"*"}, nil, true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
err := isValidPrincipals(map[string]interface{}{
|
||||
"AWS": testCase.principals,
|
||||
})
|
||||
u := policy.User{
|
||||
AWS: set.CreateStringSet(testCase.principals...),
|
||||
}
|
||||
err := isValidPrincipals(u)
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
|
||||
}
|
||||
@@ -343,80 +345,86 @@ func TestIsValidPrincipals(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// getEmptyConditionKeyMap - returns a function that generates a
|
||||
// getEmptyConditionMap - returns a function that generates a
|
||||
// condition key map for a given key.
|
||||
func getEmptyConditionKeyMap(conditionKey string) func() map[string]map[string]set.StringSet {
|
||||
emptyConditonGenerator := func() map[string]map[string]set.StringSet {
|
||||
emptyMap := make(map[string]set.StringSet)
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
func getEmptyConditionMap(conditionKey string) func() policy.ConditionMap {
|
||||
emptyConditonGenerator := func() policy.ConditionMap {
|
||||
emptyMap := make(policy.ConditionKeyMap)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions[conditionKey] = emptyMap
|
||||
return conditions
|
||||
}
|
||||
return emptyConditonGenerator
|
||||
}
|
||||
|
||||
// Tests validate policyStatement condition validator.
|
||||
// Tests validate policy.Statement condition validator.
|
||||
func TestIsValidConditions(t *testing.T) {
|
||||
// returns empty conditions map.
|
||||
setEmptyConditions := func() map[string]map[string]set.StringSet {
|
||||
return make(map[string]map[string]set.StringSet)
|
||||
setEmptyConditions := func() policy.ConditionMap {
|
||||
return make(policy.ConditionMap)
|
||||
}
|
||||
|
||||
// returns map with the "StringEquals" set to empty map.
|
||||
setEmptyStringEquals := getEmptyConditionKeyMap("StringEquals")
|
||||
setEmptyStringEquals := getEmptyConditionMap("StringEquals")
|
||||
|
||||
// returns map with the "StringNotEquals" set to empty map.
|
||||
setEmptyStringNotEquals := getEmptyConditionKeyMap("StringNotEquals")
|
||||
setEmptyStringNotEquals := getEmptyConditionMap("StringNotEquals")
|
||||
|
||||
// returns map with the "StringLike" set to empty map.
|
||||
setEmptyStringLike := getEmptyConditionKeyMap("StringLike")
|
||||
setEmptyStringLike := getEmptyConditionMap("StringLike")
|
||||
|
||||
// returns map with the "StringNotLike" set to empty map.
|
||||
setEmptyStringNotLike := getEmptyConditionKeyMap("StringNotLike")
|
||||
setEmptyStringNotLike := getEmptyConditionMap("StringNotLike")
|
||||
|
||||
// returns map with the "IpAddress" set to empty map.
|
||||
setEmptyIPAddress := getEmptyConditionMap("IpAddress")
|
||||
|
||||
// returns map with "NotIpAddress" set to empty map.
|
||||
setEmptyNotIPAddress := getEmptyConditionMap("NotIpAddress")
|
||||
|
||||
// Generate conditions.
|
||||
generateConditions := func(key1, key2, value string) map[string]map[string]set.StringSet {
|
||||
innerMap := make(map[string]set.StringSet)
|
||||
generateConditions := func(key1, key2, value string) policy.ConditionMap {
|
||||
innerMap := make(policy.ConditionKeyMap)
|
||||
innerMap[key2] = set.CreateStringSet(value)
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions[key1] = innerMap
|
||||
return conditions
|
||||
}
|
||||
|
||||
// generate ambigious conditions.
|
||||
generateAmbigiousConditions := func() map[string]map[string]set.StringSet {
|
||||
prefixMap := make(map[string]set.StringSet)
|
||||
generateAmbigiousConditions := func() policy.ConditionMap {
|
||||
prefixMap := make(policy.ConditionKeyMap)
|
||||
prefixMap["s3:prefix"] = set.CreateStringSet("Asia/")
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions["StringEquals"] = prefixMap
|
||||
conditions["StringNotEquals"] = prefixMap
|
||||
return conditions
|
||||
}
|
||||
|
||||
// generate valid and non valid type in the condition map.
|
||||
generateValidInvalidConditions := func() map[string]map[string]set.StringSet {
|
||||
innerMap := make(map[string]set.StringSet)
|
||||
generateValidInvalidConditions := func() policy.ConditionMap {
|
||||
innerMap := make(policy.ConditionKeyMap)
|
||||
innerMap["s3:prefix"] = set.CreateStringSet("Asia/")
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions["StringEquals"] = innerMap
|
||||
conditions["InvalidType"] = innerMap
|
||||
return conditions
|
||||
}
|
||||
|
||||
// generate valid and invalid keys for valid types in the same condition map.
|
||||
generateValidInvalidConditionKeys := func() map[string]map[string]set.StringSet {
|
||||
innerMapValid := make(map[string]set.StringSet)
|
||||
generateValidInvalidConditionKeys := func() policy.ConditionMap {
|
||||
innerMapValid := make(policy.ConditionKeyMap)
|
||||
innerMapValid["s3:prefix"] = set.CreateStringSet("Asia/")
|
||||
innerMapInValid := make(map[string]set.StringSet)
|
||||
innerMapInValid["s3:invalid"] = set.CreateStringSet("Asia/")
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions["StringEquals"] = innerMapValid
|
||||
conditions["StringEquals"] = innerMapInValid
|
||||
return conditions
|
||||
}
|
||||
|
||||
// List of Conditions used for test cases.
|
||||
testConditions := []map[string]map[string]set.StringSet{
|
||||
testConditions := []policy.ConditionMap{
|
||||
generateConditions("StringValues", "s3:max-keys", "100"),
|
||||
generateConditions("StringEquals", "s3:Object", "100"),
|
||||
generateAmbigiousConditions(),
|
||||
@@ -427,6 +435,8 @@ func TestIsValidConditions(t *testing.T) {
|
||||
setEmptyStringNotEquals(),
|
||||
setEmptyStringLike(),
|
||||
setEmptyStringNotLike(),
|
||||
setEmptyIPAddress(),
|
||||
setEmptyNotIPAddress(),
|
||||
generateConditions("StringEquals", "s3:prefix", "Asia/"),
|
||||
generateConditions("StringEquals", "s3:max-keys", "100"),
|
||||
generateConditions("StringNotEquals", "s3:prefix", "Asia/"),
|
||||
@@ -439,7 +449,7 @@ func TestIsValidConditions(t *testing.T) {
|
||||
"please validate your policy document", "s3:max-keys", getObjectActionSet)
|
||||
testCases := []struct {
|
||||
inputActions set.StringSet
|
||||
inputCondition map[string]map[string]set.StringSet
|
||||
inputCondition policy.ConditionMap
|
||||
// expected result.
|
||||
expectedErr error
|
||||
// flag indicating whether test should pass.
|
||||
@@ -482,7 +492,13 @@ func TestIsValidConditions(t *testing.T) {
|
||||
// Test case - 12.
|
||||
{roBucketActionSet, testConditions[11], nil, true},
|
||||
// Test case - 13.
|
||||
{getObjectActionSet, testConditions[11], maxKeysConditionErr, false},
|
||||
{roBucketActionSet, testConditions[12], nil, true},
|
||||
// Test case - 11.
|
||||
{roBucketActionSet, testConditions[13], nil, true},
|
||||
// Test case - 12.
|
||||
{roBucketActionSet, testConditions[14], nil, true},
|
||||
// Test case - 13.
|
||||
{getObjectActionSet, testConditions[15], maxKeysConditionErr, false},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
actualErr := isValidConditions(testCase.inputActions, testCase.inputCondition)
|
||||
@@ -504,26 +520,26 @@ func TestIsValidConditions(t *testing.T) {
|
||||
// Tests validate Policy Action and Resource fields.
|
||||
func TestCheckbucketPolicyResources(t *testing.T) {
|
||||
// constructing policy statement without invalidPrefixActions (check bucket-policy-parser.go).
|
||||
setValidPrefixActions := func(statements []policyStatement) []policyStatement {
|
||||
setValidPrefixActions := func(statements []policy.Statement) []policy.Statement {
|
||||
statements[0].Actions = set.CreateStringSet([]string{"s3:DeleteObject", "s3:PutObject"}...)
|
||||
return statements
|
||||
}
|
||||
// contracting policy statement with recursive resources.
|
||||
// should result in ErrMalformedPolicy
|
||||
setRecurseResource := func(statements []policyStatement) []policyStatement {
|
||||
setRecurseResource := func(statements []policy.Statement) []policy.Statement {
|
||||
statements[0].Resources = set.CreateStringSet([]string{"arn:aws:s3:::minio-bucket/Asia/*", "arn:aws:s3:::minio-bucket/Asia/India/*"}...)
|
||||
return statements
|
||||
}
|
||||
|
||||
// constructing policy statement with lexically close characters.
|
||||
// should not result in ErrMalformedPolicy
|
||||
setResourceLexical := func(statements []policyStatement) []policyStatement {
|
||||
setResourceLexical := func(statements []policy.Statement) []policy.Statement {
|
||||
statements[0].Resources = set.CreateStringSet([]string{"arn:aws:s3:::minio-bucket/op*", "arn:aws:s3:::minio-bucket/oo*"}...)
|
||||
return statements
|
||||
}
|
||||
|
||||
// List of bucketPolicy used for tests.
|
||||
bucketAccessPolicies := []bucketPolicy{
|
||||
bucketAccessPolicies := []policy.BucketAccessPolicy{
|
||||
// bucketPolicy - 1.
|
||||
// Contains valid read only policy statement.
|
||||
{Version: "1.0", Statements: getReadOnlyStatement("minio-bucket", "")},
|
||||
@@ -554,7 +570,7 @@ func TestCheckbucketPolicyResources(t *testing.T) {
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
inputPolicy bucketPolicy
|
||||
inputPolicy policy.BucketAccessPolicy
|
||||
// expected results.
|
||||
apiErrCode APIErrorCode
|
||||
// Flag indicating whether the test should pass.
|
||||
@@ -585,7 +601,7 @@ func TestCheckbucketPolicyResources(t *testing.T) {
|
||||
{bucketAccessPolicies[6], ErrNone, true},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
apiErrCode := checkBucketPolicyResources("minio-bucket", &testCase.inputPolicy)
|
||||
apiErrCode := checkBucketPolicyResources("minio-bucket", testCase.inputPolicy)
|
||||
if apiErrCode != ErrNone && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass, but failed with Errocode %v", i+1, apiErrCode)
|
||||
}
|
||||
@@ -604,39 +620,39 @@ func TestCheckbucketPolicyResources(t *testing.T) {
|
||||
// Tests validate parsing of BucketAccessPolicy.
|
||||
func TestParseBucketPolicy(t *testing.T) {
|
||||
// set Unsupported Actions.
|
||||
setUnsupportedActions := func(statements []policyStatement) []policyStatement {
|
||||
setUnsupportedActions := func(statements []policy.Statement) []policy.Statement {
|
||||
// "s3:DeleteEverything"" is an Unsupported Action.
|
||||
statements[0].Actions = set.CreateStringSet([]string{"s3:GetObject", "s3:ListBucket", "s3:PutObject", "s3:DeleteEverything"}...)
|
||||
return statements
|
||||
}
|
||||
// set unsupported Effect.
|
||||
setUnsupportedEffect := func(statements []policyStatement) []policyStatement {
|
||||
setUnsupportedEffect := func(statements []policy.Statement) []policy.Statement {
|
||||
// Effect "Don't allow" is Unsupported.
|
||||
statements[0].Effect = "DontAllow"
|
||||
return statements
|
||||
}
|
||||
// set unsupported principals.
|
||||
setUnsupportedPrincipals := func(statements []policyStatement) []policyStatement {
|
||||
setUnsupportedPrincipals := func(statements []policy.Statement) []policy.Statement {
|
||||
// "User1111"" is an Unsupported Principal.
|
||||
statements[0].Principal = map[string]interface{}{
|
||||
"AWS": []string{"*", "User1111"},
|
||||
statements[0].Principal = policy.User{
|
||||
AWS: set.CreateStringSet([]string{"*", "User1111"}...),
|
||||
}
|
||||
return statements
|
||||
}
|
||||
// set unsupported Resources.
|
||||
setUnsupportedResources := func(statements []policyStatement) []policyStatement {
|
||||
setUnsupportedResources := func(statements []policy.Statement) []policy.Statement {
|
||||
// "s3:DeleteEverything"" is an Unsupported Action.
|
||||
statements[0].Resources = set.CreateStringSet([]string{"my-resource"}...)
|
||||
return statements
|
||||
}
|
||||
// List of bucketPolicy used for test cases.
|
||||
bucketAccesPolicies := []bucketPolicy{
|
||||
bucketAccesPolicies := []policy.BucketAccessPolicy{
|
||||
// bucketPolicy - 0.
|
||||
// bucketPolicy statement empty.
|
||||
{Version: "1.0"},
|
||||
// bucketPolicy - 1.
|
||||
// bucketPolicy version empty.
|
||||
{Version: "", Statements: []policyStatement{}},
|
||||
{Version: "", Statements: []policy.Statement{}},
|
||||
// bucketPolicy - 2.
|
||||
// Readonly bucketPolicy.
|
||||
{Version: "1.0", Statements: getReadOnlyStatement("minio-bucket", "")},
|
||||
@@ -661,19 +677,19 @@ func TestParseBucketPolicy(t *testing.T) {
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
inputPolicy bucketPolicy
|
||||
inputPolicy policy.BucketAccessPolicy
|
||||
// expected results.
|
||||
expectedPolicy bucketPolicy
|
||||
expectedPolicy policy.BucketAccessPolicy
|
||||
err error
|
||||
// Flag indicating whether the test should pass.
|
||||
shouldPass bool
|
||||
}{
|
||||
// Test case - 1.
|
||||
// bucketPolicy statement empty.
|
||||
{bucketAccesPolicies[0], bucketPolicy{}, errors.New("Policy statement cannot be empty"), false},
|
||||
{bucketAccesPolicies[0], policy.BucketAccessPolicy{}, errors.New("Policy statement cannot be empty"), false},
|
||||
// Test case - 2.
|
||||
// bucketPolicy version empty.
|
||||
{bucketAccesPolicies[1], bucketPolicy{}, errors.New("Policy version cannot be empty"), false},
|
||||
{bucketAccesPolicies[1], policy.BucketAccessPolicy{}, errors.New("Policy version cannot be empty"), false},
|
||||
// Test case - 3.
|
||||
// Readonly bucketPolicy.
|
||||
{bucketAccesPolicies[2], bucketAccesPolicies[2], nil, true},
|
||||
@@ -704,8 +720,8 @@ func TestParseBucketPolicy(t *testing.T) {
|
||||
t.Fatalf("Test %d: Couldn't Marshal bucket policy %s", i+1, err)
|
||||
}
|
||||
|
||||
var actualAccessPolicy = &bucketPolicy{}
|
||||
err = parseBucketPolicy(&buffer, actualAccessPolicy)
|
||||
var actualAccessPolicy = policy.BucketAccessPolicy{}
|
||||
err = parseBucketPolicy(&buffer, &actualAccessPolicy)
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, err.Error())
|
||||
}
|
||||
@@ -720,7 +736,7 @@ func TestParseBucketPolicy(t *testing.T) {
|
||||
}
|
||||
// Test passes as expected, but the output values are verified for correctness here.
|
||||
if err == nil && testCase.shouldPass {
|
||||
if testCase.expectedPolicy.String() != actualAccessPolicy.String() {
|
||||
if !reflect.DeepEqual(testCase.expectedPolicy, actualAccessPolicy) {
|
||||
t.Errorf("Test %d: The expected statements from resource statement generator doesn't match the actual statements", i+1)
|
||||
}
|
||||
}
|
||||
@@ -737,8 +753,8 @@ func TestAWSRefererCondition(t *testing.T) {
|
||||
set.CreateStringSet("www.example.com",
|
||||
"http://www.example.com"))
|
||||
|
||||
requestConditionKeyMap := make(map[string]set.StringSet)
|
||||
requestConditionKeyMap["referer"] = set.CreateStringSet("www.example.com")
|
||||
requestConditionMap := make(policy.ConditionKeyMap)
|
||||
requestConditionMap["referer"] = set.CreateStringSet("www.example.com")
|
||||
|
||||
testCases := []struct {
|
||||
effect string
|
||||
@@ -768,20 +784,82 @@ func TestAWSRefererCondition(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions[test.conditionKey] = conditionsKeyMap
|
||||
|
||||
allowStatement := policyStatement{
|
||||
allowStatement := policy.Statement{
|
||||
Sid: "Testing AWS referer condition",
|
||||
Effect: test.effect,
|
||||
Principal: map[string]interface{}{
|
||||
"AWS": "*",
|
||||
Principal: policy.User{
|
||||
AWS: set.CreateStringSet("*"),
|
||||
},
|
||||
Resources: resource,
|
||||
Conditions: conditions,
|
||||
}
|
||||
|
||||
if result := bucketPolicyConditionMatch(requestConditionKeyMap, allowStatement); result != test.match {
|
||||
if result := bucketPolicyConditionMatch(requestConditionMap, allowStatement); result != test.match {
|
||||
t.Errorf("Test %d - Expected conditons to evaluate to %v but got %v",
|
||||
i+1, test.match, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWSSourceIPCondition(t *testing.T) {
|
||||
resource := set.CreateStringSet([]string{
|
||||
fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/"+"Asia"+"*"),
|
||||
}...)
|
||||
|
||||
conditionsKeyMap := make(policy.ConditionKeyMap)
|
||||
// Test both IPv4 and IPv6 addresses.
|
||||
conditionsKeyMap.Add("aws:SourceIp",
|
||||
set.CreateStringSet("54.240.143.0/24",
|
||||
"2001:DB8:1234:5678::/64"))
|
||||
|
||||
requestConditionMap := make(policy.ConditionKeyMap)
|
||||
requestConditionMap["ip"] = set.CreateStringSet("54.240.143.2")
|
||||
|
||||
testCases := []struct {
|
||||
effect string
|
||||
conditionKey string
|
||||
match bool
|
||||
}{
|
||||
{
|
||||
effect: "Allow",
|
||||
conditionKey: "IpAddress",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
effect: "Allow",
|
||||
conditionKey: "NotIpAddress",
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
effect: "Deny",
|
||||
conditionKey: "IpAddress",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
effect: "Deny",
|
||||
conditionKey: "NotIpAddress",
|
||||
match: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions[test.conditionKey] = conditionsKeyMap
|
||||
|
||||
allowStatement := policy.Statement{
|
||||
Sid: "Testing AWS referer condition",
|
||||
Effect: test.effect,
|
||||
Principal: policy.User{
|
||||
AWS: set.CreateStringSet("*"),
|
||||
},
|
||||
Resources: resource,
|
||||
Conditions: conditions,
|
||||
}
|
||||
|
||||
if result := bucketPolicyConditionMatch(requestConditionMap, allowStatement); result != test.match {
|
||||
t.Errorf("Test %d - Expected conditons to evaluate to %v but got %v",
|
||||
i+1, test.match, result)
|
||||
}
|
||||
|
||||
+50
-30
@@ -20,7 +20,11 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -41,7 +45,7 @@ type bucketPolicies struct {
|
||||
rwMutex *sync.RWMutex
|
||||
|
||||
// Collection of 'bucket' policies.
|
||||
bucketPolicyConfigs map[string]*bucketPolicy
|
||||
bucketPolicyConfigs map[string]policy.BucketAccessPolicy
|
||||
}
|
||||
|
||||
// Represent a policy change
|
||||
@@ -51,11 +55,11 @@ type policyChange struct {
|
||||
IsRemove bool
|
||||
|
||||
// represents the new policy for the bucket
|
||||
BktPolicy *bucketPolicy
|
||||
BktPolicy policy.BucketAccessPolicy
|
||||
}
|
||||
|
||||
// Fetch bucket policy for a given bucket.
|
||||
func (bp bucketPolicies) GetBucketPolicy(bucket string) *bucketPolicy {
|
||||
func (bp bucketPolicies) GetBucketPolicy(bucket string) policy.BucketAccessPolicy {
|
||||
bp.rwMutex.RLock()
|
||||
defer bp.rwMutex.RUnlock()
|
||||
return bp.bucketPolicyConfigs[bucket]
|
||||
@@ -70,7 +74,7 @@ func (bp *bucketPolicies) SetBucketPolicy(bucket string, pCh policyChange) error
|
||||
if pCh.IsRemove {
|
||||
delete(bp.bucketPolicyConfigs, bucket)
|
||||
} else {
|
||||
if pCh.BktPolicy == nil {
|
||||
if reflect.DeepEqual(pCh.BktPolicy, emptyBucketPolicy) {
|
||||
return errInvalidArgument
|
||||
}
|
||||
bp.bucketPolicyConfigs[bucket] = pCh.BktPolicy
|
||||
@@ -79,19 +83,19 @@ func (bp *bucketPolicies) SetBucketPolicy(bucket string, pCh policyChange) error
|
||||
}
|
||||
|
||||
// Loads all bucket policies from persistent layer.
|
||||
func loadAllBucketPolicies(objAPI ObjectLayer) (policies map[string]*bucketPolicy, err error) {
|
||||
func loadAllBucketPolicies(objAPI ObjectLayer) (policies map[string]policy.BucketAccessPolicy, err error) {
|
||||
// List buckets to proceed loading all notification configuration.
|
||||
buckets, err := objAPI.ListBuckets()
|
||||
errorIf(err, "Unable to list buckets.")
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to list buckets.")
|
||||
return nil, errorCause(err)
|
||||
}
|
||||
|
||||
policies = make(map[string]*bucketPolicy)
|
||||
policies = make(map[string]policy.BucketAccessPolicy)
|
||||
var pErrs []error
|
||||
// Loads bucket policy.
|
||||
for _, bucket := range buckets {
|
||||
policy, pErr := readBucketPolicy(bucket.Name, objAPI)
|
||||
bp, pErr := readBucketPolicy(bucket.Name, objAPI)
|
||||
if pErr != nil {
|
||||
// net.Dial fails for rpc client or any
|
||||
// other unexpected errors during net.Dial.
|
||||
@@ -103,7 +107,7 @@ func loadAllBucketPolicies(objAPI ObjectLayer) (policies map[string]*bucketPolic
|
||||
// Continue to load other bucket policies if possible.
|
||||
continue
|
||||
}
|
||||
policies[bucket.Name] = policy
|
||||
policies[bucket.Name] = bp
|
||||
}
|
||||
|
||||
// Look for any errors occurred while reading bucket policies.
|
||||
@@ -146,7 +150,9 @@ func readBucketPolicyJSON(bucket string, objAPI ObjectLayer) (bucketPolicyReader
|
||||
|
||||
// Acquire a read lock on policy config before reading.
|
||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, policyPath)
|
||||
objLock.RLock()
|
||||
if err = objLock.GetRLock(globalOperationTimeout); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer objLock.RUnlock()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
@@ -164,21 +170,20 @@ func readBucketPolicyJSON(bucket string, objAPI ObjectLayer) (bucketPolicyReader
|
||||
|
||||
// readBucketPolicy - reads bucket policy for an input bucket, returns BucketPolicyNotFound
|
||||
// if bucket policy is not found. This function also parses the bucket policy into an object.
|
||||
func readBucketPolicy(bucket string, objAPI ObjectLayer) (*bucketPolicy, error) {
|
||||
func readBucketPolicy(bucket string, objAPI ObjectLayer) (policy.BucketAccessPolicy, error) {
|
||||
// Read bucket policy JSON.
|
||||
bucketPolicyReader, err := readBucketPolicyJSON(bucket, objAPI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return emptyBucketPolicy, err
|
||||
}
|
||||
|
||||
// Parse the saved policy.
|
||||
var policy = &bucketPolicy{}
|
||||
err = parseBucketPolicy(bucketPolicyReader, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var bp policy.BucketAccessPolicy
|
||||
if err = parseBucketPolicy(bucketPolicyReader, &bp); err != nil {
|
||||
return emptyBucketPolicy, err
|
||||
|
||||
}
|
||||
return policy, nil
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
// removeBucketPolicy - removes any previously written bucket policy. Returns BucketPolicyNotFound
|
||||
@@ -187,9 +192,12 @@ func removeBucketPolicy(bucket string, objAPI ObjectLayer) error {
|
||||
policyPath := pathJoin(bucketConfigPrefix, bucket, bucketPolicyConfig)
|
||||
// Acquire a write lock on policy config before modifying.
|
||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, policyPath)
|
||||
objLock.Lock()
|
||||
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer objLock.Unlock()
|
||||
if err := objAPI.DeleteObject(minioMetaBucket, policyPath); err != nil {
|
||||
err := objAPI.DeleteObject(minioMetaBucket, policyPath)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to remove bucket-policy on bucket %s.", bucket)
|
||||
err = errorCause(err)
|
||||
if _, ok := err.(ObjectNotFound); ok {
|
||||
@@ -201,18 +209,27 @@ func removeBucketPolicy(bucket string, objAPI ObjectLayer) error {
|
||||
}
|
||||
|
||||
// writeBucketPolicy - save a bucket policy that is assumed to be validated.
|
||||
func writeBucketPolicy(bucket string, objAPI ObjectLayer, bpy *bucketPolicy) error {
|
||||
func writeBucketPolicy(bucket string, objAPI ObjectLayer, bpy policy.BucketAccessPolicy) error {
|
||||
buf, err := json.Marshal(bpy)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to marshal bucket policy '%v' to JSON", *bpy)
|
||||
errorIf(err, "Unable to marshal bucket policy '%#v' to JSON", bpy)
|
||||
return err
|
||||
}
|
||||
policyPath := pathJoin(bucketConfigPrefix, bucket, bucketPolicyConfig)
|
||||
// Acquire a write lock on policy config before modifying.
|
||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, policyPath)
|
||||
objLock.Lock()
|
||||
if err = objLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer objLock.Unlock()
|
||||
if _, err := objAPI.PutObject(minioMetaBucket, policyPath, int64(len(buf)), bytes.NewReader(buf), nil, ""); err != nil {
|
||||
|
||||
hashReader, err := hash.NewReader(bytes.NewReader(buf), int64(len(buf)), "", getSHA256Hash(buf))
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to set policy for the bucket %s", bucket)
|
||||
return errorCause(err)
|
||||
}
|
||||
|
||||
if _, err = objAPI.PutObject(minioMetaBucket, policyPath, hashReader, nil); err != nil {
|
||||
errorIf(err, "Unable to set policy for the bucket %s", bucket)
|
||||
return errorCause(err)
|
||||
}
|
||||
@@ -221,26 +238,28 @@ func writeBucketPolicy(bucket string, objAPI ObjectLayer, bpy *bucketPolicy) err
|
||||
|
||||
func parseAndPersistBucketPolicy(bucket string, policyBytes []byte, objAPI ObjectLayer) APIErrorCode {
|
||||
// Parse bucket policy.
|
||||
var policy = &bucketPolicy{}
|
||||
err := parseBucketPolicy(bytes.NewReader(policyBytes), policy)
|
||||
var bktPolicy policy.BucketAccessPolicy
|
||||
err := parseBucketPolicy(bytes.NewReader(policyBytes), &bktPolicy)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to parse bucket policy.")
|
||||
return ErrInvalidPolicyDocument
|
||||
}
|
||||
|
||||
// Parse check bucket policy.
|
||||
if s3Error := checkBucketPolicyResources(bucket, policy); s3Error != ErrNone {
|
||||
if s3Error := checkBucketPolicyResources(bucket, bktPolicy); s3Error != ErrNone {
|
||||
return s3Error
|
||||
}
|
||||
|
||||
// Acquire a write lock on bucket before modifying its configuration.
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.Lock()
|
||||
if bucketLock.GetLock(globalOperationTimeout) != nil {
|
||||
return ErrOperationTimedOut
|
||||
}
|
||||
// Release lock after notifying peers
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
// Save bucket policy.
|
||||
if err = persistAndNotifyBucketPolicyChange(bucket, policyChange{false, policy}, objAPI); err != nil {
|
||||
if err = persistAndNotifyBucketPolicyChange(bucket, policyChange{false, bktPolicy}, objAPI); err != nil {
|
||||
switch err.(type) {
|
||||
case BucketNameInvalid:
|
||||
return ErrInvalidBucketName
|
||||
@@ -259,11 +278,12 @@ func parseAndPersistBucketPolicy(bucket string, policyBytes []byte, objAPI Objec
|
||||
// change. In-memory state is updated in response to the notification.
|
||||
func persistAndNotifyBucketPolicyChange(bucket string, pCh policyChange, objAPI ObjectLayer) error {
|
||||
if pCh.IsRemove {
|
||||
if err := removeBucketPolicy(bucket, objAPI); err != nil {
|
||||
err := removeBucketPolicy(bucket, objAPI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if pCh.BktPolicy == nil {
|
||||
if reflect.DeepEqual(pCh.BktPolicy, emptyBucketPolicy) {
|
||||
return errInvalidArgument
|
||||
}
|
||||
if err := writeBucketPolicy(bucket, objAPI, pCh.BktPolicy); err != nil {
|
||||
|
||||
+31
-24
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
@@ -24,35 +25,34 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func parsePublicCertFile(certFile string) (certs []*x509.Certificate, err error) {
|
||||
var bytes []byte
|
||||
|
||||
if bytes, err = ioutil.ReadFile(certFile); err != nil {
|
||||
return certs, err
|
||||
func parsePublicCertFile(certFile string) (x509Certs []*x509.Certificate, err error) {
|
||||
// Read certificate file.
|
||||
var data []byte
|
||||
if data, err = ioutil.ReadFile(certFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse all certs in the chain.
|
||||
var block *pem.Block
|
||||
var cert *x509.Certificate
|
||||
current := bytes
|
||||
current := data
|
||||
for len(current) > 0 {
|
||||
if block, current = pem.Decode(current); block == nil {
|
||||
err = fmt.Errorf("Could not read PEM block from file %s", certFile)
|
||||
return certs, err
|
||||
var pemBlock *pem.Block
|
||||
if pemBlock, current = pem.Decode(current); pemBlock == nil {
|
||||
return nil, fmt.Errorf("Could not read PEM block from file %s", certFile)
|
||||
}
|
||||
|
||||
if cert, err = x509.ParseCertificate(block.Bytes); err != nil {
|
||||
return certs, err
|
||||
var x509Cert *x509.Certificate
|
||||
if x509Cert, err = x509.ParseCertificate(pemBlock.Bytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certs = append(certs, cert)
|
||||
x509Certs = append(x509Certs, x509Cert)
|
||||
}
|
||||
|
||||
if len(certs) == 0 {
|
||||
err = fmt.Errorf("Empty public certificate file %s", certFile)
|
||||
if len(x509Certs) == 0 {
|
||||
return nil, fmt.Errorf("Empty public certificate file %s", certFile)
|
||||
}
|
||||
|
||||
return certs, err
|
||||
return x509Certs, nil
|
||||
}
|
||||
|
||||
func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
|
||||
@@ -81,7 +81,7 @@ func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
|
||||
for _, caFile := range caFiles {
|
||||
caCert, err := ioutil.ReadFile(caFile)
|
||||
if err != nil {
|
||||
return rootCAs, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootCAs.AppendCertsFromPEM(caCert)
|
||||
@@ -90,19 +90,26 @@ func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
|
||||
return rootCAs, nil
|
||||
}
|
||||
|
||||
func getSSLConfig() (publicCerts []*x509.Certificate, rootCAs *x509.CertPool, secureConn bool, err error) {
|
||||
func getSSLConfig() (x509Certs []*x509.Certificate, rootCAs *x509.CertPool, tlsCert *tls.Certificate, secureConn bool, err error) {
|
||||
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
|
||||
return publicCerts, rootCAs, secureConn, err
|
||||
return nil, nil, nil, false, nil
|
||||
}
|
||||
|
||||
if publicCerts, err = parsePublicCertFile(getPublicCertFile()); err != nil {
|
||||
return publicCerts, rootCAs, secureConn, err
|
||||
if x509Certs, err = parsePublicCertFile(getPublicCertFile()); err != nil {
|
||||
return nil, nil, nil, false, err
|
||||
}
|
||||
|
||||
var cert tls.Certificate
|
||||
if cert, err = tls.LoadX509KeyPair(getPublicCertFile(), getPrivateKeyFile()); err != nil {
|
||||
return nil, nil, nil, false, err
|
||||
}
|
||||
|
||||
tlsCert = &cert
|
||||
|
||||
if rootCAs, err = getRootCAs(getCADir()); err != nil {
|
||||
return publicCerts, rootCAs, secureConn, err
|
||||
return nil, nil, nil, false, err
|
||||
}
|
||||
|
||||
secureConn = true
|
||||
return publicCerts, rootCAs, secureConn, err
|
||||
return x509Certs, rootCAs, tlsCert, secureConn, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Check for updates and print a notification message
|
||||
func checkUpdate(mode string) {
|
||||
// Its OK to ignore any errors during getUpdateInfo() here.
|
||||
if older, downloadURL, err := getUpdateInfo(1*time.Second, mode); err == nil {
|
||||
if updateMsg := computeUpdateMessage(downloadURL, older); updateMsg != "" {
|
||||
log.Println(updateMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func enableLoggers() {
|
||||
fileLogTarget := serverConfig.Logger.GetFile()
|
||||
if fileLogTarget.Enable {
|
||||
err := InitFileLogger(&fileLogTarget)
|
||||
fatalIf(err, "Unable to initialize file logger")
|
||||
log.AddTarget(fileLogTarget)
|
||||
}
|
||||
|
||||
consoleLogTarget := serverConfig.Logger.GetConsole()
|
||||
if consoleLogTarget.Enable {
|
||||
InitConsoleLogger(&consoleLogTarget)
|
||||
}
|
||||
|
||||
log.SetConsoleTarget(consoleLogTarget)
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
// Config file does not exist, we create it fresh and return upon success.
|
||||
if isFile(getConfigFile()) {
|
||||
fatalIf(migrateConfig(), "Config migration failed.")
|
||||
fatalIf(loadConfig(), "Unable to load config version: '%s'.", v20)
|
||||
} else {
|
||||
fatalIf(newConfig(), "Unable to initialize minio config for the first time.")
|
||||
log.Println("Created minio configuration file successfully at " + getConfigDir())
|
||||
}
|
||||
}
|
||||
|
||||
func handleCommonCmdArgs(ctx *cli.Context) {
|
||||
// Set configuration directory.
|
||||
{
|
||||
// Get configuration directory from command line argument.
|
||||
configDir := ctx.String("config-dir")
|
||||
if !ctx.IsSet("config-dir") && ctx.GlobalIsSet("config-dir") {
|
||||
configDir = ctx.GlobalString("config-dir")
|
||||
}
|
||||
if configDir == "" {
|
||||
fatalIf(errors.New("empty directory"), "Configuration directory cannot be empty.")
|
||||
}
|
||||
|
||||
// Disallow relative paths, figure out absolute paths.
|
||||
configDirAbs, err := filepath.Abs(configDir)
|
||||
fatalIf(err, "Unable to fetch absolute path for config directory %s", configDir)
|
||||
|
||||
setConfigDir(configDirAbs)
|
||||
}
|
||||
}
|
||||
|
||||
func handleCommonEnvVars() {
|
||||
// Start profiler if env is set.
|
||||
if profiler := os.Getenv("_MINIO_PROFILER"); profiler != "" {
|
||||
globalProfiler = startProfiler(profiler)
|
||||
}
|
||||
|
||||
// Check if object cache is disabled.
|
||||
globalXLObjCacheDisabled = strings.EqualFold(os.Getenv("_MINIO_CACHE"), "off")
|
||||
|
||||
accessKey := os.Getenv("MINIO_ACCESS_KEY")
|
||||
secretKey := os.Getenv("MINIO_SECRET_KEY")
|
||||
if accessKey != "" && secretKey != "" {
|
||||
cred, err := auth.CreateCredentials(accessKey, secretKey)
|
||||
fatalIf(err, "Invalid access/secret Key set in environment.")
|
||||
|
||||
// credential Envs are set globally.
|
||||
globalIsEnvCreds = true
|
||||
globalActiveCred = cred
|
||||
}
|
||||
|
||||
if browser := os.Getenv("MINIO_BROWSER"); browser != "" {
|
||||
browserFlag, err := ParseBrowserFlag(browser)
|
||||
if err != nil {
|
||||
fatalIf(errors.New("invalid value"), "Unknown value ‘%s’ in MINIO_BROWSER environment variable.", browser)
|
||||
}
|
||||
|
||||
// browser Envs are set globally, this does not represent
|
||||
// if browser is turned off or on.
|
||||
globalIsEnvBrowser = true
|
||||
globalIsBrowserEnabled = bool(browserFlag)
|
||||
}
|
||||
|
||||
globalHTTPTrace = os.Getenv("MINIO_HTTP_TRACE") != ""
|
||||
|
||||
globalDomainName = os.Getenv("MINIO_DOMAIN")
|
||||
if globalDomainName != "" {
|
||||
globalIsEnvDomainName = true
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
@@ -76,7 +77,7 @@ func (config *ConfigDir) GetCADir() string {
|
||||
|
||||
// Create - creates configuration directory tree.
|
||||
func (config *ConfigDir) Create() error {
|
||||
return mkdirAll(config.GetCADir(), 0700)
|
||||
return os.MkdirAll(config.GetCADir(), 0700)
|
||||
}
|
||||
|
||||
// GetMinioConfigFile - returns absolute path of config.json file.
|
||||
|
||||
+229
-3
@@ -21,6 +21,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
@@ -141,7 +142,18 @@ func migrateConfig() error {
|
||||
return err
|
||||
}
|
||||
fallthrough
|
||||
case v18:
|
||||
case "18":
|
||||
// Migrate version '17' to '18'.
|
||||
if err = migrateV18ToV19(); err != nil {
|
||||
return err
|
||||
}
|
||||
fallthrough
|
||||
case "19":
|
||||
if err = migrateV19ToV20(); err != nil {
|
||||
return err
|
||||
}
|
||||
fallthrough
|
||||
case "20":
|
||||
// No migration needed. this always points to current version.
|
||||
err = nil
|
||||
}
|
||||
@@ -163,7 +175,7 @@ func purgeV1() error {
|
||||
return fmt.Errorf("unrecognized config version ‘%s’", cv1.Version)
|
||||
}
|
||||
|
||||
removeAll(configFile)
|
||||
os.RemoveAll(configFile)
|
||||
log.Println("Removed unsupported config version ‘1’.")
|
||||
return nil
|
||||
}
|
||||
@@ -184,7 +196,7 @@ func migrateV2ToV3() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
cred, err := createCredential(cv2.Credentials.AccessKey, cv2.Credentials.SecretKey)
|
||||
cred, err := auth.CreateCredentials(cv2.Credentials.AccessKey, cv2.Credentials.SecretKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid credential in V2 configuration file. %v", err)
|
||||
}
|
||||
@@ -1366,3 +1378,217 @@ func migrateV17ToV18() error {
|
||||
log.Printf(configMigrateMSGTemplate, configFile, cv17.Version, srvConfig.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateV18ToV19() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv18 := &serverConfigV18{}
|
||||
_, err := quick.Load(configFile, cv18)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("Unable to load config version ‘18’. %v", err)
|
||||
}
|
||||
if cv18.Version != "18" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy over fields from V18 into V19 config struct
|
||||
srvConfig := &serverConfigV18{
|
||||
Logger: &loggers{},
|
||||
Notify: ¬ifier{},
|
||||
}
|
||||
srvConfig.Version = "19"
|
||||
srvConfig.Credential = cv18.Credential
|
||||
srvConfig.Region = cv18.Region
|
||||
if srvConfig.Region == "" {
|
||||
// Region needs to be set for AWS Signature Version 4.
|
||||
srvConfig.Region = globalMinioDefaultRegion
|
||||
}
|
||||
|
||||
srvConfig.Logger.Console = cv18.Logger.Console
|
||||
srvConfig.Logger.File = cv18.Logger.File
|
||||
|
||||
// check and set notifiers config
|
||||
if len(cv18.Notify.AMQP) == 0 {
|
||||
srvConfig.Notify.AMQP = make(map[string]amqpNotify)
|
||||
srvConfig.Notify.AMQP["1"] = amqpNotify{}
|
||||
} else {
|
||||
// New deliveryMode parameter is added for AMQP,
|
||||
// default value is already 0, so nothing to
|
||||
// explicitly migrate here.
|
||||
srvConfig.Notify.AMQP = cv18.Notify.AMQP
|
||||
}
|
||||
if len(cv18.Notify.ElasticSearch) == 0 {
|
||||
srvConfig.Notify.ElasticSearch = make(map[string]elasticSearchNotify)
|
||||
srvConfig.Notify.ElasticSearch["1"] = elasticSearchNotify{
|
||||
Format: formatNamespace,
|
||||
}
|
||||
} else {
|
||||
srvConfig.Notify.ElasticSearch = cv18.Notify.ElasticSearch
|
||||
}
|
||||
if len(cv18.Notify.Redis) == 0 {
|
||||
srvConfig.Notify.Redis = make(map[string]redisNotify)
|
||||
srvConfig.Notify.Redis["1"] = redisNotify{
|
||||
Format: formatNamespace,
|
||||
}
|
||||
} else {
|
||||
srvConfig.Notify.Redis = cv18.Notify.Redis
|
||||
}
|
||||
if len(cv18.Notify.PostgreSQL) == 0 {
|
||||
srvConfig.Notify.PostgreSQL = make(map[string]postgreSQLNotify)
|
||||
srvConfig.Notify.PostgreSQL["1"] = postgreSQLNotify{
|
||||
Format: formatNamespace,
|
||||
}
|
||||
} else {
|
||||
srvConfig.Notify.PostgreSQL = cv18.Notify.PostgreSQL
|
||||
}
|
||||
if len(cv18.Notify.Kafka) == 0 {
|
||||
srvConfig.Notify.Kafka = make(map[string]kafkaNotify)
|
||||
srvConfig.Notify.Kafka["1"] = kafkaNotify{}
|
||||
} else {
|
||||
srvConfig.Notify.Kafka = cv18.Notify.Kafka
|
||||
}
|
||||
if len(cv18.Notify.NATS) == 0 {
|
||||
srvConfig.Notify.NATS = make(map[string]natsNotify)
|
||||
srvConfig.Notify.NATS["1"] = natsNotify{}
|
||||
} else {
|
||||
srvConfig.Notify.NATS = cv18.Notify.NATS
|
||||
}
|
||||
if len(cv18.Notify.Webhook) == 0 {
|
||||
srvConfig.Notify.Webhook = make(map[string]webhookNotify)
|
||||
srvConfig.Notify.Webhook["1"] = webhookNotify{}
|
||||
} else {
|
||||
srvConfig.Notify.Webhook = cv18.Notify.Webhook
|
||||
}
|
||||
if len(cv18.Notify.MySQL) == 0 {
|
||||
srvConfig.Notify.MySQL = make(map[string]mySQLNotify)
|
||||
srvConfig.Notify.MySQL["1"] = mySQLNotify{
|
||||
Format: formatNamespace,
|
||||
}
|
||||
} else {
|
||||
srvConfig.Notify.MySQL = cv18.Notify.MySQL
|
||||
}
|
||||
|
||||
// V18 will not have mqtt support, so we add that here.
|
||||
srvConfig.Notify.MQTT = make(map[string]mqttNotify)
|
||||
srvConfig.Notify.MQTT["1"] = mqttNotify{}
|
||||
|
||||
// Load browser config from existing config in the file.
|
||||
srvConfig.Browser = cv18.Browser
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv18.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
log.Printf(configMigrateMSGTemplate, configFile, cv18.Version, srvConfig.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateV19ToV20() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv19 := &serverConfigV19{}
|
||||
_, err := quick.Load(configFile, cv19)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("Unable to load config version ‘18’. %v", err)
|
||||
}
|
||||
if cv19.Version != "19" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy over fields from V19 into V20 config struct
|
||||
srvConfig := &serverConfigV20{
|
||||
Logger: &loggers{},
|
||||
Notify: ¬ifier{},
|
||||
}
|
||||
srvConfig.Version = "20"
|
||||
srvConfig.Credential = cv19.Credential
|
||||
srvConfig.Region = cv19.Region
|
||||
if srvConfig.Region == "" {
|
||||
// Region needs to be set for AWS Signature Version 4.
|
||||
srvConfig.Region = globalMinioDefaultRegion
|
||||
}
|
||||
|
||||
srvConfig.Logger.Console = cv19.Logger.Console
|
||||
srvConfig.Logger.File = cv19.Logger.File
|
||||
|
||||
// check and set notifiers config
|
||||
if len(cv19.Notify.AMQP) == 0 {
|
||||
srvConfig.Notify.AMQP = make(map[string]amqpNotify)
|
||||
srvConfig.Notify.AMQP["1"] = amqpNotify{}
|
||||
} else {
|
||||
// New deliveryMode parameter is added for AMQP,
|
||||
// default value is already 0, so nothing to
|
||||
// explicitly migrate here.
|
||||
srvConfig.Notify.AMQP = cv19.Notify.AMQP
|
||||
}
|
||||
if len(cv19.Notify.ElasticSearch) == 0 {
|
||||
srvConfig.Notify.ElasticSearch = make(map[string]elasticSearchNotify)
|
||||
srvConfig.Notify.ElasticSearch["1"] = elasticSearchNotify{
|
||||
Format: formatNamespace,
|
||||
}
|
||||
} else {
|
||||
srvConfig.Notify.ElasticSearch = cv19.Notify.ElasticSearch
|
||||
}
|
||||
if len(cv19.Notify.Redis) == 0 {
|
||||
srvConfig.Notify.Redis = make(map[string]redisNotify)
|
||||
srvConfig.Notify.Redis["1"] = redisNotify{
|
||||
Format: formatNamespace,
|
||||
}
|
||||
} else {
|
||||
srvConfig.Notify.Redis = cv19.Notify.Redis
|
||||
}
|
||||
if len(cv19.Notify.PostgreSQL) == 0 {
|
||||
srvConfig.Notify.PostgreSQL = make(map[string]postgreSQLNotify)
|
||||
srvConfig.Notify.PostgreSQL["1"] = postgreSQLNotify{
|
||||
Format: formatNamespace,
|
||||
}
|
||||
} else {
|
||||
srvConfig.Notify.PostgreSQL = cv19.Notify.PostgreSQL
|
||||
}
|
||||
if len(cv19.Notify.Kafka) == 0 {
|
||||
srvConfig.Notify.Kafka = make(map[string]kafkaNotify)
|
||||
srvConfig.Notify.Kafka["1"] = kafkaNotify{}
|
||||
} else {
|
||||
srvConfig.Notify.Kafka = cv19.Notify.Kafka
|
||||
}
|
||||
if len(cv19.Notify.NATS) == 0 {
|
||||
srvConfig.Notify.NATS = make(map[string]natsNotify)
|
||||
srvConfig.Notify.NATS["1"] = natsNotify{}
|
||||
} else {
|
||||
srvConfig.Notify.NATS = cv19.Notify.NATS
|
||||
}
|
||||
if len(cv19.Notify.Webhook) == 0 {
|
||||
srvConfig.Notify.Webhook = make(map[string]webhookNotify)
|
||||
srvConfig.Notify.Webhook["1"] = webhookNotify{}
|
||||
} else {
|
||||
srvConfig.Notify.Webhook = cv19.Notify.Webhook
|
||||
}
|
||||
if len(cv19.Notify.MySQL) == 0 {
|
||||
srvConfig.Notify.MySQL = make(map[string]mySQLNotify)
|
||||
srvConfig.Notify.MySQL["1"] = mySQLNotify{
|
||||
Format: formatNamespace,
|
||||
}
|
||||
} else {
|
||||
srvConfig.Notify.MySQL = cv19.Notify.MySQL
|
||||
}
|
||||
if len(cv19.Notify.MQTT) == 0 {
|
||||
srvConfig.Notify.MQTT = make(map[string]mqttNotify)
|
||||
srvConfig.Notify.MQTT["1"] = mqttNotify{}
|
||||
} else {
|
||||
srvConfig.Notify.MQTT = cv19.Notify.MQTT
|
||||
}
|
||||
|
||||
// Load browser config from existing config in the file.
|
||||
srvConfig.Browser = cv19.Browser
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv19.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
log.Printf(configMigrateMSGTemplate, configFile, cv19.Version, srvConfig.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestServerConfigMigrateV1(t *testing.T) {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
setConfigDir(rootPath)
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestServerConfigMigrateV1(t *testing.T) {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
// Check if config v1 is removed from filesystem
|
||||
if _, err := osStat(configPath); err == nil || !os.IsNotExist(err) {
|
||||
if _, err := os.Stat(configPath); err == nil || !os.IsNotExist(err) {
|
||||
t.Fatal("Config V1 file is not purged")
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
setConfigDir(rootPath)
|
||||
configPath := rootPath + "/" + minioConfigFile
|
||||
@@ -122,17 +122,20 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
||||
if err := migrateV17ToV18(); err != nil {
|
||||
t.Fatal("migrate v17 to v18 should succeed when no config file is found")
|
||||
}
|
||||
if err := migrateV18ToV19(); err != nil {
|
||||
t.Fatal("migrate v18 to v19 should succeed when no config file is found")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test if a config migration from v2 to v18 is successfully done
|
||||
func TestServerConfigMigrateV2toV18(t *testing.T) {
|
||||
// Test if a config migration from v2 to v19 is successfully done
|
||||
func TestServerConfigMigrateV2toV19(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
setConfigDir(rootPath)
|
||||
configPath := rootPath + "/" + minioConfigFile
|
||||
@@ -166,7 +169,7 @@ func TestServerConfigMigrateV2toV18(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check the version number in the upgraded config file
|
||||
expectedVersion := v18
|
||||
expectedVersion := v20
|
||||
if serverConfig.Version != expectedVersion {
|
||||
t.Fatalf("Expect version "+expectedVersion+", found: %v", serverConfig.Version)
|
||||
}
|
||||
@@ -187,7 +190,7 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
setConfigDir(rootPath)
|
||||
configPath := rootPath + "/" + minioConfigFile
|
||||
@@ -246,6 +249,9 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
||||
if err := migrateV17ToV18(); err == nil {
|
||||
t.Fatal("migrateConfigV17ToV18() should fail with a corrupted json")
|
||||
}
|
||||
if err := migrateV18ToV19(); err == nil {
|
||||
t.Fatal("migrateConfigV18ToV19() should fail with a corrupted json")
|
||||
}
|
||||
}
|
||||
|
||||
// Test if all migrate code returns error with corrupted config files
|
||||
@@ -255,7 +261,7 @@ func TestServerConfigMigrateCorruptedConfig(t *testing.T) {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
setConfigDir(rootPath)
|
||||
configPath := rootPath + "/" + minioConfigFile
|
||||
|
||||
+76
-35
@@ -16,7 +16,11 @@
|
||||
|
||||
package cmd
|
||||
|
||||
import "sync"
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
/////////////////// Config V1 ///////////////////
|
||||
type configV1 struct {
|
||||
@@ -92,8 +96,8 @@ type configV3 struct {
|
||||
Addr string `json:"address"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger loggerV3 `json:"logger"`
|
||||
@@ -122,8 +126,8 @@ type configV4 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger loggerV4 `json:"logger"`
|
||||
@@ -179,8 +183,8 @@ type configV5 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger loggerV5 `json:"logger"`
|
||||
@@ -209,8 +213,8 @@ type configV6 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger loggerV6 `json:"logger"`
|
||||
@@ -246,8 +250,8 @@ type serverConfigV7 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger loggerV6 `json:"logger"`
|
||||
@@ -265,8 +269,8 @@ type serverConfigV8 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger loggerV6 `json:"logger"`
|
||||
@@ -284,8 +288,8 @@ type serverConfigV9 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger loggerV6 `json:"logger"`
|
||||
@@ -310,8 +314,8 @@ type serverConfigV10 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger loggerV7 `json:"logger"`
|
||||
@@ -338,8 +342,8 @@ type serverConfigV11 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger loggerV7 `json:"logger"`
|
||||
@@ -354,8 +358,8 @@ type serverConfigV12 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger loggerV7 `json:"logger"`
|
||||
@@ -370,8 +374,8 @@ type serverConfigV13 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger *loggerV7 `json:"logger"`
|
||||
@@ -386,9 +390,9 @@ type serverConfigV14 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Browser BrowserFlag `json:"browser"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Browser BrowserFlag `json:"browser"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger *loggerV7 `json:"logger"`
|
||||
@@ -403,9 +407,9 @@ type serverConfigV15 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Browser BrowserFlag `json:"browser"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Browser BrowserFlag `json:"browser"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger *loggerV7 `json:"logger"`
|
||||
@@ -420,9 +424,9 @@ type serverConfigV16 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Browser BrowserFlag `json:"browser"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Browser BrowserFlag `json:"browser"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger *loggers `json:"logger"`
|
||||
@@ -439,9 +443,46 @@ type serverConfigV17 struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Browser BrowserFlag `json:"browser"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Browser BrowserFlag `json:"browser"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger *loggers `json:"logger"`
|
||||
|
||||
// Notification queue configuration.
|
||||
Notify *notifier `json:"notify"`
|
||||
}
|
||||
|
||||
// serverConfigV18 server configuration version '18' which is like
|
||||
// version '17' except it adds support for "deliveryMode" parameter in
|
||||
// the AMQP notification target.
|
||||
type serverConfigV18 struct {
|
||||
sync.RWMutex
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Browser BrowserFlag `json:"browser"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger *loggers `json:"logger"`
|
||||
|
||||
// Notification queue configuration.
|
||||
Notify *notifier `json:"notify"`
|
||||
}
|
||||
|
||||
// serverConfigV19 server configuration version '19' which is like
|
||||
// version '18' except it adds support for MQTT notifications.
|
||||
type serverConfigV19 struct {
|
||||
sync.RWMutex
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Browser BrowserFlag `json:"browser"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger *loggers `json:"logger"`
|
||||
|
||||
@@ -22,30 +22,31 @@ import (
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// Config version
|
||||
const v18 = "18"
|
||||
const v20 = "20"
|
||||
|
||||
var (
|
||||
// serverConfig server config.
|
||||
serverConfig *serverConfigV18
|
||||
serverConfig *serverConfigV20
|
||||
serverConfigMu sync.RWMutex
|
||||
)
|
||||
|
||||
// serverConfigV18 server configuration version '18' which is like
|
||||
// version '17' except it adds support for "deliveryMode" parameter in
|
||||
// the AMQP notification target.
|
||||
type serverConfigV18 struct {
|
||||
// serverConfigV20 server configuration version '20' which is like
|
||||
// version '19' except it adds support for VirtualHostDomain
|
||||
type serverConfigV20 struct {
|
||||
sync.RWMutex
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential credential `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Browser BrowserFlag `json:"browser"`
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Browser BrowserFlag `json:"browser"`
|
||||
Domain string `json:"domain"`
|
||||
|
||||
// Additional error logging configuration.
|
||||
Logger *loggers `json:"logger"`
|
||||
@@ -55,40 +56,47 @@ type serverConfigV18 struct {
|
||||
}
|
||||
|
||||
// GetVersion get current config version.
|
||||
func (s *serverConfigV18) GetVersion() string {
|
||||
func (s *serverConfigV20) GetVersion() string {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.Version
|
||||
}
|
||||
|
||||
// SetRegion set new region.
|
||||
func (s *serverConfigV18) SetRegion(region string) {
|
||||
// SetRegion set a new region.
|
||||
func (s *serverConfigV20) SetRegion(region string) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
// Save new region.
|
||||
s.Region = region
|
||||
}
|
||||
|
||||
// GetRegion get current region.
|
||||
func (s *serverConfigV18) GetRegion() string {
|
||||
func (s *serverConfigV20) GetRegion() string {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.Region
|
||||
}
|
||||
|
||||
// SetCredentials set new credentials.
|
||||
func (s *serverConfigV18) SetCredential(creds credential) {
|
||||
// SetCredentials set new credentials. SetCredential returns the previous credential.
|
||||
func (s *serverConfigV20) SetCredential(creds auth.Credentials) (prevCred auth.Credentials) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
// Save previous credential.
|
||||
prevCred = s.Credential
|
||||
|
||||
// Set updated credential.
|
||||
s.Credential = creds
|
||||
|
||||
// Return previous credential.
|
||||
return prevCred
|
||||
}
|
||||
|
||||
// GetCredentials get current credentials.
|
||||
func (s *serverConfigV18) GetCredential() credential {
|
||||
func (s *serverConfigV20) GetCredential() auth.Credentials {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
@@ -96,7 +104,7 @@ func (s *serverConfigV18) GetCredential() credential {
|
||||
}
|
||||
|
||||
// SetBrowser set if browser is enabled.
|
||||
func (s *serverConfigV18) SetBrowser(b bool) {
|
||||
func (s *serverConfigV20) SetBrowser(b bool) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
@@ -105,7 +113,7 @@ func (s *serverConfigV18) SetBrowser(b bool) {
|
||||
}
|
||||
|
||||
// GetCredentials get current credentials.
|
||||
func (s *serverConfigV18) GetBrowser() bool {
|
||||
func (s *serverConfigV20) GetBrowser() bool {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
@@ -113,7 +121,7 @@ func (s *serverConfigV18) GetBrowser() bool {
|
||||
}
|
||||
|
||||
// Save config.
|
||||
func (s *serverConfigV18) Save() error {
|
||||
func (s *serverConfigV20) Save() error {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
@@ -121,10 +129,10 @@ func (s *serverConfigV18) Save() error {
|
||||
return quick.Save(getConfigFile(), s)
|
||||
}
|
||||
|
||||
func newServerConfigV18() *serverConfigV18 {
|
||||
srvCfg := &serverConfigV18{
|
||||
Version: v18,
|
||||
Credential: mustGetNewCredential(),
|
||||
func newServerConfigV20() *serverConfigV20 {
|
||||
srvCfg := &serverConfigV20{
|
||||
Version: v20,
|
||||
Credential: auth.MustGetNewCredentials(),
|
||||
Region: globalMinioDefaultRegion,
|
||||
Browser: true,
|
||||
Logger: &loggers{},
|
||||
@@ -137,6 +145,8 @@ func newServerConfigV18() *serverConfigV18 {
|
||||
// Make sure to initialize notification configs.
|
||||
srvCfg.Notify.AMQP = make(map[string]amqpNotify)
|
||||
srvCfg.Notify.AMQP["1"] = amqpNotify{}
|
||||
srvCfg.Notify.MQTT = make(map[string]mqttNotify)
|
||||
srvCfg.Notify.MQTT["1"] = mqttNotify{}
|
||||
srvCfg.Notify.ElasticSearch = make(map[string]elasticSearchNotify)
|
||||
srvCfg.Notify.ElasticSearch["1"] = elasticSearchNotify{}
|
||||
srvCfg.Notify.Redis = make(map[string]redisNotify)
|
||||
@@ -159,7 +169,7 @@ func newServerConfigV18() *serverConfigV18 {
|
||||
// found, otherwise use default parameters
|
||||
func newConfig() error {
|
||||
// Initialize server config.
|
||||
srvCfg := newServerConfigV18()
|
||||
srvCfg := newServerConfigV20()
|
||||
|
||||
// If env is set override the credentials from config file.
|
||||
if globalIsEnvCreds {
|
||||
@@ -174,6 +184,10 @@ func newConfig() error {
|
||||
srvCfg.SetRegion(globalServerRegion)
|
||||
}
|
||||
|
||||
if globalIsEnvDomainName {
|
||||
srvCfg.Domain = globalDomainName
|
||||
}
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
// Save the new config globally.
|
||||
// unlock the mutex.
|
||||
@@ -237,8 +251,8 @@ func checkDupJSONKeys(json string) error {
|
||||
}
|
||||
|
||||
// getValidConfig - returns valid server configuration
|
||||
func getValidConfig() (*serverConfigV18, error) {
|
||||
srvCfg := &serverConfigV18{
|
||||
func getValidConfig() (*serverConfigV20, error) {
|
||||
srvCfg := &serverConfigV20{
|
||||
Region: globalMinioDefaultRegion,
|
||||
Browser: true,
|
||||
}
|
||||
@@ -248,8 +262,8 @@ func getValidConfig() (*serverConfigV18, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if srvCfg.Version != v18 {
|
||||
return nil, fmt.Errorf("configuration version mismatch. Expected: ‘%s’, Got: ‘%s’", v18, srvCfg.Version)
|
||||
if srvCfg.Version != v20 {
|
||||
return nil, fmt.Errorf("configuration version mismatch. Expected: ‘%s’, Got: ‘%s’", v20, srvCfg.Version)
|
||||
}
|
||||
|
||||
// Load config file json and check for duplication json keys
|
||||
@@ -303,6 +317,10 @@ func loadConfig() error {
|
||||
srvCfg.SetRegion(globalServerRegion)
|
||||
}
|
||||
|
||||
if globalIsEnvDomainName {
|
||||
srvCfg.Domain = globalDomainName
|
||||
}
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
serverConfigMu.Lock()
|
||||
serverConfig = srvCfg
|
||||
@@ -315,6 +333,9 @@ func loadConfig() error {
|
||||
if !globalIsEnvRegion {
|
||||
globalServerRegion = serverConfig.GetRegion()
|
||||
}
|
||||
if !globalIsEnvDomainName {
|
||||
globalDomainName = serverConfig.Domain
|
||||
}
|
||||
serverConfigMu.Unlock()
|
||||
|
||||
return nil
|
||||
@@ -32,7 +32,7 @@ func TestServerConfig(t *testing.T) {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
if serverConfig.GetRegion() != globalMinioDefaultRegion {
|
||||
t.Errorf("Expecting region `us-east-1` found %s", serverConfig.GetRegion())
|
||||
@@ -80,13 +80,21 @@ func TestServerConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
// Set new console logger.
|
||||
// Set new Webhook notification id.
|
||||
// Set new MySQL notification id.
|
||||
serverConfig.Notify.SetMySQLByID("2", mySQLNotify{})
|
||||
savedNotifyCfg6 := serverConfig.Notify.GetMySQLByID("2")
|
||||
if !reflect.DeepEqual(savedNotifyCfg6, mySQLNotify{}) {
|
||||
t.Errorf("Expecting Webhook config %#v found %#v", mySQLNotify{}, savedNotifyCfg6)
|
||||
}
|
||||
|
||||
// Set new console logger.
|
||||
// Set new MQTT notification id.
|
||||
serverConfig.Notify.SetMQTTByID("2", mqttNotify{})
|
||||
savedNotifyCfg7 := serverConfig.Notify.GetMQTTByID("2")
|
||||
if !reflect.DeepEqual(savedNotifyCfg7, mqttNotify{}) {
|
||||
t.Errorf("Expecting Webhook config %#v found %#v", mqttNotify{}, savedNotifyCfg7)
|
||||
}
|
||||
|
||||
consoleLogger := NewConsoleLogger()
|
||||
serverConfig.Logger.SetConsole(consoleLogger)
|
||||
consoleCfg := serverConfig.Logger.GetConsole()
|
||||
@@ -109,8 +117,8 @@ func TestServerConfig(t *testing.T) {
|
||||
serverConfig.Logger.SetFile(fileLogger)
|
||||
|
||||
// Match version.
|
||||
if serverConfig.GetVersion() != v18 {
|
||||
t.Errorf("Expecting version %s found %s", serverConfig.GetVersion(), v18)
|
||||
if serverConfig.GetVersion() != v20 {
|
||||
t.Errorf("Expecting version %s found %s", serverConfig.GetVersion(), v20)
|
||||
}
|
||||
|
||||
// Attempt to save.
|
||||
@@ -141,6 +149,9 @@ func TestServerConfigWithEnvs(t *testing.T) {
|
||||
os.Setenv("MINIO_REGION", "us-west-1")
|
||||
defer os.Unsetenv("MINIO_REGION")
|
||||
|
||||
os.Setenv("MINIO_DOMAIN", "domain.com")
|
||||
defer os.Unsetenv("MINIO_DOMAIN")
|
||||
|
||||
defer resetGlobalIsEnvs()
|
||||
|
||||
// Get test root.
|
||||
@@ -158,7 +169,7 @@ func TestServerConfigWithEnvs(t *testing.T) {
|
||||
initConfig()
|
||||
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// Check if serverConfig has
|
||||
if serverConfig.GetBrowser() {
|
||||
@@ -181,6 +192,9 @@ func TestServerConfigWithEnvs(t *testing.T) {
|
||||
t.Errorf("Expecting access key to be `minio123` found %s", cred.SecretKey)
|
||||
}
|
||||
|
||||
if serverConfig.Domain != "domain.com" {
|
||||
t.Errorf("Expecting Domain to be `domain.com` found " + serverConfig.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDupJSONKeys(t *testing.T) {
|
||||
@@ -219,11 +233,11 @@ func TestValidateConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
configPath := filepath.Join(rootPath, minioConfigFile)
|
||||
|
||||
v := v18
|
||||
v := v20
|
||||
|
||||
testCases := []struct {
|
||||
configData string
|
||||
@@ -309,6 +323,9 @@ func TestValidateConfig(t *testing.T) {
|
||||
|
||||
// Test 28 - Test valid Format for Redis
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
|
||||
|
||||
// Test 29 - Test MQTT
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "clientId": "", "username": "", "password": ""}}}}`, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
// Minimum length for Minio access key.
|
||||
accessKeyMinLen = 5
|
||||
|
||||
// Maximum length for Minio access key.
|
||||
accessKeyMaxLen = 20
|
||||
|
||||
// Minimum length for Minio secret key for both server and gateway mode.
|
||||
secretKeyMinLen = 8
|
||||
|
||||
// Maximum secret key length for Minio, this
|
||||
// is used when autogenerating new credentials.
|
||||
secretKeyMaxLenMinio = 40
|
||||
|
||||
// Maximum secret key length allowed from client side
|
||||
// caters for both server and gateway mode.
|
||||
secretKeyMaxLen = 100
|
||||
|
||||
// Alpha numeric table used for generating access keys.
|
||||
alphaNumericTable = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
// Total length of the alpha numeric table.
|
||||
alphaNumericTableLen = byte(len(alphaNumericTable))
|
||||
)
|
||||
|
||||
// Common errors generated for access and secret key validation.
|
||||
var (
|
||||
errInvalidAccessKeyLength = errors.New("Invalid access key, access key should be 5 to 20 characters in length")
|
||||
errInvalidSecretKeyLength = errors.New("Invalid secret key, secret key should be 8 to 100 characters in length")
|
||||
)
|
||||
|
||||
// isAccessKeyValid - validate access key for right length.
|
||||
func isAccessKeyValid(accessKey string) bool {
|
||||
return len(accessKey) >= accessKeyMinLen && len(accessKey) <= accessKeyMaxLen
|
||||
}
|
||||
|
||||
// isSecretKeyValid - validate secret key for right length.
|
||||
func isSecretKeyValid(secretKey string) bool {
|
||||
return len(secretKey) >= secretKeyMinLen && len(secretKey) <= secretKeyMaxLen
|
||||
}
|
||||
|
||||
// credential container for access and secret keys.
|
||||
type credential struct {
|
||||
AccessKey string `json:"accessKey,omitempty"`
|
||||
SecretKey string `json:"secretKey,omitempty"`
|
||||
secretKeyHash []byte
|
||||
}
|
||||
|
||||
// IsValid - returns whether credential is valid or not.
|
||||
func (cred credential) IsValid() bool {
|
||||
return isAccessKeyValid(cred.AccessKey) && isSecretKeyValid(cred.SecretKey)
|
||||
}
|
||||
|
||||
// Equals - returns whether two credentials are equal or not.
|
||||
func (cred credential) Equal(ccred credential) bool {
|
||||
if !ccred.IsValid() {
|
||||
return false
|
||||
}
|
||||
|
||||
if cred.secretKeyHash == nil {
|
||||
secretKeyHash, err := bcrypt.GenerateFromPassword([]byte(cred.SecretKey), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to generate hash of given password")
|
||||
return false
|
||||
}
|
||||
|
||||
cred.secretKeyHash = secretKeyHash
|
||||
}
|
||||
|
||||
return (cred.AccessKey == ccred.AccessKey &&
|
||||
bcrypt.CompareHashAndPassword(cred.secretKeyHash, []byte(ccred.SecretKey)) == nil)
|
||||
}
|
||||
|
||||
func createCredential(accessKey, secretKey string) (cred credential, err error) {
|
||||
if !isAccessKeyValid(accessKey) {
|
||||
err = errInvalidAccessKeyLength
|
||||
} else if !isSecretKeyValid(secretKey) {
|
||||
err = errInvalidSecretKeyLength
|
||||
} else {
|
||||
var secretKeyHash []byte
|
||||
secretKeyHash, err = bcrypt.GenerateFromPassword([]byte(secretKey), bcrypt.DefaultCost)
|
||||
if err == nil {
|
||||
cred.AccessKey = accessKey
|
||||
cred.SecretKey = secretKey
|
||||
cred.secretKeyHash = secretKeyHash
|
||||
}
|
||||
}
|
||||
|
||||
return cred, err
|
||||
}
|
||||
|
||||
// Initialize a new credential object
|
||||
func mustGetNewCredential() credential {
|
||||
// Generate access key.
|
||||
keyBytes := make([]byte, accessKeyMaxLen)
|
||||
_, err := rand.Read(keyBytes)
|
||||
fatalIf(err, "Unable to generate access key.")
|
||||
for i := 0; i < accessKeyMaxLen; i++ {
|
||||
keyBytes[i] = alphaNumericTable[keyBytes[i]%alphaNumericTableLen]
|
||||
}
|
||||
accessKey := string(keyBytes)
|
||||
|
||||
// Generate secret key.
|
||||
keyBytes = make([]byte, secretKeyMaxLenMinio)
|
||||
_, err = rand.Read(keyBytes)
|
||||
fatalIf(err, "Unable to generate secret key.")
|
||||
secretKey := string([]byte(base64.StdEncoding.EncodeToString(keyBytes))[:secretKeyMaxLenMinio])
|
||||
|
||||
cred, err := createCredential(accessKey, secretKey)
|
||||
fatalIf(err, "Unable to generate new credential.")
|
||||
|
||||
return cred
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMustGetNewCredential(t *testing.T) {
|
||||
cred := mustGetNewCredential()
|
||||
if !cred.IsValid() {
|
||||
t.Fatalf("Failed to get new valid credential")
|
||||
}
|
||||
if len(cred.SecretKey) != secretKeyMaxLenMinio {
|
||||
t.Fatalf("Invalid length %d of the secretKey credential generated, expected %d", len(cred.SecretKey), secretKeyMaxLenMinio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCredential(t *testing.T) {
|
||||
cred := mustGetNewCredential()
|
||||
testCases := []struct {
|
||||
accessKey string
|
||||
secretKey string
|
||||
expectedResult bool
|
||||
expectedErr error
|
||||
}{
|
||||
// Access key too small.
|
||||
{"user", "pass", false, errInvalidAccessKeyLength},
|
||||
// Access key too long.
|
||||
{"user12345678901234567", "pass", false, errInvalidAccessKeyLength},
|
||||
// Access key contains unsuppported characters.
|
||||
{"!@#$", "pass", false, errInvalidAccessKeyLength},
|
||||
// Secret key too small.
|
||||
{"myuser", "pass", false, errInvalidSecretKeyLength},
|
||||
// Secret key too long.
|
||||
{"myuser", "pass1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", false, errInvalidSecretKeyLength},
|
||||
// Success when access key contains leading/trailing spaces.
|
||||
{" user ", cred.SecretKey, true, nil},
|
||||
{"myuser", "mypassword", true, nil},
|
||||
{cred.AccessKey, cred.SecretKey, true, nil},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
cred, err := createCredential(testCase.accessKey, testCase.secretKey)
|
||||
if testCase.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
} else if testCase.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
|
||||
if testCase.expectedResult != cred.IsValid() {
|
||||
t.Fatalf("cred: expected: %v, got: %v", testCase.expectedResult, cred.IsValid())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialEqual(t *testing.T) {
|
||||
cred := mustGetNewCredential()
|
||||
testCases := []struct {
|
||||
cred credential
|
||||
ccred credential
|
||||
expectedResult bool
|
||||
}{
|
||||
// Empty compare credential
|
||||
{cred, credential{}, false},
|
||||
// Empty credential
|
||||
{credential{}, cred, false},
|
||||
// Two different credentials
|
||||
{cred, mustGetNewCredential(), false},
|
||||
// Access key is different in compare credential.
|
||||
{cred, credential{AccessKey: "myuser", SecretKey: cred.SecretKey}, false},
|
||||
// Secret key is different in compare credential.
|
||||
{cred, credential{AccessKey: cred.AccessKey, SecretKey: "mypassword"}, false},
|
||||
// secretHashKey is missing in compare credential.
|
||||
{cred, credential{AccessKey: cred.AccessKey, SecretKey: cred.SecretKey}, true},
|
||||
// secretHashKey is missing in credential.
|
||||
{credential{AccessKey: cred.AccessKey, SecretKey: cred.SecretKey}, cred, true},
|
||||
// Same credentials.
|
||||
{cred, cred, true},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
result := testCase.cred.Equal(testCase.ccred)
|
||||
if result != testCase.expectedResult {
|
||||
t.Fatalf("cred: expected: %v, got: %v", testCase.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
dynamicTimeoutIncreaseThresholdPct = 0.33 // Upper threshold for failures in order to increase timeout
|
||||
dynamicTimeoutDecreaseThresholdPct = 0.10 // Lower threshold for failures in order to decrease timeout
|
||||
dynamicTimeoutLogSize = 16
|
||||
maxDuration = time.Duration(1<<63 - 1)
|
||||
)
|
||||
|
||||
// timeouts that are dynamically adapted based on actual usage results
|
||||
type dynamicTimeout struct {
|
||||
timeout int64
|
||||
minimum int64
|
||||
entries int64
|
||||
log [dynamicTimeoutLogSize]time.Duration
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// newDynamicTimeout returns a new dynamic timeout initialized with timeout value
|
||||
func newDynamicTimeout(timeout, minimum time.Duration) *dynamicTimeout {
|
||||
return &dynamicTimeout{timeout: int64(timeout), minimum: int64(minimum)}
|
||||
}
|
||||
|
||||
// Timeout returns the current timeout value
|
||||
func (dt *dynamicTimeout) Timeout() time.Duration {
|
||||
return time.Duration(atomic.LoadInt64(&dt.timeout))
|
||||
}
|
||||
|
||||
// LogSuccess logs the duration of a successful action that
|
||||
// did not hit the timeout
|
||||
func (dt *dynamicTimeout) LogSuccess(duration time.Duration) {
|
||||
dt.logEntry(duration)
|
||||
}
|
||||
|
||||
// LogFailure logs an action that hit the timeout
|
||||
func (dt *dynamicTimeout) LogFailure() {
|
||||
dt.logEntry(maxDuration)
|
||||
}
|
||||
|
||||
// logEntry stores a log entry
|
||||
func (dt *dynamicTimeout) logEntry(duration time.Duration) {
|
||||
entries := int(atomic.AddInt64(&dt.entries, 1))
|
||||
index := entries - 1
|
||||
if index < dynamicTimeoutLogSize {
|
||||
dt.mutex.Lock()
|
||||
dt.log[index] = duration
|
||||
dt.mutex.Unlock()
|
||||
}
|
||||
if entries == dynamicTimeoutLogSize {
|
||||
dt.mutex.Lock()
|
||||
|
||||
// Make copy on stack in order to call adjust()
|
||||
logCopy := [dynamicTimeoutLogSize]time.Duration{}
|
||||
copy(logCopy[:], dt.log[:])
|
||||
|
||||
// reset log entries
|
||||
atomic.StoreInt64(&dt.entries, 0)
|
||||
|
||||
dt.mutex.Unlock()
|
||||
|
||||
dt.adjust(logCopy)
|
||||
}
|
||||
}
|
||||
|
||||
// adjust changes the value of the dynamic timeout based on the
|
||||
// previous results
|
||||
func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) {
|
||||
|
||||
failures, average := 0, int64(0)
|
||||
for i := 0; i < len(entries); i++ {
|
||||
if entries[i] == maxDuration {
|
||||
failures++
|
||||
} else {
|
||||
average += int64(entries[i])
|
||||
}
|
||||
}
|
||||
if failures < len(entries) {
|
||||
average /= int64(len(entries) - failures)
|
||||
}
|
||||
|
||||
timeOutHitPct := float64(failures) / float64(len(entries))
|
||||
|
||||
if timeOutHitPct > dynamicTimeoutIncreaseThresholdPct {
|
||||
// We are hitting the timeout too often, so increase the timeout by 25%
|
||||
timeout := atomic.LoadInt64(&dt.timeout) * 125 / 100
|
||||
atomic.StoreInt64(&dt.timeout, timeout)
|
||||
} else if timeOutHitPct < dynamicTimeoutDecreaseThresholdPct {
|
||||
// We are hitting the timeout relatively few times, so decrease the timeout
|
||||
average = average * 125 / 100 // Add buffer of 25% on top of average
|
||||
|
||||
timeout := (atomic.LoadInt64(&dt.timeout) + int64(average)) / 2 // Middle between current timeout and average success
|
||||
if timeout < dt.minimum {
|
||||
timeout = dt.minimum
|
||||
}
|
||||
atomic.StoreInt64(&dt.timeout, timeout)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDynamicTimeoutSingleIncrease(t *testing.T) {
|
||||
|
||||
timeout := newDynamicTimeout(time.Minute, time.Second)
|
||||
|
||||
initial := timeout.Timeout()
|
||||
|
||||
for i := 0; i < dynamicTimeoutLogSize; i++ {
|
||||
timeout.LogFailure()
|
||||
}
|
||||
|
||||
adjusted := timeout.Timeout()
|
||||
|
||||
if initial >= adjusted {
|
||||
t.Errorf("Failure to increase timeout, expected %v to be more than %v", adjusted, initial)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynamicTimeoutDualIncrease(t *testing.T) {
|
||||
|
||||
timeout := newDynamicTimeout(time.Minute, time.Second)
|
||||
|
||||
initial := timeout.Timeout()
|
||||
|
||||
for i := 0; i < dynamicTimeoutLogSize; i++ {
|
||||
timeout.LogFailure()
|
||||
}
|
||||
|
||||
adjusted := timeout.Timeout()
|
||||
|
||||
for i := 0; i < dynamicTimeoutLogSize; i++ {
|
||||
timeout.LogFailure()
|
||||
}
|
||||
|
||||
adjustedAgain := timeout.Timeout()
|
||||
|
||||
if initial >= adjusted || adjusted >= adjustedAgain {
|
||||
t.Errorf("Failure to increase timeout multiple times")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynamicTimeoutSingleDecrease(t *testing.T) {
|
||||
|
||||
timeout := newDynamicTimeout(time.Minute, time.Second)
|
||||
|
||||
initial := timeout.Timeout()
|
||||
|
||||
for i := 0; i < dynamicTimeoutLogSize; i++ {
|
||||
timeout.LogSuccess(20 * time.Second)
|
||||
}
|
||||
|
||||
adjusted := timeout.Timeout()
|
||||
|
||||
if initial <= adjusted {
|
||||
t.Errorf("Failure to decrease timeout, expected %v to be less than %v", adjusted, initial)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynamicTimeoutDualDecrease(t *testing.T) {
|
||||
|
||||
timeout := newDynamicTimeout(time.Minute, time.Second)
|
||||
|
||||
initial := timeout.Timeout()
|
||||
|
||||
for i := 0; i < dynamicTimeoutLogSize; i++ {
|
||||
timeout.LogSuccess(20 * time.Second)
|
||||
}
|
||||
|
||||
adjusted := timeout.Timeout()
|
||||
|
||||
for i := 0; i < dynamicTimeoutLogSize; i++ {
|
||||
timeout.LogSuccess(20 * time.Second)
|
||||
}
|
||||
|
||||
adjustedAgain := timeout.Timeout()
|
||||
|
||||
if initial <= adjusted || adjusted <= adjustedAgain {
|
||||
t.Errorf("Failure to decrease timeout multiple times")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynamicTimeoutManyDecreases(t *testing.T) {
|
||||
|
||||
timeout := newDynamicTimeout(time.Minute, time.Second)
|
||||
|
||||
initial := timeout.Timeout()
|
||||
|
||||
const successTimeout = 20 * time.Second
|
||||
for l := 0; l < 100; l++ {
|
||||
for i := 0; i < dynamicTimeoutLogSize; i++ {
|
||||
timeout.LogSuccess(successTimeout)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
adjusted := timeout.Timeout()
|
||||
// Check whether eventual timeout is between initial value and success timeout
|
||||
if initial <= adjusted || adjusted <= successTimeout {
|
||||
t.Errorf("Failure to decrease timeout appropriately")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynamicTimeoutHitMinimum(t *testing.T) {
|
||||
|
||||
const minimum = 30 * time.Second
|
||||
timeout := newDynamicTimeout(time.Minute, minimum)
|
||||
|
||||
initial := timeout.Timeout()
|
||||
|
||||
const successTimeout = 20 * time.Second
|
||||
for l := 0; l < 100; l++ {
|
||||
for i := 0; i < dynamicTimeoutLogSize; i++ {
|
||||
timeout.LogSuccess(successTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
adjusted := timeout.Timeout()
|
||||
// Check whether eventual timeout has hit the minimum value
|
||||
if initial <= adjusted || adjusted != minimum {
|
||||
t.Errorf("Failure to decrease timeout appropriately")
|
||||
}
|
||||
}
|
||||
|
||||
func testDynamicTimeoutAdjust(t *testing.T, timeout *dynamicTimeout, f func() float64) {
|
||||
|
||||
const successTimeout = 20 * time.Second
|
||||
|
||||
for i := 0; i < dynamicTimeoutLogSize; i++ {
|
||||
|
||||
rnd := f()
|
||||
duration := time.Duration(float64(successTimeout) * rnd)
|
||||
|
||||
if duration < 100*time.Millisecond {
|
||||
duration = 100 * time.Millisecond
|
||||
}
|
||||
if duration >= time.Minute {
|
||||
timeout.LogFailure()
|
||||
} else {
|
||||
timeout.LogSuccess(duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynamicTimeoutAdjustExponential(t *testing.T) {
|
||||
|
||||
timeout := newDynamicTimeout(time.Minute, time.Second)
|
||||
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
||||
initial := timeout.Timeout()
|
||||
|
||||
for try := 0; try < 10; try++ {
|
||||
|
||||
testDynamicTimeoutAdjust(t, timeout, rand.ExpFloat64)
|
||||
|
||||
}
|
||||
|
||||
adjusted := timeout.Timeout()
|
||||
if initial <= adjusted {
|
||||
t.Errorf("Failure to decrease timeout, expected %v to be less than %v", adjusted, initial)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDynamicTimeoutAdjustNormalized(t *testing.T) {
|
||||
|
||||
timeout := newDynamicTimeout(time.Minute, time.Second)
|
||||
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
||||
initial := timeout.Timeout()
|
||||
|
||||
for try := 0; try < 10; try++ {
|
||||
|
||||
testDynamicTimeoutAdjust(t, timeout, func() float64 {
|
||||
return 1.0 + rand.NormFloat64()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
adjusted := timeout.Timeout()
|
||||
if initial <= adjusted {
|
||||
t.Errorf("Failure to decrease timeout, expected %v to be less than %v", adjusted, initial)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
sha256 "github.com/minio/sha256-simd"
|
||||
"github.com/minio/sio"
|
||||
)
|
||||
|
||||
var (
|
||||
// AWS errors for invalid SSE-C requests.
|
||||
errInsecureSSERequest = errors.New("Requests specifying Server Side Encryption with Customer provided keys must be made over a secure connection")
|
||||
errEncryptedObject = errors.New("The object was stored using a form of Server Side Encryption. The correct parameters must be provided to retrieve the object")
|
||||
errInvalidSSEAlgorithm = errors.New("Requests specifying Server Side Encryption with Customer provided keys must provide a valid encryption algorithm")
|
||||
errMissingSSEKey = errors.New("Requests specifying Server Side Encryption with Customer provided keys must provide an appropriate secret key")
|
||||
errInvalidSSEKey = errors.New("The secret key was invalid for the specified algorithm")
|
||||
errMissingSSEKeyMD5 = errors.New("Requests specifying Server Side Encryption with Customer provided keys must provide the client calculated MD5 of the secret key")
|
||||
errSSEKeyMD5Mismatch = errors.New("The calculated MD5 hash of the key did not match the hash that was provided")
|
||||
errSSEKeyMismatch = errors.New("The client provided key does not match the key provided when the object was encrypted") // this msg is not shown to the client
|
||||
|
||||
// Additional Minio errors for SSE-C requests.
|
||||
errObjectTampered = errors.New("The requested object was modified and may be compromised")
|
||||
)
|
||||
|
||||
const (
|
||||
// SSECustomerAlgorithm is the AWS SSE-C algorithm HTTP header key.
|
||||
SSECustomerAlgorithm = "X-Amz-Server-Side-Encryption-Customer-Algorithm"
|
||||
// SSECustomerKey is the AWS SSE-C encryption key HTTP header key.
|
||||
SSECustomerKey = "X-Amz-Server-Side-Encryption-Customer-Key"
|
||||
// SSECustomerKeyMD5 is the AWS SSE-C encryption key MD5 HTTP header key.
|
||||
SSECustomerKeyMD5 = "X-Amz-Server-Side-Encryption-Customer-Key-MD5"
|
||||
)
|
||||
|
||||
const (
|
||||
// SSECustomerKeySize is the size of valid client provided encryption keys in bytes.
|
||||
// Currently AWS supports only AES256. So the SSE-C key size is fixed to 32 bytes.
|
||||
SSECustomerKeySize = 32
|
||||
|
||||
// SSECustomerAlgorithmAES256 the only valid S3 SSE-C encryption algorithm identifier.
|
||||
SSECustomerAlgorithmAES256 = "AES256"
|
||||
)
|
||||
|
||||
// SSE-C key derivation, key verification and key update:
|
||||
// H: Hash function [32 = |H(m)|]
|
||||
// AE: authenticated encryption scheme, AD: authenticated decryption scheme [m = AD(k, AE(k, m))]
|
||||
//
|
||||
// Key derivation:
|
||||
// Input:
|
||||
// key := 32 bytes # client provided key
|
||||
// Re, Rm := 32 bytes, 32 bytes # uniformly random
|
||||
//
|
||||
// Seal:
|
||||
// k := H(key || Re) # object encryption key
|
||||
// r := H(Rm) # save as object metadata [ServerSideEncryptionIV]
|
||||
// KeK := H(key || r) # key encryption key
|
||||
// K := AE(KeK, k) # save as object metadata [ServerSideEncryptionSealedKey]
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Key verification:
|
||||
// Input:
|
||||
// key := 32 bytes # client provided key
|
||||
// r := 32 bytes # object metadata [ServerSideEncryptionIV]
|
||||
// K := 32 bytes # object metadata [ServerSideEncryptionSealedKey]
|
||||
//
|
||||
// Open:
|
||||
// KeK := H(key || r) # key encryption key
|
||||
// k := AD(Kek, K) # object encryption key
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Key update:
|
||||
// Input:
|
||||
// key := 32 bytes # old client provided key
|
||||
// key' := 32 bytes # new client provided key
|
||||
// Rm := 32 bytes # uniformly random
|
||||
// r := 32 bytes # object metadata [ServerSideEncryptionIV]
|
||||
// K := 32 bytes # object metadata [ServerSideEncryptionSealedKey]
|
||||
//
|
||||
// Update:
|
||||
// 1. open:
|
||||
// KeK := H(key || r) # key encryption key
|
||||
// k := AD(Kek, K) # object encryption key
|
||||
// 2. seal:
|
||||
// r' := H(Rm) # save as object metadata [ServerSideEncryptionIV]
|
||||
// KeK' := H(key' || r') # new key encryption key
|
||||
// K' := AE(KeK', k) # save as object metadata [ServerSideEncryptionSealedKey]
|
||||
|
||||
const (
|
||||
// ServerSideEncryptionIV is a 32 byte randomly generated IV used to derive an
|
||||
// unique key encryption key from the client provided key. The combination of this value
|
||||
// and the client-provided key MUST be unique.
|
||||
ServerSideEncryptionIV = ReservedMetadataPrefix + "Server-Side-Encryption-Iv"
|
||||
|
||||
// ServerSideEncryptionSealAlgorithm identifies a combination of a cryptographic hash function and
|
||||
// an authenticated en/decryption scheme to seal the object encryption key.
|
||||
ServerSideEncryptionSealAlgorithm = ReservedMetadataPrefix + "Server-Side-Encryption-Seal-Algorithm"
|
||||
|
||||
// ServerSideEncryptionSealedKey is the sealed object encryption key. The sealed key can be decrypted
|
||||
// by the key encryption key derived from the client provided key and the server-side-encryption IV.
|
||||
ServerSideEncryptionSealedKey = ReservedMetadataPrefix + "Server-Side-Encryption-Sealed-Key"
|
||||
)
|
||||
|
||||
// SSESealAlgorithmDareSha256 specifies DARE as authenticated en/decryption scheme and SHA256 as cryptographic
|
||||
// hash function.
|
||||
const SSESealAlgorithmDareSha256 = "DARE-SHA256"
|
||||
|
||||
// IsSSECustomerRequest returns true if the given HTTP header
|
||||
// contains server-side-encryption with customer provided key fields.
|
||||
func IsSSECustomerRequest(header http.Header) bool {
|
||||
return header.Get(SSECustomerAlgorithm) != "" || header.Get(SSECustomerKey) != "" || header.Get(SSECustomerKeyMD5) != ""
|
||||
}
|
||||
|
||||
// ParseSSECustomerRequest parses the SSE-C header fields of the provided request.
|
||||
// It returns the client provided key on success.
|
||||
func ParseSSECustomerRequest(r *http.Request) (key []byte, err error) {
|
||||
if !globalIsSSL { // minio only supports HTTP or HTTPS requests not both at the same time
|
||||
// we cannot use r.TLS == nil here because Go's http implementation reflects on
|
||||
// the net.Conn and sets the TLS field of http.Request only if it's an tls.Conn.
|
||||
// Minio uses a BufConn (wrapping a tls.Conn) so the type check within the http package
|
||||
// will always fail -> r.TLS is always nil even for TLS requests.
|
||||
return nil, errInsecureSSERequest
|
||||
}
|
||||
header := r.Header
|
||||
if algorithm := header.Get(SSECustomerAlgorithm); algorithm != SSECustomerAlgorithmAES256 {
|
||||
return nil, errInvalidSSEAlgorithm
|
||||
}
|
||||
if header.Get(SSECustomerKey) == "" {
|
||||
return nil, errMissingSSEKey
|
||||
}
|
||||
if header.Get(SSECustomerKeyMD5) == "" {
|
||||
return nil, errMissingSSEKeyMD5
|
||||
}
|
||||
|
||||
key, err = base64.StdEncoding.DecodeString(header.Get(SSECustomerKey))
|
||||
if err != nil {
|
||||
return nil, errInvalidSSEKey
|
||||
}
|
||||
header.Del(SSECustomerKey) // make sure we do not save the key by accident
|
||||
|
||||
if len(key) != SSECustomerKeySize {
|
||||
return nil, errInvalidSSEKey
|
||||
}
|
||||
|
||||
keyMD5, err := base64.StdEncoding.DecodeString(header.Get(SSECustomerKeyMD5))
|
||||
if err != nil {
|
||||
return nil, errSSEKeyMD5Mismatch
|
||||
}
|
||||
if md5Sum := md5.Sum(key); !bytes.Equal(md5Sum[:], keyMD5) {
|
||||
return nil, errSSEKeyMD5Mismatch
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// EncryptRequest takes the client provided content and encrypts the data
|
||||
// with the client provided key. It also marks the object as client-side-encrypted
|
||||
// and sets the correct headers.
|
||||
func EncryptRequest(content io.Reader, r *http.Request, metadata map[string]string) (io.Reader, error) {
|
||||
key, err := ParseSSECustomerRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident
|
||||
|
||||
// security notice:
|
||||
// - If the first 32 bytes of the random value are ever repeated under the same client-provided
|
||||
// key the encrypted object will not be tamper-proof. [ P(coll) ~= 1 / 2^(256 / 2)]
|
||||
// - If the last 32 bytes of the random value are ever repeated under the same client-provided
|
||||
// key an adversary may be able to extract the object encryption key. This depends on the
|
||||
// authenticated en/decryption scheme. The DARE format will generate an 8 byte nonce which must
|
||||
// be repeated in addition to reveal the object encryption key.
|
||||
// [ P(coll) ~= 1 / 2^((256 + 64) / 2) ]
|
||||
nonce := make([]byte, 64) // generate random values for key derivation
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sha := sha256.New() // derive object encryption key
|
||||
sha.Write(key)
|
||||
sha.Write(nonce[:32])
|
||||
objectEncryptionKey := sha.Sum(nil)
|
||||
|
||||
iv := sha256.Sum256(nonce[32:]) // derive key encryption key
|
||||
sha = sha256.New()
|
||||
sha.Write(key)
|
||||
sha.Write(iv[:])
|
||||
keyEncryptionKey := sha.Sum(nil)
|
||||
|
||||
sealedKey := bytes.NewBuffer(nil) // sealedKey := 16 byte header + 32 byte payload + 16 byte tag
|
||||
n, err := sio.Encrypt(sealedKey, bytes.NewReader(objectEncryptionKey), sio.Config{
|
||||
Key: keyEncryptionKey,
|
||||
})
|
||||
if n != 64 || err != nil {
|
||||
return nil, errors.New("failed to seal object encryption key") // if this happens there's a bug in the code (may panic ?)
|
||||
}
|
||||
|
||||
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey})
|
||||
if err != nil {
|
||||
return nil, errInvalidSSEKey
|
||||
}
|
||||
|
||||
metadata[ServerSideEncryptionIV] = base64.StdEncoding.EncodeToString(iv[:])
|
||||
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareSha256
|
||||
metadata[ServerSideEncryptionSealedKey] = base64.StdEncoding.EncodeToString(sealedKey.Bytes())
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// DecryptRequest decrypts the object with the client provided key. It also removes
|
||||
// the client-side-encryption metadata from the object and sets the correct headers.
|
||||
func DecryptRequest(client io.Writer, r *http.Request, metadata map[string]string) (io.WriteCloser, error) {
|
||||
key, err := ParseSSECustomerRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident
|
||||
|
||||
if metadata[ServerSideEncryptionSealAlgorithm] != SSESealAlgorithmDareSha256 { // currently DARE-SHA256 is the only option
|
||||
return nil, errObjectTampered
|
||||
}
|
||||
iv, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionIV])
|
||||
if err != nil || len(iv) != 32 {
|
||||
return nil, errObjectTampered
|
||||
}
|
||||
sealedKey, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionSealedKey])
|
||||
if err != nil || len(sealedKey) != 64 {
|
||||
return nil, errObjectTampered
|
||||
}
|
||||
|
||||
sha := sha256.New() // derive key encryption key
|
||||
sha.Write(key)
|
||||
sha.Write(iv)
|
||||
keyEncryptionKey := sha.Sum(nil)
|
||||
|
||||
objectEncryptionKey := bytes.NewBuffer(nil) // decrypt object encryption key
|
||||
n, err := sio.Decrypt(objectEncryptionKey, bytes.NewReader(sealedKey), sio.Config{
|
||||
Key: keyEncryptionKey,
|
||||
})
|
||||
if n != 32 || err != nil {
|
||||
// Either the provided key does not match or the object was tampered.
|
||||
// To provide strict AWS S3 compatibility we return: access denied.
|
||||
return nil, errSSEKeyMismatch
|
||||
}
|
||||
|
||||
writer, err := sio.DecryptWriter(client, sio.Config{Key: objectEncryptionKey.Bytes()})
|
||||
if err != nil {
|
||||
return nil, errInvalidSSEKey
|
||||
}
|
||||
|
||||
delete(metadata, ServerSideEncryptionIV)
|
||||
delete(metadata, ServerSideEncryptionSealAlgorithm)
|
||||
delete(metadata, ServerSideEncryptionSealedKey)
|
||||
return writer, nil
|
||||
}
|
||||
|
||||
// IsEncrypted returns true if the object is marked as encrypted.
|
||||
func (o *ObjectInfo) IsEncrypted() bool {
|
||||
if _, ok := o.UserDefined[ServerSideEncryptionIV]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := o.UserDefined[ServerSideEncryptionSealAlgorithm]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := o.UserDefined[ServerSideEncryptionSealedKey]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DecryptedSize returns the size of the object after decryption in bytes.
|
||||
// It returns an error if the object is not encrypted or marked as encrypted
|
||||
// but has an invalid size.
|
||||
// DecryptedSize panics if the referred object is not encrypted.
|
||||
func (o *ObjectInfo) DecryptedSize() (int64, error) {
|
||||
if !o.IsEncrypted() {
|
||||
panic("cannot compute decrypted size of an object which is not encrypted")
|
||||
}
|
||||
if o.Size == 0 {
|
||||
return o.Size, nil
|
||||
}
|
||||
size := (o.Size / (32 + 64*1024)) * (64 * 1024)
|
||||
if mod := o.Size % (32 + 64*1024); mod > 0 {
|
||||
if mod < 33 {
|
||||
return -1, errObjectTampered // object is not 0 size but smaller than the smallest valid encrypted object
|
||||
}
|
||||
size += mod - 32
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
|
||||
// EncryptedSize returns the size of the object after encryption.
|
||||
// An encrypted object is always larger than a plain object
|
||||
// except for zero size objects.
|
||||
func (o *ObjectInfo) EncryptedSize() int64 {
|
||||
size := (o.Size / (64 * 1024)) * (32 + 64*1024)
|
||||
if mod := o.Size % (64 * 1024); mod > 0 {
|
||||
size += mod + 32
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// DecryptObjectInfo tries to decrypt the provided object if it is encrypted.
|
||||
// It fails if the object is encrypted and the HTTP headers don't contain
|
||||
// SSE-C headers or the object is not encrypted but SSE-C headers are provided. (AWS behavior)
|
||||
// DecryptObjectInfo returns 'ErrNone' if the object is not encrypted or the
|
||||
// decryption succeeded.
|
||||
//
|
||||
// DecryptObjectInfo also returns whether the object is encrypted or not.
|
||||
func DecryptObjectInfo(info *ObjectInfo, headers http.Header) (apiErr APIErrorCode, encrypted bool) {
|
||||
if apiErr, encrypted = ErrNone, info.IsEncrypted(); !encrypted && IsSSECustomerRequest(headers) {
|
||||
apiErr = ErrInvalidEncryptionParameters
|
||||
} else if encrypted {
|
||||
if !IsSSECustomerRequest(headers) {
|
||||
apiErr = ErrSSEEncryptedObject
|
||||
return
|
||||
}
|
||||
var err error
|
||||
if info.Size, err = info.DecryptedSize(); err != nil {
|
||||
apiErr = toAPIErrorCode(err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var isSSECustomerRequestTests = []struct {
|
||||
headers map[string]string
|
||||
sseRequest bool
|
||||
}{
|
||||
{headers: map[string]string{SSECustomerAlgorithm: "AES256", SSECustomerKey: "key", SSECustomerKeyMD5: "md5"}, sseRequest: true}, // 0
|
||||
{headers: map[string]string{SSECustomerAlgorithm: "AES256"}, sseRequest: true}, // 1
|
||||
{headers: map[string]string{SSECustomerKey: "key"}, sseRequest: true}, // 2
|
||||
{headers: map[string]string{SSECustomerKeyMD5: "md5"}, sseRequest: true}, // 3
|
||||
{headers: map[string]string{}, sseRequest: false}, // 4
|
||||
{headers: map[string]string{SSECustomerAlgorithm + " ": "AES256", " " + SSECustomerKey: "key", SSECustomerKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
|
||||
{headers: map[string]string{SSECustomerAlgorithm: "", SSECustomerKey: "", SSECustomerKeyMD5: ""}, sseRequest: false}, // 6
|
||||
}
|
||||
|
||||
func TestIsSSECustomerRequest(t *testing.T) {
|
||||
for i, test := range isSSECustomerRequestTests {
|
||||
headers := http.Header{}
|
||||
for k, v := range test.headers {
|
||||
headers.Set(k, v)
|
||||
}
|
||||
if IsSSECustomerRequest(headers) != test.sseRequest {
|
||||
t.Errorf("Test %d: Expected IsSSECustomerRequest to return %v", i, test.sseRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var parseSSECustomerRequestTests = []struct {
|
||||
headers map[string]string
|
||||
useTLS bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
useTLS: true, err: nil,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
useTLS: false, err: errInsecureSSERequest,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES 256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
useTLS: true, err: errInvalidSSEAlgorithm,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
|
||||
SSECustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||
},
|
||||
useTLS: true, err: errSSEKeyMD5Mismatch,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
|
||||
SSECustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||
},
|
||||
useTLS: true, err: errInvalidSSEKey,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
|
||||
SSECustomerKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
|
||||
},
|
||||
useTLS: true, err: errSSEKeyMD5Mismatch,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
|
||||
SSECustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||
},
|
||||
useTLS: true, err: errInvalidSSEKey,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "", // 7
|
||||
SSECustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||
},
|
||||
useTLS: true, err: errMissingSSEKey,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
||||
SSECustomerKeyMD5: "",
|
||||
},
|
||||
useTLS: true, err: errMissingSSEKeyMD5,
|
||||
},
|
||||
}
|
||||
|
||||
func TestParseSSECustomerRequest(t *testing.T) {
|
||||
defer func(flag bool) { globalIsSSL = flag }(globalIsSSL)
|
||||
for i, test := range parseSSECustomerRequestTests {
|
||||
headers := http.Header{}
|
||||
for k, v := range test.headers {
|
||||
headers.Set(k, v)
|
||||
}
|
||||
request := &http.Request{}
|
||||
request.Header = headers
|
||||
globalIsSSL = test.useTLS
|
||||
|
||||
_, err := ParseSSECustomerRequest(request)
|
||||
if err != test.err {
|
||||
t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err)
|
||||
}
|
||||
key := request.Header.Get(SSECustomerKey)
|
||||
if (err == nil || err == errSSEKeyMD5Mismatch) && key != "" {
|
||||
t.Errorf("Test %d: Client key survived parsing - found key: %v", i, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var encryptedSizeTests = []struct {
|
||||
size, encsize int64
|
||||
}{
|
||||
{size: 0, encsize: 0}, // 0
|
||||
{size: 1, encsize: 33}, // 1
|
||||
{size: 1024, encsize: 1024 + 32}, // 2
|
||||
{size: 2 * 64 * 1024, encsize: 2 * (64*1024 + 32)}, // 3
|
||||
{size: 100*64*1024 + 1, encsize: 100*(64*1024+32) + 33}, // 4
|
||||
{size: 64*1024 + 1, encsize: (64*1024 + 32) + 33}, // 5
|
||||
{size: 5 * 1024 * 1024 * 1024, encsize: 81920 * (64*1024 + 32)}, // 6
|
||||
}
|
||||
|
||||
func TestEncryptedSize(t *testing.T) {
|
||||
for i, test := range encryptedSizeTests {
|
||||
objInfo := ObjectInfo{Size: test.size}
|
||||
if size := objInfo.EncryptedSize(); test.encsize != size {
|
||||
t.Errorf("Test %d: got encrypted size: #%d want: #%d", i, size, test.encsize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var decryptSSECustomerObjectInfoTests = []struct {
|
||||
encsize, size int64
|
||||
err error
|
||||
}{
|
||||
{encsize: 0, size: 0, err: nil}, // 0
|
||||
{encsize: 33, size: 1, err: nil}, // 1
|
||||
{encsize: 1024 + 32, size: 1024, err: nil}, // 2
|
||||
{encsize: 2 * (64*1024 + 32), size: 2 * 64 * 1024, err: nil}, // 3
|
||||
{encsize: 100*(64*1024+32) + 33, size: 100*64*1024 + 1, err: nil}, // 4
|
||||
{encsize: (64*1024 + 32) + 33, size: 64*1024 + 1, err: nil}, // 5
|
||||
{encsize: 81920 * (64*1024 + 32), size: 5 * 1024 * 1024 * 1024, err: nil}, // 6
|
||||
{encsize: 0, size: 0, err: nil}, // 7
|
||||
{encsize: 64*1024 + 32 + 31, size: 0, err: errObjectTampered}, // 8
|
||||
}
|
||||
|
||||
func TestDecryptedSize(t *testing.T) {
|
||||
for i, test := range decryptSSECustomerObjectInfoTests {
|
||||
objInfo := ObjectInfo{Size: test.encsize}
|
||||
objInfo.UserDefined = map[string]string{
|
||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256,
|
||||
}
|
||||
|
||||
size, err := objInfo.DecryptedSize()
|
||||
if err != test.err || (size != test.size && err == nil) {
|
||||
t.Errorf("Test %d: decryption returned: %v want: %v", i, err, test.err)
|
||||
}
|
||||
if err == nil && size != test.size {
|
||||
t.Errorf("Test %d: got decrypted size: #%d want: #%d", i, size, test.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var encryptRequestTests = []struct {
|
||||
header map[string]string
|
||||
metadata map[string]string
|
||||
}{
|
||||
{
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
metadata: map[string]string{},
|
||||
},
|
||||
{
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestEncryptRequest(t *testing.T) {
|
||||
defer func(flag bool) { globalIsSSL = flag }(globalIsSSL)
|
||||
globalIsSSL = true
|
||||
for i, test := range encryptRequestTests {
|
||||
content := bytes.NewReader(make([]byte, 64))
|
||||
req := &http.Request{Header: http.Header{}}
|
||||
for k, v := range test.header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
_, err := EncryptRequest(content, req, test.metadata)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
||||
}
|
||||
if key, ok := test.metadata[SSECustomerKey]; ok {
|
||||
t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key)
|
||||
}
|
||||
if kdf, ok := test.metadata[ServerSideEncryptionSealAlgorithm]; !ok {
|
||||
t.Errorf("Test %d: ServerSideEncryptionKDF must be part of metadata: %v", i, kdf)
|
||||
}
|
||||
if iv, ok := test.metadata[ServerSideEncryptionIV]; !ok {
|
||||
t.Errorf("Test %d: ServerSideEncryptionIV must be part of metadata: %v", i, iv)
|
||||
}
|
||||
if mac, ok := test.metadata[ServerSideEncryptionSealedKey]; !ok {
|
||||
t.Errorf("Test %d: ServerSideEncryptionKeyMAC must be part of metadata: %v", i, mac)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var decryptRequestTests = []struct {
|
||||
header map[string]string
|
||||
metadata map[string]string
|
||||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||
SSECustomerKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256,
|
||||
ServerSideEncryptionIV: "7nQqotA8xgrPx6QK7Ap3GCfjKitqJSrGP7xzgErSJlw=",
|
||||
ServerSideEncryptionSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA==",
|
||||
},
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
ServerSideEncryptionSealAlgorithm: "HMAC-SHA3",
|
||||
ServerSideEncryptionIV: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
ServerSideEncryptionSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
||||
},
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256,
|
||||
ServerSideEncryptionIV: "RrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
ServerSideEncryptionSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
||||
},
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256,
|
||||
ServerSideEncryptionIV: "XAm0dRrJsEsyPb1UuFNezv1bl9ehxuYsgUVC/MUctE2k=",
|
||||
ServerSideEncryptionSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hds4rR9z/CUuPqu5N4=",
|
||||
},
|
||||
shouldFail: true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestDecryptRequest(t *testing.T) {
|
||||
defer func(flag bool) { globalIsSSL = flag }(globalIsSSL)
|
||||
globalIsSSL = true
|
||||
for i, test := range decryptRequestTests {
|
||||
client := bytes.NewBuffer(nil)
|
||||
req := &http.Request{Header: http.Header{}}
|
||||
for k, v := range test.header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
_, err := DecryptRequest(client, req, test.metadata)
|
||||
if err != nil && !test.shouldFail {
|
||||
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
||||
}
|
||||
if key, ok := test.metadata[SSECustomerKey]; ok {
|
||||
t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key)
|
||||
}
|
||||
if kdf, ok := test.metadata[ServerSideEncryptionSealAlgorithm]; ok && !test.shouldFail {
|
||||
t.Errorf("Test %d: ServerSideEncryptionKDF should not be part of metadata: %v", i, kdf)
|
||||
}
|
||||
if iv, ok := test.metadata[ServerSideEncryptionIV]; ok && !test.shouldFail {
|
||||
t.Errorf("Test %d: ServerSideEncryptionIV should not be part of metadata: %v", i, iv)
|
||||
}
|
||||
if mac, ok := test.metadata[ServerSideEncryptionSealedKey]; ok && !test.shouldFail {
|
||||
t.Errorf("Test %d: ServerSideEncryptionKeyMAC should not be part of metadata: %v", i, mac)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var decryptObjectInfoTests = []struct {
|
||||
info ObjectInfo
|
||||
headers http.Header
|
||||
expErr APIErrorCode
|
||||
}{
|
||||
{
|
||||
info: ObjectInfo{Size: 100},
|
||||
headers: http.Header{},
|
||||
expErr: ErrNone,
|
||||
},
|
||||
{
|
||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}},
|
||||
expErr: ErrNone,
|
||||
},
|
||||
{
|
||||
info: ObjectInfo{Size: 0, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}},
|
||||
expErr: ErrNone,
|
||||
},
|
||||
{
|
||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||
headers: http.Header{},
|
||||
expErr: ErrSSEEncryptedObject,
|
||||
},
|
||||
{
|
||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{}},
|
||||
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}},
|
||||
expErr: ErrInvalidEncryptionParameters,
|
||||
},
|
||||
{
|
||||
info: ObjectInfo{Size: 31, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}},
|
||||
expErr: ErrObjectTampered,
|
||||
},
|
||||
}
|
||||
|
||||
func TestDecryptObjectInfo(t *testing.T) {
|
||||
for i, test := range decryptObjectInfoTests {
|
||||
if err, encrypted := DecryptObjectInfo(&test.info, test.headers); err != test.expErr {
|
||||
t.Errorf("Test %d: Decryption returned wrong error code: got %d , want %d", i, err, test.expErr)
|
||||
} else if enc := test.info.IsEncrypted(); encrypted && enc != encrypted {
|
||||
t.Errorf("Test %d: Decryption thinks object is encrypted but it is not", i)
|
||||
} else if !encrypted && enc != encrypted {
|
||||
t.Errorf("Test %d: Decryption thinks object is not encrypted but it is", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
-13
@@ -21,11 +21,14 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/pkg/mountinfo"
|
||||
)
|
||||
|
||||
// EndpointType - enum for endpoint type.
|
||||
@@ -77,14 +80,14 @@ func (endpoint Endpoint) SetHTTP() {
|
||||
}
|
||||
|
||||
// NewEndpoint - returns new endpoint based on given arguments.
|
||||
func NewEndpoint(arg string) (Endpoint, error) {
|
||||
func NewEndpoint(arg string) (ep Endpoint, e error) {
|
||||
// isEmptyPath - check whether given path is not empty.
|
||||
isEmptyPath := func(path string) bool {
|
||||
return path == "" || path == "/" || path == `\`
|
||||
}
|
||||
|
||||
if isEmptyPath(arg) {
|
||||
return Endpoint{}, fmt.Errorf("empty or root endpoint is not supported")
|
||||
return ep, fmt.Errorf("empty or root endpoint is not supported")
|
||||
}
|
||||
|
||||
var isLocal bool
|
||||
@@ -96,13 +99,14 @@ func NewEndpoint(arg string) (Endpoint, error) {
|
||||
// - All field should be empty except Host and Path.
|
||||
if !((u.Scheme == "http" || u.Scheme == "https") &&
|
||||
u.User == nil && u.Opaque == "" && u.ForceQuery == false && u.RawQuery == "" && u.Fragment == "") {
|
||||
return Endpoint{}, fmt.Errorf("invalid URL endpoint format")
|
||||
return ep, fmt.Errorf("invalid URL endpoint format")
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
var host, port string
|
||||
host, port, err = net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "missing port in address") {
|
||||
return Endpoint{}, fmt.Errorf("invalid URL endpoint format: %s", err)
|
||||
return ep, fmt.Errorf("invalid URL endpoint format: %s", err)
|
||||
}
|
||||
|
||||
host = u.Host
|
||||
@@ -110,28 +114,54 @@ func NewEndpoint(arg string) (Endpoint, error) {
|
||||
var p int
|
||||
p, err = strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return Endpoint{}, fmt.Errorf("invalid URL endpoint format: invalid port number")
|
||||
return ep, fmt.Errorf("invalid URL endpoint format: invalid port number")
|
||||
} else if p < 1 || p > 65535 {
|
||||
return Endpoint{}, fmt.Errorf("invalid URL endpoint format: port number must be between 1 to 65535")
|
||||
return ep, fmt.Errorf("invalid URL endpoint format: port number must be between 1 to 65535")
|
||||
}
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
return Endpoint{}, fmt.Errorf("invalid URL endpoint format: empty host name")
|
||||
return ep, fmt.Errorf("invalid URL endpoint format: empty host name")
|
||||
}
|
||||
|
||||
// As this is path in the URL, we should use path package, not filepath package.
|
||||
// On MS Windows, filepath.Clean() converts into Windows path style ie `/foo` becomes `\foo`
|
||||
u.Path = path.Clean(u.Path)
|
||||
if isEmptyPath(u.Path) {
|
||||
return Endpoint{}, fmt.Errorf("empty or root path is not supported in URL endpoint")
|
||||
return ep, fmt.Errorf("empty or root path is not supported in URL endpoint")
|
||||
}
|
||||
|
||||
// On windows having a preceding "/" will cause problems, if the
|
||||
// command line already has C:/<export-folder/ in it. Final resulting
|
||||
// path on windows might become C:/C:/ this will cause problems
|
||||
// of starting minio server properly in distributed mode on windows.
|
||||
// As a special case make sure to trim the separator.
|
||||
|
||||
// NOTE: It is also perfectly fine for windows users to have a path
|
||||
// without C:/ since at that point we treat it as relative path
|
||||
// and obtain the full filesystem path as well. Providing C:/
|
||||
// style is necessary to provide paths other than C:/,
|
||||
// such as F:/, D:/ etc.
|
||||
//
|
||||
// Another additional benefit here is that this style also
|
||||
// supports providing \\host\share support as well.
|
||||
if runtime.GOOS == globalWindowsOSName {
|
||||
if filepath.VolumeName(u.Path[1:]) != "" {
|
||||
u.Path = u.Path[1:]
|
||||
}
|
||||
}
|
||||
|
||||
isLocal, err = isLocalHost(host)
|
||||
if err != nil {
|
||||
return Endpoint{}, err
|
||||
return ep, err
|
||||
}
|
||||
} else {
|
||||
// Only check if the arg is an ip address and ask for scheme since its absent.
|
||||
// localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as
|
||||
// /mnt/export1. So we go ahead and start the minio server in FS modes in these cases.
|
||||
if isHostIPv4(arg) {
|
||||
return ep, fmt.Errorf("invalid URL endpoint format: missing scheme http or https")
|
||||
}
|
||||
u = &url.URL{Path: path.Clean(arg)}
|
||||
isLocal = true
|
||||
}
|
||||
@@ -212,7 +242,6 @@ func NewEndpointList(args ...string) (endpoints EndpointList, err error) {
|
||||
return nil, fmt.Errorf("duplicate endpoints found")
|
||||
}
|
||||
uniqueArgs.Add(arg)
|
||||
|
||||
endpoints = append(endpoints, endpoint)
|
||||
}
|
||||
|
||||
@@ -221,6 +250,22 @@ func NewEndpointList(args ...string) (endpoints EndpointList, err error) {
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// Checks if there are any cross device mounts.
|
||||
func checkCrossDeviceMounts(endpoints EndpointList) (err error) {
|
||||
var absPaths []string
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.IsLocal {
|
||||
var absPath string
|
||||
absPath, err = filepath.Abs(endpoint.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
absPaths = append(absPaths, absPath)
|
||||
}
|
||||
}
|
||||
return mountinfo.CheckCrossDevice(absPaths)
|
||||
}
|
||||
|
||||
// CreateEndpoints - validates and creates new endpoints for given args.
|
||||
func CreateEndpoints(serverAddr string, args ...string) (string, EndpointList, SetupType, error) {
|
||||
var endpoints EndpointList
|
||||
@@ -241,13 +286,16 @@ func CreateEndpoints(serverAddr string, args ...string) (string, EndpointList, S
|
||||
if err != nil {
|
||||
return serverAddr, endpoints, setupType, err
|
||||
}
|
||||
|
||||
if endpoint.Type() != PathEndpointType {
|
||||
return serverAddr, endpoints, setupType, fmt.Errorf("use path style endpoint for FS setup")
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, endpoint)
|
||||
setupType = FSSetupType
|
||||
|
||||
// Check for cross device mounts if any.
|
||||
if err = checkCrossDeviceMounts(endpoints); err != nil {
|
||||
return serverAddr, endpoints, setupType, err
|
||||
}
|
||||
return serverAddr, endpoints, setupType, nil
|
||||
}
|
||||
|
||||
@@ -256,6 +304,11 @@ func CreateEndpoints(serverAddr string, args ...string) (string, EndpointList, S
|
||||
return serverAddr, endpoints, setupType, err
|
||||
}
|
||||
|
||||
// Check for cross device mounts if any.
|
||||
if err = checkCrossDeviceMounts(endpoints); err != nil {
|
||||
return serverAddr, endpoints, setupType, err
|
||||
}
|
||||
|
||||
// Return XL setup when all endpoints are path style.
|
||||
if endpoints[0].Type() == PathEndpointType {
|
||||
setupType = XLSetupType
|
||||
@@ -267,6 +320,7 @@ func CreateEndpoints(serverAddr string, args ...string) (string, EndpointList, S
|
||||
localEndpointCount := 0
|
||||
localServerAddrSet := set.NewStringSet()
|
||||
localPortSet := set.NewStringSet()
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpointPathSet.Add(endpoint.Path)
|
||||
if endpoint.IsLocal {
|
||||
@@ -386,6 +440,29 @@ func CreateEndpoints(serverAddr string, args ...string) (string, EndpointList, S
|
||||
return serverAddr, endpoints, setupType, nil
|
||||
}
|
||||
|
||||
// GetLocalPeer - returns local peer value, returns globalMinioAddr
|
||||
// for FS and Erasure mode. In case of distributed server return
|
||||
// the first element from the set of peers which indicate that
|
||||
// they are local. There is always one entry that is local
|
||||
// even with repeated server endpoints.
|
||||
func GetLocalPeer(endpoints EndpointList) (localPeer string) {
|
||||
peerSet := set.NewStringSet()
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type() != URLEndpointType {
|
||||
continue
|
||||
}
|
||||
if endpoint.IsLocal && endpoint.Host != "" {
|
||||
peerSet.Add(endpoint.Host)
|
||||
}
|
||||
}
|
||||
if peerSet.IsEmpty() {
|
||||
// If local peer is empty can happen in FS or Erasure coded mode.
|
||||
// then set the value to globalMinioAddr instead.
|
||||
return globalMinioAddr
|
||||
}
|
||||
return peerSet.ToSlice()[0]
|
||||
}
|
||||
|
||||
// GetRemotePeers - get hosts information other than this minio service.
|
||||
func GetRemotePeers(endpoints EndpointList) []string {
|
||||
peerSet := set.NewStringSet()
|
||||
|
||||
+37
-18
@@ -20,7 +20,6 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -32,11 +31,6 @@ func TestNewEndpoint(t *testing.T) {
|
||||
u3, _ := url.Parse("http://127.0.0.1:8080/path")
|
||||
u4, _ := url.Parse("http://192.168.253.200/path")
|
||||
|
||||
errMsg := ": no such host"
|
||||
if runtime.GOOS == "windows" {
|
||||
errMsg = ": No such host is known."
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
arg string
|
||||
expectedEndpoint Endpoint
|
||||
@@ -73,7 +67,7 @@ func TestNewEndpoint(t *testing.T) {
|
||||
{"https://93.184.216.34:808080/path", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format: port number must be between 1 to 65535")},
|
||||
{"http://server:8080//", Endpoint{}, -1, fmt.Errorf("empty or root path is not supported in URL endpoint")},
|
||||
{"http://server:8080/", Endpoint{}, -1, fmt.Errorf("empty or root path is not supported in URL endpoint")},
|
||||
{"http://server/path", Endpoint{}, -1, fmt.Errorf("lookup server" + errMsg)},
|
||||
{"192.168.1.210:9000", Endpoint{}, -1, fmt.Errorf("invalid URL endpoint format: missing scheme http or https")},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
@@ -84,16 +78,8 @@ func TestNewEndpoint(t *testing.T) {
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
} else {
|
||||
var match bool
|
||||
if strings.HasSuffix(testCase.expectedErr.Error(), errMsg) {
|
||||
match = strings.HasSuffix(err.Error(), errMsg)
|
||||
} else {
|
||||
match = (testCase.expectedErr.Error() == err.Error())
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
} else if testCase.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
|
||||
if err == nil && !reflect.DeepEqual(testCase.expectedEndpoint, endpoint) {
|
||||
@@ -126,6 +112,7 @@ func TestNewEndpointList(t *testing.T) {
|
||||
{[]string{"ftp://server/d1", "http://server/d2", "http://server/d3", "http://server/d4"}, fmt.Errorf("'ftp://server/d1': invalid URL endpoint format")},
|
||||
{[]string{"d1", "http://localhost/d2", "d3", "d4"}, fmt.Errorf("mixed style endpoints are not supported")},
|
||||
{[]string{"http://example.org/d1", "https://example.com/d1", "http://example.net/d1", "https://example.edut/d1"}, fmt.Errorf("mixed scheme is not supported")},
|
||||
{[]string{"192.168.1.210:9000/tmp/dir0", "192.168.1.210:9000/tmp/dir1", "192.168.1.210:9000/tmp/dir2", "192.168.110:9000/tmp/dir3"}, fmt.Errorf("'192.168.1.210:9000/tmp/dir0': invalid URL endpoint format: missing scheme http or https")},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
@@ -232,7 +219,7 @@ func TestCreateEndpoints(t *testing.T) {
|
||||
expectedSetupType SetupType
|
||||
expectedErr error
|
||||
}{
|
||||
{"localhost", []string{}, "", EndpointList{}, -1, fmt.Errorf("missing port in address localhost")},
|
||||
{"localhost", []string{}, "", EndpointList{}, -1, fmt.Errorf("address localhost: missing port in address")},
|
||||
|
||||
// FS Setup
|
||||
{"localhost:9000", []string{"http://localhost/d1"}, "", EndpointList{}, -1, fmt.Errorf("use path style endpoint for FS setup")},
|
||||
@@ -343,6 +330,38 @@ func TestCreateEndpoints(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Tests get local peer functionality, local peer is supposed to only return one entry per minio service.
|
||||
// So it means that if you have say localhost:9000 and localhost:9001 as endpointArgs then localhost:9001
|
||||
// is considered a remote service from localhost:9000 perspective.
|
||||
func TestGetLocalPeer(t *testing.T) {
|
||||
tempGlobalMinioAddr := globalMinioAddr
|
||||
defer func() {
|
||||
globalMinioAddr = tempGlobalMinioAddr
|
||||
}()
|
||||
globalMinioAddr = ":9000"
|
||||
|
||||
testCases := []struct {
|
||||
endpointArgs []string
|
||||
expectedResult string
|
||||
}{
|
||||
{[]string{"/d1", "/d2", "d3", "d4"}, ":9000"},
|
||||
{[]string{"http://localhost:9000/d1", "http://localhost:9000/d2", "http://example.org:9000/d3", "http://example.com:9000/d4"},
|
||||
"localhost:9000"},
|
||||
{[]string{"http://localhost:9000/d1", "http://example.org:9000/d2", "http://example.com:9000/d3", "http://example.net:9000/d4"},
|
||||
"localhost:9000"},
|
||||
{[]string{"http://localhost:9000/d1", "http://localhost:9001/d2", "http://localhost:9002/d3", "http://localhost:9003/d4"},
|
||||
"localhost:9000"},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
endpoints, _ := NewEndpointList(testCase.endpointArgs...)
|
||||
remotePeer := GetLocalPeer(endpoints)
|
||||
if remotePeer != testCase.expectedResult {
|
||||
t.Fatalf("Test %d: expected: %v, got: %v", i+1, testCase.expectedResult, remotePeer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRemotePeers(t *testing.T) {
|
||||
tempGlobalMinioPort := globalMinioPort
|
||||
defer func() {
|
||||
|
||||
+64
-114
@@ -17,130 +17,80 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"hash"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
)
|
||||
|
||||
// erasureCreateFile - writes an entire stream by erasure coding to
|
||||
// all the disks, writes also calculate individual block's checksum
|
||||
// for future bit-rot protection.
|
||||
func erasureCreateFile(disks []StorageAPI, volume, path string, reader io.Reader, allowEmpty bool, blockSize int64,
|
||||
dataBlocks, parityBlocks int, algo HashAlgo, writeQuorum int) (bytesWritten int64, checkSums []string, err error) {
|
||||
|
||||
// Allocated blockSized buffer for reading from incoming stream.
|
||||
buf := make([]byte, blockSize)
|
||||
|
||||
hashWriters := newHashWriters(len(disks), algo)
|
||||
|
||||
// Read until io.EOF, erasure codes data and writes to all disks.
|
||||
for {
|
||||
var blocks [][]byte
|
||||
n, rErr := io.ReadFull(reader, buf)
|
||||
// FIXME: this is a bug in Golang, n == 0 and err ==
|
||||
// io.ErrUnexpectedEOF for io.ReadFull function.
|
||||
if n == 0 && rErr == io.ErrUnexpectedEOF {
|
||||
return 0, nil, traceError(rErr)
|
||||
}
|
||||
if rErr == io.EOF {
|
||||
// We have reached EOF on the first byte read, io.Reader
|
||||
// must be 0bytes, we don't need to erasure code
|
||||
// data. Will create a 0byte file instead.
|
||||
if bytesWritten == 0 && allowEmpty {
|
||||
blocks = make([][]byte, len(disks))
|
||||
rErr = appendFile(disks, volume, path, blocks, hashWriters, writeQuorum)
|
||||
if rErr != nil {
|
||||
return 0, nil, rErr
|
||||
}
|
||||
} // else we have reached EOF after few reads, no need to
|
||||
// add an additional 0bytes at the end.
|
||||
break
|
||||
}
|
||||
if rErr != nil && rErr != io.ErrUnexpectedEOF {
|
||||
return 0, nil, traceError(rErr)
|
||||
}
|
||||
if n > 0 {
|
||||
// Returns encoded blocks.
|
||||
var enErr error
|
||||
blocks, enErr = encodeData(buf[0:n], dataBlocks, parityBlocks)
|
||||
if enErr != nil {
|
||||
return 0, nil, enErr
|
||||
}
|
||||
|
||||
// Write to all disks.
|
||||
if err = appendFile(disks, volume, path, blocks, hashWriters, writeQuorum); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
bytesWritten += int64(n)
|
||||
}
|
||||
// CreateFile creates a new bitrot encoded file spread over all available disks. CreateFile will create
|
||||
// the file at the given volume and path. It will read from src until an io.EOF occurs. The given algorithm will
|
||||
// be used to protect the erasure encoded file.
|
||||
func (s *ErasureStorage) CreateFile(src io.Reader, volume, path string, buffer []byte, algorithm BitrotAlgorithm, writeQuorum int) (f ErasureFileInfo, err error) {
|
||||
if !algorithm.Available() {
|
||||
return f, traceError(errBitrotHashAlgoInvalid)
|
||||
}
|
||||
f.Checksums = make([][]byte, len(s.disks))
|
||||
hashers := make([]hash.Hash, len(s.disks))
|
||||
for i := range hashers {
|
||||
hashers[i] = algorithm.New()
|
||||
}
|
||||
errChans, errors := make([]chan error, len(s.disks)), make([]error, len(s.disks))
|
||||
for i := range errChans {
|
||||
errChans[i] = make(chan error, 1) // create buffered channel to let finished go-routines die early
|
||||
}
|
||||
|
||||
checkSums = make([]string, len(disks))
|
||||
for i := range checkSums {
|
||||
checkSums[i] = hex.EncodeToString(hashWriters[i].Sum(nil))
|
||||
}
|
||||
return bytesWritten, checkSums, nil
|
||||
}
|
||||
|
||||
// encodeData - encodes incoming data buffer into
|
||||
// dataBlocks+parityBlocks returns a 2 dimensional byte array.
|
||||
func encodeData(dataBuffer []byte, dataBlocks, parityBlocks int) ([][]byte, error) {
|
||||
rs, err := reedsolomon.New(dataBlocks, parityBlocks)
|
||||
if err != nil {
|
||||
return nil, traceError(err)
|
||||
}
|
||||
// Split the input buffer into data and parity blocks.
|
||||
var blocks [][]byte
|
||||
blocks, err = rs.Split(dataBuffer)
|
||||
if err != nil {
|
||||
return nil, traceError(err)
|
||||
var n = len(buffer)
|
||||
for n == len(buffer) {
|
||||
n, err = io.ReadFull(src, buffer)
|
||||
if n == 0 && err == io.EOF {
|
||||
if f.Size != 0 { // don't write empty block if we have written to the disks
|
||||
break
|
||||
}
|
||||
blocks = make([][]byte, len(s.disks)) // write empty block
|
||||
} else if err == nil || (n > 0 && err == io.ErrUnexpectedEOF) {
|
||||
blocks, err = s.ErasureEncode(buffer[:n])
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
} else {
|
||||
return f, traceError(err)
|
||||
}
|
||||
|
||||
for i := range errChans { // span workers
|
||||
go erasureAppendFile(s.disks[i], volume, path, hashers[i], blocks[i], errChans[i])
|
||||
}
|
||||
for i := range errChans { // what until all workers are finished
|
||||
errors[i] = <-errChans[i]
|
||||
}
|
||||
if err = reduceWriteQuorumErrs(errors, objectOpIgnoredErrs, writeQuorum); err != nil {
|
||||
return f, err
|
||||
}
|
||||
s.disks = evalDisks(s.disks, errors)
|
||||
f.Size += int64(n)
|
||||
}
|
||||
|
||||
// Encode parity blocks using data blocks.
|
||||
err = rs.Encode(blocks)
|
||||
if err != nil {
|
||||
return nil, traceError(err)
|
||||
}
|
||||
|
||||
// Return encoded blocks.
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
// appendFile - append data buffer at path.
|
||||
func appendFile(disks []StorageAPI, volume, path string, enBlocks [][]byte, hashWriters []hash.Hash, writeQuorum int) (err error) {
|
||||
var wg = &sync.WaitGroup{}
|
||||
var wErrs = make([]error, len(disks))
|
||||
// Write encoded data to quorum disks in parallel.
|
||||
for index, disk := range disks {
|
||||
if disk == nil {
|
||||
wErrs[index] = traceError(errDiskNotFound)
|
||||
f.Algorithm = algorithm
|
||||
for i, disk := range s.disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
// Write encoded data in routine.
|
||||
go func(index int, disk StorageAPI) {
|
||||
defer wg.Done()
|
||||
wErr := disk.AppendFile(volume, path, enBlocks[index])
|
||||
if wErr != nil {
|
||||
wErrs[index] = traceError(wErr)
|
||||
// Ignore disk which returned an error.
|
||||
disks[index] = nil
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate hash for each blocks.
|
||||
hashWriters[index].Write(enBlocks[index])
|
||||
|
||||
// Successfully wrote.
|
||||
wErrs[index] = nil
|
||||
}(index, disk)
|
||||
f.Checksums[i] = hashers[i].Sum(nil)
|
||||
}
|
||||
|
||||
// Wait for all the appends to finish.
|
||||
wg.Wait()
|
||||
|
||||
return reduceWriteQuorumErrs(wErrs, objectOpIgnoredErrs, writeQuorum)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// erasureAppendFile appends the content of buf to the file on the given disk and updates computes
|
||||
// the hash of the written data. It sends the write error (or nil) over the error channel.
|
||||
func erasureAppendFile(disk StorageAPI, volume, path string, hash hash.Hash, buf []byte, errChan chan<- error) {
|
||||
if disk == OfflineDisk {
|
||||
errChan <- traceError(errDiskNotFound)
|
||||
return
|
||||
}
|
||||
err := disk.AppendFile(volume, path, buf)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
hash.Write(buf)
|
||||
errChan <- err
|
||||
}
|
||||
|
||||
+153
-165
@@ -19,186 +19,174 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/klauspost/reedsolomon"
|
||||
)
|
||||
|
||||
// Simulates a faulty disk for AppendFile()
|
||||
type AppendDiskDown struct {
|
||||
*posix
|
||||
}
|
||||
type badDisk struct{ StorageAPI }
|
||||
|
||||
func (a AppendDiskDown) AppendFile(volume string, path string, buf []byte) error {
|
||||
func (a badDisk) AppendFile(volume string, path string, buf []byte) error {
|
||||
return errFaultyDisk
|
||||
}
|
||||
|
||||
// Test erasureCreateFile()
|
||||
func TestErasureCreateFile(t *testing.T) {
|
||||
// Initialize environment needed for the test.
|
||||
dataBlocks := 7
|
||||
parityBlocks := 7
|
||||
blockSize := int64(blockSizeV1)
|
||||
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer setup.Remove()
|
||||
const oneMiByte = 1 * humanize.MiByte
|
||||
|
||||
disks := setup.disks
|
||||
|
||||
// Prepare a slice of 1MiB with random data.
|
||||
data := make([]byte, 1*humanize.MiByte)
|
||||
_, err = rand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Test when all disks are up.
|
||||
size, _, err := erasureCreateFile(disks, "testbucket", "testobject1", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != int64(len(data)) {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, len(data))
|
||||
}
|
||||
|
||||
// 2 disks down.
|
||||
disks[4] = AppendDiskDown{disks[4].(*posix)}
|
||||
disks[5] = AppendDiskDown{disks[5].(*posix)}
|
||||
|
||||
// Test when two disks are down.
|
||||
size, _, err = erasureCreateFile(disks, "testbucket", "testobject2", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != int64(len(data)) {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, len(data))
|
||||
}
|
||||
|
||||
// 4 more disks down. 6 disks down in total.
|
||||
disks[6] = AppendDiskDown{disks[6].(*posix)}
|
||||
disks[7] = AppendDiskDown{disks[7].(*posix)}
|
||||
disks[8] = AppendDiskDown{disks[8].(*posix)}
|
||||
disks[9] = AppendDiskDown{disks[9].(*posix)}
|
||||
|
||||
size, _, err = erasureCreateFile(disks, "testbucket", "testobject3", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != int64(len(data)) {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, len(data))
|
||||
}
|
||||
|
||||
// 1 more disk down. 7 disk down in total. Should return quorum error.
|
||||
disks[10] = AppendDiskDown{disks[10].(*posix)}
|
||||
_, _, err = erasureCreateFile(disks, "testbucket", "testobject4", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if errorCause(err) != errXLWriteQuorum {
|
||||
t.Errorf("erasureCreateFile return value: expected errXLWriteQuorum, got %s", err)
|
||||
}
|
||||
var erasureCreateFileTests = []struct {
|
||||
dataBlocks int
|
||||
onDisks, offDisks int
|
||||
blocksize, data int64
|
||||
offset int
|
||||
algorithm BitrotAlgorithm
|
||||
shouldFail, shouldFailQuorum bool
|
||||
}{
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 0
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 1, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 1
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 2
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 3
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 4
|
||||
{dataBlocks: 7, onDisks: 14, offDisks: 5, blocksize: int64(blockSizeV1), data: 0, offset: 0, shouldFail: false, algorithm: SHA256, shouldFailQuorum: false}, // 5
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV1), data: 0, offset: 0, shouldFail: false, algorithm: DefaultBitrotAlgorithm, shouldFailQuorum: false}, // 6
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: true}, // 7
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 4, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: SHA256, shouldFail: false, shouldFailQuorum: true}, // 8
|
||||
{dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 9
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 10
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 5, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 102, algorithm: 0, shouldFail: true, shouldFailQuorum: false}, // 12
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(oneMiByte / 2), data: oneMiByte, offset: oneMiByte/2 + 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 14
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(oneMiByte - 1), data: oneMiByte, offset: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 15
|
||||
{dataBlocks: 8, onDisks: 12, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 16
|
||||
{dataBlocks: 8, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17
|
||||
{dataBlocks: 10, onDisks: 14, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 17, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 18
|
||||
{dataBlocks: 2, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 19
|
||||
{dataBlocks: 10, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 20
|
||||
}
|
||||
|
||||
// TestErasureEncode checks for encoding for different data sets.
|
||||
func TestErasureEncode(t *testing.T) {
|
||||
// Collection of cases for encode cases.
|
||||
testEncodeCases := []struct {
|
||||
inputData []byte
|
||||
inputDataBlocks int
|
||||
inputParityBlocks int
|
||||
|
||||
shouldPass bool
|
||||
expectedErr error
|
||||
}{
|
||||
// TestCase - 1.
|
||||
// Regular data encoded.
|
||||
{
|
||||
[]byte("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."),
|
||||
8,
|
||||
8,
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
// TestCase - 2.
|
||||
// Empty data errors out.
|
||||
{
|
||||
[]byte(""),
|
||||
8,
|
||||
8,
|
||||
false,
|
||||
reedsolomon.ErrShortData,
|
||||
},
|
||||
// TestCase - 3.
|
||||
// Single byte encoded.
|
||||
{
|
||||
[]byte("1"),
|
||||
4,
|
||||
4,
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
// TestCase - 4.
|
||||
// test case with negative data block.
|
||||
{
|
||||
[]byte("1"),
|
||||
-1,
|
||||
8,
|
||||
false,
|
||||
reedsolomon.ErrInvShardNum,
|
||||
},
|
||||
// TestCase - 5.
|
||||
// test case with negative parity block.
|
||||
{
|
||||
[]byte("1"),
|
||||
8,
|
||||
-1,
|
||||
false,
|
||||
reedsolomon.ErrInvShardNum,
|
||||
},
|
||||
// TestCase - 6.
|
||||
// test case with zero data block.
|
||||
{
|
||||
[]byte("1"),
|
||||
0,
|
||||
8,
|
||||
false,
|
||||
reedsolomon.ErrInvShardNum,
|
||||
},
|
||||
// TestCase - 7.
|
||||
// test case with zero parity block.
|
||||
{
|
||||
[]byte("1"),
|
||||
8,
|
||||
0,
|
||||
false,
|
||||
reedsolomon.ErrInvShardNum,
|
||||
},
|
||||
// TestCase - 8.
|
||||
// test case with data + parity blocks > 255.
|
||||
// expected to fail with Error Max Shard number.
|
||||
{
|
||||
[]byte("1"),
|
||||
128,
|
||||
128,
|
||||
false,
|
||||
reedsolomon.ErrMaxShardNum,
|
||||
},
|
||||
}
|
||||
|
||||
// Test encode cases.
|
||||
for i, testCase := range testEncodeCases {
|
||||
_, actualErr := encodeData(testCase.inputData, testCase.inputDataBlocks, testCase.inputParityBlocks)
|
||||
if actualErr != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass but failed instead with \"%s\"", i+1, actualErr)
|
||||
func TestErasureCreateFile(t *testing.T) {
|
||||
for i, test := range erasureCreateFileTests {
|
||||
setup, err := newErasureTestSetup(test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
||||
}
|
||||
if actualErr == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to fail with error <Error> \"%v\", but instead passed", i+1, testCase.expectedErr)
|
||||
storage, err := NewErasureStorage(setup.disks, test.dataBlocks, test.onDisks-test.dataBlocks)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if actualErr != nil && !testCase.shouldPass {
|
||||
if errorCause(actualErr) != testCase.expectedErr {
|
||||
t.Errorf("Test %d: Expected Error to be \"%v\", but instead found \"%v\" ", i+1, testCase.expectedErr, actualErr)
|
||||
buffer := make([]byte, test.blocksize, 2*test.blocksize)
|
||||
|
||||
data := make([]byte, test.data)
|
||||
if _, err = io.ReadFull(rand.Reader, data); err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
|
||||
}
|
||||
file, err := storage.CreateFile(bytes.NewReader(data[test.offset:]), "testbucket", "object", buffer, test.algorithm, test.dataBlocks+1)
|
||||
if err != nil && !test.shouldFail {
|
||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||
}
|
||||
if err == nil && test.shouldFail {
|
||||
t.Errorf("Test %d: should fail but it passed", i)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if length := int64(len(data[test.offset:])); file.Size != length {
|
||||
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, file.Size, length)
|
||||
}
|
||||
for j := range storage.disks[:test.offDisks] {
|
||||
storage.disks[j] = badDisk{nil}
|
||||
}
|
||||
if test.offDisks > 0 {
|
||||
storage.disks[0] = OfflineDisk
|
||||
}
|
||||
file, err = storage.CreateFile(bytes.NewReader(data[test.offset:]), "testbucket", "object2", buffer, test.algorithm, test.dataBlocks+1)
|
||||
if err != nil && !test.shouldFailQuorum {
|
||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||
}
|
||||
if err == nil && test.shouldFailQuorum {
|
||||
t.Errorf("Test %d: should fail but it passed", i)
|
||||
}
|
||||
if err == nil {
|
||||
if length := int64(len(data[test.offset:])); file.Size != length {
|
||||
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, file.Size, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
setup.Remove()
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmarks
|
||||
|
||||
func benchmarkErasureWrite(data, parity, dataDown, parityDown int, size int64, b *testing.B) {
|
||||
setup, err := newErasureTestSetup(data, parity, blockSizeV1)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create test setup: %v", err)
|
||||
}
|
||||
defer setup.Remove()
|
||||
storage, err := NewErasureStorage(setup.disks, data, parity)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create ErasureStorage: %v", err)
|
||||
}
|
||||
buffer := make([]byte, blockSizeV1, 2*blockSizeV1)
|
||||
content := make([]byte, size)
|
||||
|
||||
for i := 0; i < dataDown; i++ {
|
||||
storage.disks[i] = OfflineDisk
|
||||
}
|
||||
for i := data; i < data+parityDown; i++ {
|
||||
storage.disks[i] = OfflineDisk
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.SetBytes(size)
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := storage.CreateFile(bytes.NewReader(content), "testbucket", "object", buffer, DefaultBitrotAlgorithm, data+1)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkErasureWriteQuick(b *testing.B) {
|
||||
const size = 12 * 1024 * 1024
|
||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 0, 0, size, b) })
|
||||
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 0, 1, size, b) })
|
||||
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 1, 0, size, b) })
|
||||
}
|
||||
|
||||
func BenchmarkErasureWrite_4_64KB(b *testing.B) {
|
||||
const size = 64 * 1024
|
||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 0, 0, size, b) })
|
||||
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 0, 1, size, b) })
|
||||
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 1, 0, size, b) })
|
||||
}
|
||||
|
||||
func BenchmarkErasureWrite_8_20MB(b *testing.B) {
|
||||
const size = 20 * 1024 * 1024
|
||||
b.Run(" 0000|0000 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 0, 0, size, b) })
|
||||
b.Run(" 0000|X000 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 0, 1, size, b) })
|
||||
b.Run(" X000|0000 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 1, 0, size, b) })
|
||||
b.Run(" 0000|XXX0 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 0, 3, size, b) })
|
||||
b.Run(" XXX0|0000 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 3, 0, size, b) })
|
||||
}
|
||||
|
||||
func BenchmarkErasureWrite_12_30MB(b *testing.B) {
|
||||
const size = 30 * 1024 * 1024
|
||||
b.Run(" 000000|000000 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 0, 0, size, b) })
|
||||
b.Run(" 000000|X00000 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 0, 1, size, b) })
|
||||
b.Run(" X00000|000000 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 1, 0, size, b) })
|
||||
b.Run(" 000000|XXXXX0 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 0, 5, size, b) })
|
||||
b.Run(" XXXXX0|000000 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 5, 0, size, b) })
|
||||
}
|
||||
|
||||
func BenchmarkErasureWrite_16_40MB(b *testing.B) {
|
||||
const size = 40 * 1024 * 1024
|
||||
b.Run(" 00000000|00000000 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 0, 0, size, b) })
|
||||
b.Run(" 00000000|X0000000 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 0, 1, size, b) })
|
||||
b.Run(" X0000000|00000000 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 1, 0, size, b) })
|
||||
b.Run(" 00000000|XXXXXXX0 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 0, 7, size, b) })
|
||||
b.Run(" XXXXXXX0|00000000 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 7, 0, size, b) })
|
||||
}
|
||||
|
||||
+147
-60
@@ -16,71 +16,158 @@
|
||||
|
||||
package cmd
|
||||
|
||||
import "encoding/hex"
|
||||
import (
|
||||
"fmt"
|
||||
"hash"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Heals the erasure coded file. reedsolomon.Reconstruct() is used to reconstruct the missing parts.
|
||||
func erasureHealFile(latestDisks []StorageAPI, outDatedDisks []StorageAPI, volume, path, healBucket, healPath string,
|
||||
size, blockSize int64, dataBlocks, parityBlocks int, algo HashAlgo) (checkSums []string, err error) {
|
||||
// HealFile tries to reconstruct an erasure-coded file spread over all
|
||||
// available disks. HealFile will read the valid parts of the file,
|
||||
// reconstruct the missing data and write the reconstructed parts back
|
||||
// to `staleDisks` at the destination `dstVol/dstPath/`. Parts are
|
||||
// verified against the given BitrotAlgorithm and checksums.
|
||||
//
|
||||
// `staleDisks` is a slice of disks where each non-nil entry has stale
|
||||
// or no data, and so will be healed.
|
||||
//
|
||||
// It is required that `s.disks` have a (read-quorum) majority of
|
||||
// disks with valid data for healing to work.
|
||||
//
|
||||
// In addition, `staleDisks` and `s.disks` must have the same ordering
|
||||
// of disks w.r.t. erasure coding of the object.
|
||||
//
|
||||
// Errors when writing to `staleDisks` are not propagated as long as
|
||||
// writes succeed for at least one disk. This allows partial healing
|
||||
// despite stale disks being faulty.
|
||||
//
|
||||
// It returns bitrot checksums for the non-nil staleDisks on which
|
||||
// healing succeeded.
|
||||
func (s ErasureStorage) HealFile(staleDisks []StorageAPI, volume, path string, blocksize int64,
|
||||
dstVol, dstPath string, size int64, alg BitrotAlgorithm, checksums [][]byte) (
|
||||
f ErasureFileInfo, err error) {
|
||||
|
||||
var offset int64
|
||||
remainingSize := size
|
||||
|
||||
// Hash for bitrot protection.
|
||||
hashWriters := newHashWriters(len(outDatedDisks), bitRotAlgo)
|
||||
|
||||
for remainingSize > 0 {
|
||||
curBlockSize := blockSize
|
||||
if remainingSize < curBlockSize {
|
||||
curBlockSize = remainingSize
|
||||
}
|
||||
|
||||
// Calculate the block size that needs to be read from each disk.
|
||||
curEncBlockSize := getChunkSize(curBlockSize, dataBlocks)
|
||||
|
||||
// Memory for reading data from disks and reconstructing missing data using erasure coding.
|
||||
enBlocks := make([][]byte, len(latestDisks))
|
||||
|
||||
// Read data from the latest disks.
|
||||
// FIXME: no need to read from all the disks. dataBlocks+1 is enough.
|
||||
for index, disk := range latestDisks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
enBlocks[index] = make([]byte, curEncBlockSize)
|
||||
_, err := disk.ReadFile(volume, path, offset, enBlocks[index])
|
||||
if err != nil {
|
||||
enBlocks[index] = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruct missing data.
|
||||
err := decodeData(enBlocks, dataBlocks, parityBlocks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write to the healPath file.
|
||||
for index, disk := range outDatedDisks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
err := disk.AppendFile(healBucket, healPath, enBlocks[index])
|
||||
if err != nil {
|
||||
return nil, traceError(err)
|
||||
}
|
||||
hashWriters[index].Write(enBlocks[index])
|
||||
}
|
||||
remainingSize -= curBlockSize
|
||||
offset += curEncBlockSize
|
||||
if !alg.Available() {
|
||||
return f, traceError(errBitrotHashAlgoInvalid)
|
||||
}
|
||||
|
||||
// Checksums for the bit rot.
|
||||
checkSums = make([]string, len(outDatedDisks))
|
||||
for index, disk := range outDatedDisks {
|
||||
if disk == nil {
|
||||
// Initialization
|
||||
f.Checksums = make([][]byte, len(s.disks))
|
||||
hashers := make([]hash.Hash, len(s.disks))
|
||||
verifiers := make([]*BitrotVerifier, len(s.disks))
|
||||
for i, disk := range s.disks {
|
||||
switch {
|
||||
case staleDisks[i] != nil:
|
||||
hashers[i] = alg.New()
|
||||
case disk == nil:
|
||||
// disregard unavailable disk
|
||||
continue
|
||||
default:
|
||||
verifiers[i] = NewBitrotVerifier(alg, checksums[i])
|
||||
}
|
||||
}
|
||||
writeErrors := make([]error, len(s.disks))
|
||||
|
||||
// Scan part files on disk, block-by-block reconstruct it and
|
||||
// write to stale disks.
|
||||
chunksize := getChunkSize(blocksize, s.dataBlocks)
|
||||
blocks := make([][]byte, len(s.disks))
|
||||
for i := range blocks {
|
||||
blocks[i] = make([]byte, chunksize)
|
||||
}
|
||||
var chunkOffset, blockOffset int64
|
||||
for ; blockOffset < size; blockOffset += blocksize {
|
||||
// last iteration may have less than blocksize data
|
||||
// left, so chunksize needs to be recomputed.
|
||||
if size < blockOffset+blocksize {
|
||||
blocksize = size - blockOffset
|
||||
chunksize = getChunkSize(blocksize, s.dataBlocks)
|
||||
for i := range blocks {
|
||||
blocks[i] = blocks[i][:chunksize]
|
||||
}
|
||||
}
|
||||
// read a chunk from each disk, until we have
|
||||
// `s.dataBlocks` number of chunks set to non-nil in
|
||||
// `blocks`
|
||||
numReads := 0
|
||||
for i, disk := range s.disks {
|
||||
// skip reading from unavailable or stale disks
|
||||
if disk == nil || staleDisks[i] != nil {
|
||||
blocks[i] = blocks[i][:0] // mark shard as missing
|
||||
continue
|
||||
}
|
||||
_, err = disk.ReadFile(volume, path, chunkOffset, blocks[i], verifiers[i])
|
||||
if err != nil {
|
||||
// LOG FIXME: add a conditional log
|
||||
// for read failures, once per-disk
|
||||
// per-function-invocation.
|
||||
blocks[i] = blocks[i][:0] // mark shard as missing
|
||||
continue
|
||||
}
|
||||
numReads++
|
||||
if numReads == s.dataBlocks {
|
||||
// we have enough data to reconstruct
|
||||
// mark all other blocks as missing
|
||||
for j := i + 1; j < len(blocks); j++ {
|
||||
blocks[j] = blocks[j][:0] // mark shard as missing
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// advance the chunk offset to prepare for next loop
|
||||
// iteration
|
||||
chunkOffset += chunksize
|
||||
|
||||
// reconstruct data - this computes all data and parity shards
|
||||
if err = s.ErasureDecodeDataAndParityBlocks(blocks); err != nil {
|
||||
return f, err
|
||||
}
|
||||
|
||||
// write computed shards as chunks on file in each
|
||||
// stale disk
|
||||
writeSucceeded := false
|
||||
for i, disk := range staleDisks {
|
||||
// skip nil disk or disk that had error on
|
||||
// previous write
|
||||
if disk == nil || writeErrors[i] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
writeErrors[i] = disk.AppendFile(dstVol, dstPath, blocks[i])
|
||||
if writeErrors[i] == nil {
|
||||
hashers[i].Write(blocks[i])
|
||||
writeSucceeded = true
|
||||
}
|
||||
}
|
||||
|
||||
// If all disks had write errors we quit.
|
||||
if !writeSucceeded {
|
||||
// build error from all write errors
|
||||
return f, traceError(joinWriteErrors(writeErrors))
|
||||
}
|
||||
}
|
||||
|
||||
// copy computed file hashes into output variable
|
||||
f.Size = size
|
||||
f.Algorithm = alg
|
||||
for i, disk := range staleDisks {
|
||||
if disk == nil || writeErrors[i] != nil {
|
||||
continue
|
||||
}
|
||||
checkSums[index] = hex.EncodeToString(hashWriters[index].Sum(nil))
|
||||
f.Checksums[i] = hashers[i].Sum(nil)
|
||||
}
|
||||
return checkSums, nil
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func joinWriteErrors(errs []error) error {
|
||||
msgs := []string{}
|
||||
for i, err := range errs {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
msgs = append(msgs, fmt.Sprintf("disk %d: %v", i+1, err))
|
||||
}
|
||||
return fmt.Errorf("all stale disks had write errors during healing: %s",
|
||||
strings.Join(msgs, ", "))
|
||||
}
|
||||
|
||||
+110
-96
@@ -19,111 +19,125 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"os"
|
||||
"path"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
// Test erasureHealFile()
|
||||
var erasureHealFileTests = []struct {
|
||||
dataBlocks, disks int
|
||||
|
||||
// number of offline disks is also number of staleDisks for
|
||||
// erasure reconstruction in this test
|
||||
offDisks int
|
||||
|
||||
// bad disks are online disks which return errors
|
||||
badDisks, badStaleDisks int
|
||||
|
||||
blocksize, size int64
|
||||
algorithm BitrotAlgorithm
|
||||
shouldFail bool
|
||||
}{
|
||||
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: SHA256, shouldFail: false}, // 0
|
||||
{dataBlocks: 3, disks: 6, offDisks: 2, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 1
|
||||
{dataBlocks: 4, disks: 8, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 2
|
||||
{dataBlocks: 5, disks: 10, offDisks: 3, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 3
|
||||
{dataBlocks: 6, disks: 12, offDisks: 2, badDisks: 3, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: SHA256, shouldFail: false}, // 4
|
||||
{dataBlocks: 7, disks: 14, offDisks: 4, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 5
|
||||
{dataBlocks: 8, disks: 16, offDisks: 6, badDisks: 1, badStaleDisks: 1, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 6
|
||||
{dataBlocks: 7, disks: 14, offDisks: 2, badDisks: 3, badStaleDisks: 0, blocksize: int64(oneMiByte / 2), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 7
|
||||
{dataBlocks: 6, disks: 12, offDisks: 1, badDisks: 0, badStaleDisks: 1, blocksize: int64(oneMiByte - 1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 8
|
||||
{dataBlocks: 5, disks: 10, offDisks: 3, badDisks: 0, badStaleDisks: 3, blocksize: int64(oneMiByte / 2), size: oneMiByte, algorithm: SHA256, shouldFail: true}, // 9
|
||||
{dataBlocks: 4, disks: 8, offDisks: 1, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 10
|
||||
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 1, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 11
|
||||
{dataBlocks: 6, disks: 12, offDisks: 8, badDisks: 3, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 12
|
||||
{dataBlocks: 7, disks: 14, offDisks: 3, badDisks: 4, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 13
|
||||
{dataBlocks: 7, disks: 14, offDisks: 6, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 14
|
||||
{dataBlocks: 8, disks: 16, offDisks: 4, badDisks: 5, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 15
|
||||
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 16
|
||||
{dataBlocks: 2, disks: 4, offDisks: 0, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: 0, shouldFail: true}, // 17
|
||||
{dataBlocks: 12, disks: 16, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 18
|
||||
{dataBlocks: 6, disks: 8, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 19
|
||||
{dataBlocks: 7, disks: 10, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: 0, shouldFail: true}, // 20
|
||||
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte * 64, algorithm: SHA256, shouldFail: false}, // 21
|
||||
}
|
||||
|
||||
func TestErasureHealFile(t *testing.T) {
|
||||
// Initialize environment needed for the test.
|
||||
dataBlocks := 7
|
||||
parityBlocks := 7
|
||||
blockSize := int64(blockSizeV1)
|
||||
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer setup.Remove()
|
||||
for i, test := range erasureHealFileTests {
|
||||
if test.offDisks < test.badStaleDisks {
|
||||
// test case sanity check
|
||||
t.Fatalf("Test %d: Bad test case - number of stale disks cannot be less than number of badstale disks", i)
|
||||
}
|
||||
|
||||
disks := setup.disks
|
||||
|
||||
// Prepare a slice of 1MiB with random data.
|
||||
data := make([]byte, 1*humanize.MiByte)
|
||||
_, err = rand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Create a test file.
|
||||
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject1", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != int64(len(data)) {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, len(data))
|
||||
}
|
||||
|
||||
latest := make([]StorageAPI, len(disks)) // Slice of latest disks
|
||||
outDated := make([]StorageAPI, len(disks)) // Slice of outdated disks
|
||||
|
||||
// Test case when one part needs to be healed.
|
||||
dataPath := path.Join(setup.diskPaths[0], "testbucket", "testobject1")
|
||||
err = os.Remove(dataPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copy(latest, disks)
|
||||
latest[0] = nil
|
||||
outDated[0] = disks[0]
|
||||
|
||||
healCheckSums, err := erasureHealFile(latest, outDated, "testbucket", "testobject1", "testbucket", "testobject1", 1*humanize.MiByte, blockSize, dataBlocks, parityBlocks, bitRotAlgo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Checksum of the healed file should match.
|
||||
if checkSums[0] != healCheckSums[0] {
|
||||
t.Error("Healing failed, data does not match.")
|
||||
}
|
||||
|
||||
// Test case when parityBlocks number of disks need to be healed.
|
||||
// Should succeed.
|
||||
copy(latest, disks)
|
||||
for index := 0; index < parityBlocks; index++ {
|
||||
dataPath := path.Join(setup.diskPaths[index], "testbucket", "testobject1")
|
||||
err = os.Remove(dataPath)
|
||||
// create some test data
|
||||
setup, err := newErasureTestSetup(test.dataBlocks, test.disks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatalf("Test %d: failed to setup XL environment: %v", i, err)
|
||||
}
|
||||
|
||||
latest[index] = nil
|
||||
outDated[index] = disks[index]
|
||||
}
|
||||
|
||||
healCheckSums, err = erasureHealFile(latest, outDated, "testbucket", "testobject1", "testbucket", "testobject1", 1*humanize.MiByte, blockSize, dataBlocks, parityBlocks, bitRotAlgo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Checksums of the healed files should match.
|
||||
for index := 0; index < parityBlocks; index++ {
|
||||
if checkSums[index] != healCheckSums[index] {
|
||||
t.Error("Healing failed, data does not match.")
|
||||
}
|
||||
}
|
||||
for index := dataBlocks; index < len(disks); index++ {
|
||||
if healCheckSums[index] != "" {
|
||||
t.Errorf("expected healCheckSums[%d] to be empty", index)
|
||||
}
|
||||
}
|
||||
|
||||
// Test case when parityBlocks+1 number of disks need to be healed.
|
||||
// Should fail.
|
||||
copy(latest, disks)
|
||||
for index := 0; index < parityBlocks+1; index++ {
|
||||
dataPath := path.Join(setup.diskPaths[index], "testbucket", "testobject1")
|
||||
err = os.Remove(dataPath)
|
||||
storage, err := NewErasureStorage(setup.disks, test.dataBlocks, test.disks-test.dataBlocks)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||
}
|
||||
data := make([]byte, test.size)
|
||||
if _, err = io.ReadFull(rand.Reader, data); err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
||||
}
|
||||
algorithm := test.algorithm
|
||||
if !algorithm.Available() {
|
||||
algorithm = DefaultBitrotAlgorithm
|
||||
}
|
||||
buffer := make([]byte, test.blocksize, 2*test.blocksize)
|
||||
file, err := storage.CreateFile(bytes.NewReader(data), "testbucket", "testobject", buffer, algorithm, test.dataBlocks+1)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
||||
}
|
||||
|
||||
latest[index] = nil
|
||||
outDated[index] = disks[index]
|
||||
}
|
||||
_, err = erasureHealFile(latest, outDated, "testbucket", "testobject1", "testbucket", "testobject1", 1*humanize.MiByte, blockSize, dataBlocks, parityBlocks, bitRotAlgo)
|
||||
if err == nil {
|
||||
t.Error("Expected erasureHealFile() to fail when the number of available disks <= parityBlocks")
|
||||
// setup stale disks for the test case
|
||||
staleDisks := make([]StorageAPI, len(storage.disks))
|
||||
copy(staleDisks, storage.disks)
|
||||
for j := 0; j < len(storage.disks); j++ {
|
||||
if j < test.offDisks {
|
||||
storage.disks[j] = OfflineDisk
|
||||
} else {
|
||||
staleDisks[j] = nil
|
||||
}
|
||||
}
|
||||
for j := 0; j < test.badDisks; j++ {
|
||||
storage.disks[test.offDisks+j] = badDisk{nil}
|
||||
}
|
||||
for j := 0; j < test.badStaleDisks; j++ {
|
||||
staleDisks[j] = badDisk{nil}
|
||||
}
|
||||
|
||||
// test case setup is complete - now call Healfile()
|
||||
info, err := storage.HealFile(staleDisks, "testbucket", "testobject", test.blocksize, "testbucket", "healedobject", test.size, test.algorithm, file.Checksums)
|
||||
if err != nil && !test.shouldFail {
|
||||
t.Errorf("Test %d: should pass but it failed with: %v", i, err)
|
||||
}
|
||||
if err == nil && test.shouldFail {
|
||||
t.Errorf("Test %d: should fail but it passed", i)
|
||||
}
|
||||
if err == nil {
|
||||
if info.Size != test.size {
|
||||
t.Errorf("Test %d: healed wrong number of bytes: got: #%d want: #%d", i, info.Size, test.size)
|
||||
}
|
||||
if info.Algorithm != test.algorithm {
|
||||
t.Errorf("Test %d: healed with wrong algorithm: got: %v want: %v", i, info.Algorithm, test.algorithm)
|
||||
}
|
||||
// Verify that checksums of staleDisks
|
||||
// match expected values
|
||||
for i, disk := range staleDisks {
|
||||
if disk == nil || info.Checksums[i] == nil {
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(info.Checksums[i], file.Checksums[i]) {
|
||||
t.Errorf("Test %d: heal returned different bitrot checksums", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
setup.Remove()
|
||||
}
|
||||
}
|
||||
|
||||
+111
-297
@@ -17,328 +17,142 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
"github.com/minio/minio/pkg/bpool"
|
||||
)
|
||||
|
||||
// isSuccessDecodeBlocks - do we have all the blocks to be
|
||||
// successfully decoded?. Input encoded blocks ordered matrix.
|
||||
func isSuccessDecodeBlocks(enBlocks [][]byte, dataBlocks int) bool {
|
||||
// Count number of data and parity blocks that were read.
|
||||
var successDataBlocksCount = 0
|
||||
var successParityBlocksCount = 0
|
||||
for index := range enBlocks {
|
||||
if enBlocks[index] == nil {
|
||||
continue
|
||||
}
|
||||
// block index lesser than data blocks, update data block count.
|
||||
if index < dataBlocks {
|
||||
successDataBlocksCount++
|
||||
continue
|
||||
} // else { // update parity block count.
|
||||
successParityBlocksCount++
|
||||
}
|
||||
// Returns true if we have atleast dataBlocks parity.
|
||||
return successDataBlocksCount == dataBlocks || successDataBlocksCount+successParityBlocksCount >= dataBlocks
|
||||
}
|
||||
|
||||
// isSuccessDataBlocks - do we have all the data blocks?
|
||||
// Input encoded blocks ordered matrix.
|
||||
func isSuccessDataBlocks(enBlocks [][]byte, dataBlocks int) bool {
|
||||
// Count number of data blocks that were read.
|
||||
var successDataBlocksCount = 0
|
||||
for index := range enBlocks[:dataBlocks] {
|
||||
if enBlocks[index] == nil {
|
||||
continue
|
||||
}
|
||||
// block index lesser than data blocks, update data block count.
|
||||
if index < dataBlocks {
|
||||
successDataBlocksCount++
|
||||
}
|
||||
}
|
||||
// Returns true if we have atleast the dataBlocks.
|
||||
return successDataBlocksCount >= dataBlocks
|
||||
}
|
||||
|
||||
// Return readable disks slice from which we can read parallelly.
|
||||
func getReadDisks(orderedDisks []StorageAPI, index int, dataBlocks int) (readDisks []StorageAPI, nextIndex int, err error) {
|
||||
readDisks = make([]StorageAPI, len(orderedDisks))
|
||||
dataDisks := 0
|
||||
parityDisks := 0
|
||||
// Count already read data and parity chunks.
|
||||
for i := 0; i < index; i++ {
|
||||
if orderedDisks[i] == nil {
|
||||
continue
|
||||
}
|
||||
if i < dataBlocks {
|
||||
dataDisks++
|
||||
} else {
|
||||
parityDisks++
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity checks - we should never have this situation.
|
||||
if dataDisks == dataBlocks {
|
||||
return nil, 0, traceError(errUnexpected)
|
||||
}
|
||||
if dataDisks+parityDisks >= dataBlocks {
|
||||
return nil, 0, traceError(errUnexpected)
|
||||
}
|
||||
|
||||
// Find the disks from which next set of parallel reads should happen.
|
||||
for i := index; i < len(orderedDisks); i++ {
|
||||
if orderedDisks[i] == nil {
|
||||
continue
|
||||
}
|
||||
if i < dataBlocks {
|
||||
dataDisks++
|
||||
} else {
|
||||
parityDisks++
|
||||
}
|
||||
readDisks[i] = orderedDisks[i]
|
||||
if dataDisks == dataBlocks {
|
||||
return readDisks, i + 1, nil
|
||||
} else if dataDisks+parityDisks == dataBlocks {
|
||||
return readDisks, i + 1, nil
|
||||
}
|
||||
}
|
||||
return nil, 0, traceError(errXLReadQuorum)
|
||||
}
|
||||
|
||||
// parallelRead - reads chunks in parallel from the disks specified in []readDisks.
|
||||
func parallelRead(volume, path string, readDisks, orderedDisks []StorageAPI, enBlocks [][]byte,
|
||||
blockOffset, curChunkSize int64, brVerifiers []bitRotVerifier, pool *bpool.BytePool) {
|
||||
|
||||
// WaitGroup to synchronise the read go-routines.
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
// Read disks in parallel.
|
||||
for index := range readDisks {
|
||||
if readDisks[index] == nil {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
// Reads chunk from readDisk[index] in routine.
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
|
||||
// evaluate if we need to perform bit-rot checking
|
||||
needBitRotVerification := true
|
||||
if brVerifiers[index].isVerified {
|
||||
needBitRotVerification = false
|
||||
// if file has bit-rot, do not reuse disk
|
||||
if brVerifiers[index].hasBitRot {
|
||||
orderedDisks[index] = nil
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
buf, err := pool.Get()
|
||||
if err != nil {
|
||||
errorIf(err, "unable to get buffer from byte pool")
|
||||
orderedDisks[index] = nil
|
||||
return
|
||||
}
|
||||
buf = buf[:curChunkSize]
|
||||
|
||||
if needBitRotVerification {
|
||||
_, err = readDisks[index].ReadFileWithVerify(
|
||||
volume, path, blockOffset, buf,
|
||||
brVerifiers[index].algo,
|
||||
brVerifiers[index].checkSum)
|
||||
} else {
|
||||
_, err = readDisks[index].ReadFile(volume, path,
|
||||
blockOffset, buf)
|
||||
}
|
||||
|
||||
// if bit-rot verification was done, store the
|
||||
// result of verification so we can skip
|
||||
// re-doing it next time
|
||||
if needBitRotVerification {
|
||||
brVerifiers[index].isVerified = true
|
||||
_, ok := err.(hashMismatchError)
|
||||
brVerifiers[index].hasBitRot = ok
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
orderedDisks[index] = nil
|
||||
return
|
||||
}
|
||||
enBlocks[index] = buf
|
||||
}(index)
|
||||
}
|
||||
|
||||
// Waiting for first routines to finish.
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// erasureReadFile - read bytes from erasure coded files and writes to
|
||||
// given writer. Erasure coded files are read block by block as per
|
||||
// given erasureInfo and data chunks are decoded into a data
|
||||
// block. Data block is trimmed for given offset and length, then
|
||||
// written to given writer. This function also supports bit-rot
|
||||
// detection by verifying checksum of individual block's checksum.
|
||||
func erasureReadFile(writer io.Writer, disks []StorageAPI, volume, path string,
|
||||
offset, length, totalLength, blockSize int64, dataBlocks, parityBlocks int,
|
||||
checkSums []string, algo HashAlgo, pool *bpool.BytePool) (int64, error) {
|
||||
|
||||
// Offset and length cannot be negative.
|
||||
// ReadFile reads as much data as requested from the file under the given volume and path and writes the data to the provided writer.
|
||||
// The algorithm and the keys/checksums are used to verify the integrity of the given file. ReadFile will read data from the given offset
|
||||
// up to the given length. If parts of the file are corrupted ReadFile tries to reconstruct the data.
|
||||
func (s ErasureStorage) ReadFile(writer io.Writer, volume, path string, offset, length int64, totalLength int64, checksums [][]byte, algorithm BitrotAlgorithm, blocksize int64) (f ErasureFileInfo, err error) {
|
||||
if offset < 0 || length < 0 {
|
||||
return 0, traceError(errUnexpected)
|
||||
return f, traceError(errUnexpected)
|
||||
}
|
||||
|
||||
// Can't request more data than what is available.
|
||||
if offset+length > totalLength {
|
||||
return 0, traceError(errUnexpected)
|
||||
return f, traceError(errUnexpected)
|
||||
}
|
||||
if !algorithm.Available() {
|
||||
return f, traceError(errBitrotHashAlgoInvalid)
|
||||
}
|
||||
|
||||
// chunkSize is the amount of data that needs to be read from
|
||||
// each disk at a time.
|
||||
chunkSize := getChunkSize(blockSize, dataBlocks)
|
||||
|
||||
brVerifiers := make([]bitRotVerifier, len(disks))
|
||||
for i := range brVerifiers {
|
||||
brVerifiers[i].algo = algo
|
||||
brVerifiers[i].checkSum = checkSums[i]
|
||||
f.Checksums = make([][]byte, len(s.disks))
|
||||
verifiers := make([]*BitrotVerifier, len(s.disks))
|
||||
for i, disk := range s.disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
verifiers[i] = NewBitrotVerifier(algorithm, checksums[i])
|
||||
}
|
||||
errChans := make([]chan error, len(s.disks))
|
||||
for i := range errChans {
|
||||
errChans[i] = make(chan error, 1)
|
||||
}
|
||||
lastBlock := totalLength / blocksize
|
||||
startOffset := offset % blocksize
|
||||
chunksize := getChunkSize(blocksize, s.dataBlocks)
|
||||
|
||||
// Total bytes written to writer
|
||||
var bytesWritten int64
|
||||
blocks := make([][]byte, len(s.disks))
|
||||
for i := range blocks {
|
||||
blocks[i] = make([]byte, chunksize)
|
||||
}
|
||||
for off := offset / blocksize; length > 0; off++ {
|
||||
blockOffset := off * chunksize
|
||||
|
||||
startBlock := offset / blockSize
|
||||
endBlock := (offset + length) / blockSize
|
||||
|
||||
// curChunkSize = chunk size for the current block in the for loop below.
|
||||
// curBlockSize = block size for the current block in the for loop below.
|
||||
// curChunkSize and curBlockSize can change for the last block if totalLength%blockSize != 0
|
||||
curChunkSize := chunkSize
|
||||
curBlockSize := blockSize
|
||||
|
||||
// For each block, read chunk from each disk. If we are able to read all the data disks then we don't
|
||||
// need to read parity disks. If one of the data disk is missing we need to read DataBlocks+1 number
|
||||
// of disks. Once read, we Reconstruct() missing data if needed and write it to the given writer.
|
||||
for block := startBlock; block <= endBlock; block++ {
|
||||
// Mark all buffers as unused at the start of the loop so that the buffers
|
||||
// can be reused.
|
||||
pool.Reset()
|
||||
|
||||
// Each element of enBlocks holds curChunkSize'd amount of data read from its corresponding disk.
|
||||
enBlocks := make([][]byte, len(disks))
|
||||
|
||||
if ((offset + bytesWritten) / blockSize) == (totalLength / blockSize) {
|
||||
// This is the last block for which curBlockSize and curChunkSize can change.
|
||||
// For ex. if totalLength is 15M and blockSize is 10MB, curBlockSize for
|
||||
// the last block should be 5MB.
|
||||
curBlockSize = totalLength % blockSize
|
||||
curChunkSize = getChunkSize(curBlockSize, dataBlocks)
|
||||
}
|
||||
|
||||
// NOTE: That for the offset calculation we have to use chunkSize and
|
||||
// not curChunkSize. If we use curChunkSize for offset calculation
|
||||
// then it can result in wrong offset for the last block.
|
||||
blockOffset := block * chunkSize
|
||||
|
||||
// nextIndex - index from which next set of parallel reads
|
||||
// should happen.
|
||||
nextIndex := 0
|
||||
|
||||
for {
|
||||
// readDisks - disks from which we need to read in parallel.
|
||||
var readDisks []StorageAPI
|
||||
var err error
|
||||
// get readable disks slice from which we can read parallelly.
|
||||
readDisks, nextIndex, err = getReadDisks(disks, nextIndex, dataBlocks)
|
||||
if err != nil {
|
||||
return bytesWritten, err
|
||||
}
|
||||
// Issue a parallel read across the disks specified in readDisks.
|
||||
parallelRead(volume, path, readDisks, disks, enBlocks, blockOffset, curChunkSize, brVerifiers, pool)
|
||||
if isSuccessDecodeBlocks(enBlocks, dataBlocks) {
|
||||
// If enough blocks are available to do rs.Reconstruct()
|
||||
break
|
||||
}
|
||||
if nextIndex == len(disks) {
|
||||
// No more disks to read from.
|
||||
return bytesWritten, traceError(errXLReadQuorum)
|
||||
}
|
||||
// We do not have enough enough data blocks to reconstruct the data
|
||||
// hence continue the for-loop till we have enough data blocks.
|
||||
}
|
||||
|
||||
// If we have all the data blocks no need to decode, continue to write.
|
||||
if !isSuccessDataBlocks(enBlocks, dataBlocks) {
|
||||
// Reconstruct the missing data blocks.
|
||||
if err := decodeData(enBlocks, dataBlocks, parityBlocks); err != nil {
|
||||
return bytesWritten, err
|
||||
if currentBlock := (offset + f.Size) / blocksize; currentBlock == lastBlock {
|
||||
blocksize = totalLength % blocksize
|
||||
chunksize = getChunkSize(blocksize, s.dataBlocks)
|
||||
for i := range blocks {
|
||||
blocks[i] = blocks[i][:chunksize]
|
||||
}
|
||||
}
|
||||
|
||||
// Offset in enBlocks from where data should be read from.
|
||||
var enBlocksOffset int64
|
||||
|
||||
// Total data to be read from enBlocks.
|
||||
enBlocksLength := curBlockSize
|
||||
|
||||
// If this is the start block then enBlocksOffset might not be 0.
|
||||
if block == startBlock {
|
||||
enBlocksOffset = offset % blockSize
|
||||
enBlocksLength -= enBlocksOffset
|
||||
}
|
||||
|
||||
remaining := length - bytesWritten
|
||||
if remaining < enBlocksLength {
|
||||
// We should not send more data than what was requested.
|
||||
enBlocksLength = remaining
|
||||
}
|
||||
|
||||
// Write data blocks.
|
||||
n, err := writeDataBlocks(writer, enBlocks, dataBlocks, enBlocksOffset, enBlocksLength)
|
||||
err = s.readConcurrent(volume, path, blockOffset, blocks, verifiers, errChans)
|
||||
if err != nil {
|
||||
return bytesWritten, err
|
||||
return f, traceError(errXLReadQuorum)
|
||||
}
|
||||
|
||||
// Update total bytes written.
|
||||
bytesWritten += n
|
||||
|
||||
if bytesWritten == length {
|
||||
// Done writing all the requested data.
|
||||
break
|
||||
writeLength := blocksize - startOffset
|
||||
if length < writeLength {
|
||||
writeLength = length
|
||||
}
|
||||
n, err := writeDataBlocks(writer, blocks, s.dataBlocks, startOffset, writeLength)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
startOffset = 0
|
||||
f.Size += n
|
||||
length -= n
|
||||
}
|
||||
|
||||
// Success.
|
||||
return bytesWritten, nil
|
||||
f.Algorithm = algorithm
|
||||
for i, disk := range s.disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
f.Checksums[i] = verifiers[i].Sum(nil)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// decodeData - decode encoded blocks.
|
||||
func decodeData(enBlocks [][]byte, dataBlocks, parityBlocks int) error {
|
||||
// Initialized reedsolomon.
|
||||
rs, err := reedsolomon.New(dataBlocks, parityBlocks)
|
||||
if err != nil {
|
||||
return traceError(err)
|
||||
func erasureCountMissingBlocks(blocks [][]byte, limit int) int {
|
||||
missing := 0
|
||||
for i := range blocks[:limit] {
|
||||
if len(blocks[i]) == 0 {
|
||||
missing++
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
// Reconstruct encoded blocks.
|
||||
err = rs.Reconstruct(enBlocks)
|
||||
if err != nil {
|
||||
return traceError(err)
|
||||
}
|
||||
// readConcurrent reads all requested data concurrently from the disks into blocks. It returns an error if
|
||||
// too many disks failed while reading.
|
||||
func (s *ErasureStorage) readConcurrent(volume, path string, offset int64, blocks [][]byte, verifiers []*BitrotVerifier, errChans []chan error) (err error) {
|
||||
errs := make([]error, len(s.disks))
|
||||
|
||||
// Verify reconstructed blocks (parity).
|
||||
ok, err := rs.Verify(enBlocks)
|
||||
if err != nil {
|
||||
return traceError(err)
|
||||
erasureReadBlocksConcurrent(s.disks[:s.dataBlocks], volume, path, offset, blocks[:s.dataBlocks], verifiers[:s.dataBlocks], errs[:s.dataBlocks], errChans[:s.dataBlocks])
|
||||
missingDataBlocks := erasureCountMissingBlocks(blocks, s.dataBlocks)
|
||||
mustReconstruct := missingDataBlocks > 0
|
||||
if mustReconstruct {
|
||||
requiredReads := s.dataBlocks + missingDataBlocks
|
||||
if requiredReads > s.dataBlocks+s.parityBlocks {
|
||||
return errXLReadQuorum
|
||||
}
|
||||
erasureReadBlocksConcurrent(s.disks[s.dataBlocks:requiredReads], volume, path, offset, blocks[s.dataBlocks:requiredReads], verifiers[s.dataBlocks:requiredReads], errs[s.dataBlocks:requiredReads], errChans[s.dataBlocks:requiredReads])
|
||||
if erasureCountMissingBlocks(blocks, requiredReads) > 0 {
|
||||
erasureReadBlocksConcurrent(s.disks[requiredReads:], volume, path, offset, blocks[requiredReads:], verifiers[requiredReads:], errs[requiredReads:], errChans[requiredReads:])
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
// Blocks cannot be reconstructed, corrupted data.
|
||||
err = errors.New("Verification failed after reconstruction, data likely corrupted")
|
||||
return traceError(err)
|
||||
if err = reduceReadQuorumErrs(errs, []error{}, s.dataBlocks); err != nil {
|
||||
return err
|
||||
}
|
||||
if mustReconstruct {
|
||||
if err = s.ErasureDecodeDataBlocks(blocks); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Success.
|
||||
return nil
|
||||
}
|
||||
|
||||
// erasureReadBlocksConcurrent reads all data from each disk to each data block in parallel.
|
||||
// Therefore disks, blocks, verifiers errors and locks must have the same length.
|
||||
func erasureReadBlocksConcurrent(disks []StorageAPI, volume, path string, offset int64, blocks [][]byte, verifiers []*BitrotVerifier, errors []error, errChans []chan error) {
|
||||
for i := range errChans {
|
||||
go erasureReadFromFile(disks[i], volume, path, offset, blocks[i], verifiers[i], errChans[i])
|
||||
}
|
||||
for i := range errChans {
|
||||
errors[i] = <-errChans[i] // blocks until the go routine 'i' is done - no data race
|
||||
if errors[i] != nil {
|
||||
disks[i] = OfflineDisk
|
||||
blocks[i] = blocks[i][:0] // mark shard as missing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// erasureReadFromFile reads data from the disk to buffer in parallel.
|
||||
// It sends the returned error through the error channel.
|
||||
func erasureReadFromFile(disk StorageAPI, volume, path string, offset int64, buffer []byte, verifier *BitrotVerifier, errChan chan<- error) {
|
||||
if disk == OfflineDisk {
|
||||
errChan <- traceError(errDiskNotFound)
|
||||
return
|
||||
}
|
||||
_, err := disk.ReadFile(volume, path, offset, buffer, verifier)
|
||||
errChan <- err
|
||||
}
|
||||
|
||||
+220
-349
@@ -18,358 +18,142 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
crand "crypto/rand"
|
||||
"io"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"reflect"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio/pkg/bpool"
|
||||
)
|
||||
|
||||
// Tests getReadDisks which returns readable disks slice from which we can
|
||||
// read parallelly.
|
||||
func testGetReadDisks(t *testing.T, xl *xlObjects) {
|
||||
d := xl.storageDisks
|
||||
testCases := []struct {
|
||||
index int // index argument for getReadDisks
|
||||
argDisks []StorageAPI // disks argument for getReadDisks
|
||||
retDisks []StorageAPI // disks return value from getReadDisks
|
||||
nextIndex int // return value from getReadDisks
|
||||
err error // error return value from getReadDisks
|
||||
}{
|
||||
// Test case - 1.
|
||||
// When all disks are available, should return data disks.
|
||||
{
|
||||
0,
|
||||
[]StorageAPI{d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15]},
|
||||
[]StorageAPI{d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], nil, nil, nil, nil, nil, nil, nil, nil},
|
||||
8,
|
||||
nil,
|
||||
},
|
||||
// Test case - 2.
|
||||
// If a parity disk is down, should return all data disks.
|
||||
{
|
||||
0,
|
||||
[]StorageAPI{d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], nil, d[10], d[11], d[12], d[13], d[14], d[15]},
|
||||
[]StorageAPI{d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], nil, nil, nil, nil, nil, nil, nil, nil},
|
||||
8,
|
||||
nil,
|
||||
},
|
||||
// Test case - 3.
|
||||
// If a data disk is down, should return 7 data and 1 parity.
|
||||
{
|
||||
0,
|
||||
[]StorageAPI{nil, d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15]},
|
||||
[]StorageAPI{nil, d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], nil, nil, nil, nil, nil, nil, nil},
|
||||
9,
|
||||
nil,
|
||||
},
|
||||
// Test case - 4.
|
||||
// If 7 data disks are down, should return 1 data and 7 parity.
|
||||
{
|
||||
0,
|
||||
[]StorageAPI{nil, nil, nil, nil, nil, nil, nil, d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15]},
|
||||
[]StorageAPI{nil, nil, nil, nil, nil, nil, nil, d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], nil},
|
||||
15,
|
||||
nil,
|
||||
},
|
||||
// Test case - 5.
|
||||
// When 2 disks fail during parallelRead, next call to getReadDisks should return 3 disks
|
||||
{
|
||||
8,
|
||||
[]StorageAPI{nil, nil, d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15]},
|
||||
[]StorageAPI{nil, nil, nil, nil, nil, nil, nil, nil, d[8], d[9], nil, nil, nil, nil, nil, nil},
|
||||
10,
|
||||
nil,
|
||||
},
|
||||
// Test case - 6.
|
||||
// If 2 disks again fail from the 3 disks returned previously, return next 2 disks
|
||||
{
|
||||
11,
|
||||
[]StorageAPI{nil, nil, d[2], d[3], d[4], d[5], d[6], d[7], nil, nil, d[10], d[11], d[12], d[13], d[14], d[15]},
|
||||
[]StorageAPI{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, d[11], nil, nil, nil, nil},
|
||||
12,
|
||||
nil,
|
||||
},
|
||||
// Test case - 7.
|
||||
// No more disks are available for read, return error
|
||||
{
|
||||
13,
|
||||
[]StorageAPI{nil, nil, d[2], d[3], d[4], d[5], d[6], d[7], nil, nil, d[10], nil, nil, nil, nil, nil},
|
||||
nil,
|
||||
0,
|
||||
errXLReadQuorum,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
disks, nextIndex, err := getReadDisks(test.argDisks, test.index, xl.dataBlocks)
|
||||
if errorCause(err) != test.err {
|
||||
t.Errorf("test-case %d - expected error : %s, got : %s", i+1, test.err, err)
|
||||
continue
|
||||
}
|
||||
if test.nextIndex != nextIndex {
|
||||
t.Errorf("test-case %d - expected nextIndex: %d, got : %d", i+1, test.nextIndex, nextIndex)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(test.retDisks, disks) {
|
||||
t.Errorf("test-case %d : incorrect disks returned. expected %+v, got %+v", i+1, test.retDisks, disks)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test for isSuccessDataBlocks and isSuccessDecodeBlocks.
|
||||
func TestIsSuccessBlocks(t *testing.T) {
|
||||
dataBlocks := 8
|
||||
testCases := []struct {
|
||||
enBlocks [][]byte // data and parity blocks.
|
||||
successData bool // expected return value of isSuccessDataBlocks()
|
||||
successDecode bool // expected return value of isSuccessDecodeBlocks()
|
||||
}{
|
||||
{
|
||||
// When all data and partity blocks are available.
|
||||
[][]byte{
|
||||
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
},
|
||||
true,
|
||||
true,
|
||||
},
|
||||
{
|
||||
// When one data block is not available.
|
||||
[][]byte{
|
||||
nil, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
// When one data and all parity are available, enough for reedsolomon.Reconstruct()
|
||||
[][]byte{
|
||||
nil, nil, nil, nil, nil, nil, nil, {'a'},
|
||||
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
// When all data disks are not available, enough for reedsolomon.Reconstruct()
|
||||
[][]byte{
|
||||
nil, nil, nil, nil, nil, nil, nil, nil,
|
||||
{'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
// Not enough disks for reedsolomon.Reconstruct()
|
||||
[][]byte{
|
||||
nil, nil, nil, nil, nil, nil, nil, nil,
|
||||
nil, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'}, {'a'},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
got := isSuccessDataBlocks(test.enBlocks, dataBlocks)
|
||||
if test.successData != got {
|
||||
t.Errorf("test-case %d : expected %v got %v", i+1, test.successData, got)
|
||||
}
|
||||
got = isSuccessDecodeBlocks(test.enBlocks, dataBlocks)
|
||||
if test.successDecode != got {
|
||||
t.Errorf("test-case %d : expected %v got %v", i+1, test.successDecode, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper function for testGetReadDisks, testShuffleDisks.
|
||||
func TestErasureReadUtils(t *testing.T) {
|
||||
nDisks := 16
|
||||
disks, err := getRandomDisks(nDisks)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
objLayer, _, err := initObjectLayer(mustGetNewEndpointList(disks...))
|
||||
if err != nil {
|
||||
removeRoots(disks)
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer removeRoots(disks)
|
||||
xl := objLayer.(*xlObjects)
|
||||
testGetReadDisks(t, xl)
|
||||
}
|
||||
|
||||
// Simulates a faulty disk for ReadFile()
|
||||
type ReadDiskDown struct {
|
||||
*posix
|
||||
}
|
||||
|
||||
func (r ReadDiskDown) ReadFile(volume string, path string, offset int64, buf []byte) (n int64, err error) {
|
||||
func (d badDisk) ReadFile(volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error) {
|
||||
return 0, errFaultyDisk
|
||||
}
|
||||
|
||||
func (r ReadDiskDown) ReadFileWithVerify(volume string, path string, offset int64, buf []byte,
|
||||
algo HashAlgo, expectedHash string) (n int64, err error) {
|
||||
|
||||
return 0, errFaultyDisk
|
||||
var erasureReadFileTests = []struct {
|
||||
dataBlocks int
|
||||
onDisks, offDisks int
|
||||
blocksize, data int64
|
||||
offset int64
|
||||
length int64
|
||||
algorithm BitrotAlgorithm
|
||||
shouldFail, shouldFailQuorum bool
|
||||
}{
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 0
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 1
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 2
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 1, length: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 3
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte, length: 0, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 4
|
||||
{dataBlocks: 7, onDisks: 14, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: 3, length: 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 5
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(oneMiByte), data: oneMiByte, offset: 4, length: 8 * 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 6
|
||||
{dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte, length: 1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 7
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 6, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 8
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 5, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 9
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 4, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 10
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 12
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 14
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 3, blocksize: int64(2 * oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 15
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 6, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 16
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 2, blocksize: int64(blockSizeV1), data: 2 * oneMiByte, offset: oneMiByte, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 18
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 3, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: SHA256, shouldFail: false, shouldFailQuorum: false}, // 19
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 20
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 21
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 9, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 22
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 23
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 24
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 25
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: 0, shouldFail: true, shouldFailQuorum: false}, // 26
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1) + 1, offset: 0, length: int64(blockSizeV1) + 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 27
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 28
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 29
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 30
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 512, length: int64(blockSizeV1) + 8*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 31
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: int64(blockSizeV1), length: int64(blockSizeV1) - 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 32
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: -1, length: 3, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 33
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: 1024, length: -1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 34
|
||||
{dataBlocks: 4, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1), offset: 0, length: int64(blockSizeV1), algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 35
|
||||
{dataBlocks: 4, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 36
|
||||
{dataBlocks: 4, onDisks: 6, offDisks: 3, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 37
|
||||
{dataBlocks: 8, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 38
|
||||
}
|
||||
|
||||
func TestErasureReadFileDiskFail(t *testing.T) {
|
||||
// Initialize environment needed for the test.
|
||||
dataBlocks := 7
|
||||
parityBlocks := 7
|
||||
blockSize := int64(blockSizeV1)
|
||||
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer setup.Remove()
|
||||
|
||||
disks := setup.disks
|
||||
|
||||
// Prepare a slice of 1humanize.MiByte with random data.
|
||||
data := make([]byte, 1*humanize.MiByte)
|
||||
length := int64(len(data))
|
||||
_, err = rand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a test file to read from.
|
||||
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != length {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, length)
|
||||
}
|
||||
|
||||
// create byte pool which will be used by erasureReadFile for
|
||||
// reading from disks and erasure decoding.
|
||||
chunkSize := getChunkSize(blockSize, dataBlocks)
|
||||
pool := bpool.NewBytePool(chunkSize, len(disks))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", 0, length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !bytes.Equal(buf.Bytes(), data) {
|
||||
t.Error("Contents of the erasure coded file differs")
|
||||
}
|
||||
|
||||
// 2 disks down. Read should succeed.
|
||||
disks[4] = ReadDiskDown{disks[4].(*posix)}
|
||||
disks[5] = ReadDiskDown{disks[5].(*posix)}
|
||||
|
||||
buf.Reset()
|
||||
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", 0, length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !bytes.Equal(buf.Bytes(), data) {
|
||||
t.Error("Contents of the erasure coded file differs")
|
||||
}
|
||||
|
||||
// 4 more disks down. 6 disks down in total. Read should succeed.
|
||||
disks[6] = ReadDiskDown{disks[6].(*posix)}
|
||||
disks[8] = ReadDiskDown{disks[8].(*posix)}
|
||||
disks[9] = ReadDiskDown{disks[9].(*posix)}
|
||||
disks[11] = ReadDiskDown{disks[11].(*posix)}
|
||||
|
||||
buf.Reset()
|
||||
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", 0, length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !bytes.Equal(buf.Bytes(), data) {
|
||||
t.Error("Contents of the erasure coded file differs")
|
||||
}
|
||||
|
||||
// 2 more disk down. 8 disks down in total. Read should fail.
|
||||
disks[12] = ReadDiskDown{disks[12].(*posix)}
|
||||
disks[13] = ReadDiskDown{disks[13].(*posix)}
|
||||
buf.Reset()
|
||||
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", 0, length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
|
||||
if errorCause(err) != errXLReadQuorum {
|
||||
t.Fatal("expected errXLReadQuorum error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErasureReadFileOffsetLength(t *testing.T) {
|
||||
// Initialize environment needed for the test.
|
||||
dataBlocks := 7
|
||||
parityBlocks := 7
|
||||
blockSize := int64(1 * humanize.MiByte)
|
||||
setup, err := newErasureTestSetup(dataBlocks, parityBlocks, blockSize)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
defer setup.Remove()
|
||||
|
||||
disks := setup.disks
|
||||
|
||||
// Prepare a slice of 5humanize.MiByte with random data.
|
||||
data := make([]byte, 5*humanize.MiByte)
|
||||
length := int64(len(data))
|
||||
_, err = rand.Read(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a test file to read from.
|
||||
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != length {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, length)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
offset, length int64
|
||||
}{
|
||||
// Full file.
|
||||
{0, length},
|
||||
// Read nothing.
|
||||
{length, 0},
|
||||
// 2nd block.
|
||||
{blockSize, blockSize},
|
||||
// Test cases for random offsets and lengths.
|
||||
{blockSize - 1, 2},
|
||||
{blockSize - 1, blockSize + 1},
|
||||
{blockSize + 1, blockSize - 1},
|
||||
{blockSize + 1, blockSize},
|
||||
{blockSize + 1, blockSize + 1},
|
||||
{blockSize*2 - 1, blockSize + 1},
|
||||
{length - 1, 1},
|
||||
{length - blockSize, blockSize},
|
||||
{length - blockSize - 1, blockSize},
|
||||
{length - blockSize - 1, blockSize + 1},
|
||||
}
|
||||
chunkSize := getChunkSize(blockSize, dataBlocks)
|
||||
pool := bpool.NewBytePool(chunkSize, len(disks))
|
||||
|
||||
// Compare the data read from file with "data" byte array.
|
||||
for i, testCase := range testCases {
|
||||
expected := data[testCase.offset:(testCase.offset + testCase.length)]
|
||||
buf := &bytes.Buffer{}
|
||||
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", testCase.offset, testCase.length, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
|
||||
func TestErasureReadFile(t *testing.T) {
|
||||
for i, test := range erasureReadFileTests {
|
||||
setup, err := newErasureTestSetup(test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
||||
}
|
||||
got := buf.Bytes()
|
||||
if !bytes.Equal(expected, got) {
|
||||
t.Errorf("Test %d : read data is different from what was expected", i+1)
|
||||
storage, err := NewErasureStorage(setup.disks, test.dataBlocks, test.onDisks-test.dataBlocks)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||
}
|
||||
|
||||
data := make([]byte, test.data)
|
||||
if _, err = io.ReadFull(crand.Reader, data); err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
|
||||
}
|
||||
writeAlgorithm := test.algorithm
|
||||
if !test.algorithm.Available() {
|
||||
writeAlgorithm = DefaultBitrotAlgorithm
|
||||
}
|
||||
buffer := make([]byte, test.blocksize, 2*test.blocksize)
|
||||
file, err := storage.CreateFile(bytes.NewReader(data[:]), "testbucket", "object", buffer, writeAlgorithm, test.dataBlocks+1)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create erasure test file: %v", i, err)
|
||||
}
|
||||
writer := bytes.NewBuffer(nil)
|
||||
readInfo, err := storage.ReadFile(writer, "testbucket", "object", test.offset, test.length, test.data, file.Checksums, test.algorithm, test.blocksize)
|
||||
if err != nil && !test.shouldFail {
|
||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||
}
|
||||
if err == nil && test.shouldFail {
|
||||
t.Errorf("Test %d: should fail but it passed", i)
|
||||
}
|
||||
if err == nil {
|
||||
if readInfo.Size != test.length {
|
||||
t.Errorf("Test %d: read returns wrong number of bytes: got: #%d want: #%d", i, readInfo.Size, test.length)
|
||||
}
|
||||
if readInfo.Algorithm != test.algorithm {
|
||||
t.Errorf("Test %d: read returns wrong algorithm: got: %v want: %v", i, readInfo.Algorithm, test.algorithm)
|
||||
}
|
||||
if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) {
|
||||
t.Errorf("Test %d: read retruns wrong file content", i)
|
||||
}
|
||||
}
|
||||
if err == nil && !test.shouldFail {
|
||||
writer.Reset()
|
||||
for j := range storage.disks[:test.offDisks] {
|
||||
storage.disks[j] = badDisk{nil}
|
||||
}
|
||||
if test.offDisks > 0 {
|
||||
storage.disks[0] = OfflineDisk
|
||||
}
|
||||
readInfo, err = storage.ReadFile(writer, "testbucket", "object", test.offset, test.length, test.data, file.Checksums, test.algorithm, test.blocksize)
|
||||
if err != nil && !test.shouldFailQuorum {
|
||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||
}
|
||||
if err == nil && test.shouldFailQuorum {
|
||||
t.Errorf("Test %d: should fail but it passed", i)
|
||||
}
|
||||
if !test.shouldFailQuorum {
|
||||
if readInfo.Size != test.length {
|
||||
t.Errorf("Test %d: read returns wrong number of bytes: got: #%d want: #%d", i, readInfo.Size, test.length)
|
||||
}
|
||||
if readInfo.Algorithm != test.algorithm {
|
||||
t.Errorf("Test %d: read returns wrong algorithm: got: %v want: %v", i, readInfo.Algorithm, test.algorithm)
|
||||
}
|
||||
if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) {
|
||||
t.Errorf("Test %d: read retruns wrong file content", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
setup.Remove()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,8 +174,10 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
||||
}
|
||||
defer setup.Remove()
|
||||
|
||||
disks := setup.disks
|
||||
|
||||
storage, err := NewErasureStorage(setup.disks, dataBlocks, parityBlocks)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create ErasureStorage: %v", err)
|
||||
}
|
||||
// Prepare a slice of 5MiB with random data.
|
||||
data := make([]byte, 5*humanize.MiByte)
|
||||
length := int64(len(data))
|
||||
@@ -404,22 +190,18 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
||||
iterations := 10000
|
||||
|
||||
// Create a test file to read from.
|
||||
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||
buffer := make([]byte, blockSize, 2*blockSize)
|
||||
file, err := storage.CreateFile(bytes.NewReader(data), "testbucket", "testobject", buffer, DefaultBitrotAlgorithm, dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != length {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", size, length)
|
||||
if file.Size != length {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", file.Size, length)
|
||||
}
|
||||
|
||||
// To generate random offset/length.
|
||||
r := rand.New(rand.NewSource(UTCNow().UnixNano()))
|
||||
|
||||
// create pool buffer which will be used by erasureReadFile for
|
||||
// reading from disks and erasure decoding.
|
||||
chunkSize := getChunkSize(blockSize, dataBlocks)
|
||||
pool := bpool.NewBytePool(chunkSize, len(disks))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
// Verify erasureReadFile() for random offsets and lengths.
|
||||
@@ -429,7 +211,7 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
||||
|
||||
expected := data[offset : offset+readLen]
|
||||
|
||||
_, err = erasureReadFile(buf, disks, "testbucket", "testobject", offset, readLen, length, blockSize, dataBlocks, parityBlocks, checkSums, bitRotAlgo, pool)
|
||||
_, err = storage.ReadFile(buf, "testbucket", "testobject", offset, readLen, length, file.Checksums, DefaultBitrotAlgorithm, blockSize)
|
||||
if err != nil {
|
||||
t.Fatal(err, offset, readLen)
|
||||
}
|
||||
@@ -440,3 +222,92 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmarks
|
||||
|
||||
func benchmarkErasureRead(data, parity, dataDown, parityDown int, size int64, b *testing.B) {
|
||||
setup, err := newErasureTestSetup(data, parity, blockSizeV1)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create test setup: %v", err)
|
||||
}
|
||||
defer setup.Remove()
|
||||
storage, err := NewErasureStorage(setup.disks, data, parity)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create ErasureStorage: %v", err)
|
||||
}
|
||||
|
||||
content := make([]byte, size)
|
||||
buffer := make([]byte, blockSizeV1, 2*blockSizeV1)
|
||||
file, err := storage.CreateFile(bytes.NewReader(content), "testbucket", "object", buffer, DefaultBitrotAlgorithm, data+1)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create erasure test file: %v", err)
|
||||
}
|
||||
checksums := file.Checksums
|
||||
|
||||
for i := 0; i < dataDown; i++ {
|
||||
storage.disks[i] = OfflineDisk
|
||||
}
|
||||
for i := data; i < data+parityDown; i++ {
|
||||
storage.disks[i] = OfflineDisk
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.SetBytes(size)
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if file, err = storage.ReadFile(bytes.NewBuffer(content[:0]), "testbucket", "object", 0, size, size, checksums, DefaultBitrotAlgorithm, blockSizeV1); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkErasureReadQuick(b *testing.B) {
|
||||
const size = 12 * 1024 * 1024
|
||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 0, size, b) })
|
||||
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 1, size, b) })
|
||||
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 1, 0, size, b) })
|
||||
b.Run(" X0|X0 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 1, 1, size, b) })
|
||||
}
|
||||
|
||||
func BenchmarkErasureRead_4_64KB(b *testing.B) {
|
||||
const size = 64 * 1024
|
||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 0, size, b) })
|
||||
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 1, size, b) })
|
||||
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 1, 0, size, b) })
|
||||
b.Run(" X0|X0 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 1, 1, size, b) })
|
||||
b.Run(" 00|XX ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 2, size, b) })
|
||||
b.Run(" XX|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 2, 0, size, b) })
|
||||
}
|
||||
|
||||
func BenchmarkErasureRead_8_20MB(b *testing.B) {
|
||||
const size = 20 * 1024 * 1024
|
||||
b.Run(" 0000|0000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 0, 0, size, b) })
|
||||
b.Run(" 0000|X000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 0, 1, size, b) })
|
||||
b.Run(" X000|0000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 1, 0, size, b) })
|
||||
b.Run(" X000|X000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 1, 1, size, b) })
|
||||
b.Run(" 0000|XXXX ", func(b *testing.B) { benchmarkErasureRead(4, 4, 0, 4, size, b) })
|
||||
b.Run(" XX00|XX00 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 2, 2, size, b) })
|
||||
b.Run(" XXXX|0000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 4, 0, size, b) })
|
||||
}
|
||||
|
||||
func BenchmarkErasureRead_12_30MB(b *testing.B) {
|
||||
const size = 30 * 1024 * 1024
|
||||
b.Run(" 000000|000000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 0, 0, size, b) })
|
||||
b.Run(" 000000|X00000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 0, 1, size, b) })
|
||||
b.Run(" X00000|000000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 1, 0, size, b) })
|
||||
b.Run(" X00000|X00000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 1, 1, size, b) })
|
||||
b.Run(" 000000|XXXXXX ", func(b *testing.B) { benchmarkErasureRead(6, 6, 0, 6, size, b) })
|
||||
b.Run(" XXX000|XXX000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 3, 3, size, b) })
|
||||
b.Run(" XXXXXX|000000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 6, 0, size, b) })
|
||||
}
|
||||
|
||||
func BenchmarkErasureRead_16_40MB(b *testing.B) {
|
||||
const size = 40 * 1024 * 1024
|
||||
b.Run(" 00000000|00000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 0, 0, size, b) })
|
||||
b.Run(" 00000000|X0000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 0, 1, size, b) })
|
||||
b.Run(" X0000000|00000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 1, 0, size, b) })
|
||||
b.Run(" X0000000|X0000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 1, 1, size, b) })
|
||||
b.Run(" 00000000|XXXXXXXX ", func(b *testing.B) { benchmarkErasureRead(8, 8, 0, 8, size, b) })
|
||||
b.Run(" XXXX0000|XXXX0000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 4, 4, size, b) })
|
||||
b.Run(" XXXXXXXX|00000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 8, 0, size, b) })
|
||||
}
|
||||
|
||||
@@ -18,72 +18,11 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"hash"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
"github.com/minio/sha256-simd"
|
||||
"golang.org/x/crypto/blake2b"
|
||||
)
|
||||
|
||||
// newHashWriters - inititialize a slice of hashes for the disk count.
|
||||
func newHashWriters(diskCount int, algo HashAlgo) []hash.Hash {
|
||||
hashWriters := make([]hash.Hash, diskCount)
|
||||
for index := range hashWriters {
|
||||
hashWriters[index] = newHash(algo)
|
||||
}
|
||||
return hashWriters
|
||||
}
|
||||
|
||||
// newHash - gives you a newly allocated hash depending on the input algorithm.
|
||||
func newHash(algo HashAlgo) (h hash.Hash) {
|
||||
switch algo {
|
||||
case HashSha256:
|
||||
// sha256 checksum specially on ARM64 platforms or whenever
|
||||
// requested as dictated by `xl.json` entry.
|
||||
h = sha256.New()
|
||||
case HashBlake2b:
|
||||
// ignore the error, because New512 without a key never fails
|
||||
// New512 only returns a non-nil error, if the length of the passed
|
||||
// key > 64 bytes - but we use blake2b as hash function (no key)
|
||||
h, _ = blake2b.New512(nil)
|
||||
// Add new hashes here.
|
||||
default:
|
||||
// Default to blake2b.
|
||||
// ignore the error, because New512 without a key never fails
|
||||
// New512 only returns a non-nil error, if the length of the passed
|
||||
// key > 64 bytes - but we use blake2b as hash function (no key)
|
||||
h, _ = blake2b.New512(nil)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Hash buffer pool is a pool of reusable
|
||||
// buffers used while checksumming a stream.
|
||||
var hashBufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
b := make([]byte, readSizeV1)
|
||||
return &b
|
||||
},
|
||||
}
|
||||
|
||||
// hashSum calculates the hash of the entire path and returns.
|
||||
func hashSum(disk StorageAPI, volume, path string, writer hash.Hash) ([]byte, error) {
|
||||
// Fetch a new staging buffer from the pool.
|
||||
bufp := hashBufferPool.Get().(*[]byte)
|
||||
defer hashBufferPool.Put(bufp)
|
||||
|
||||
// Copy entire buffer to writer.
|
||||
if err := copyBuffer(writer, disk, volume, path, *bufp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return the final hash sum.
|
||||
return writer.Sum(nil), nil
|
||||
}
|
||||
|
||||
// getDataBlockLen - get length of data blocks from encoded blocks.
|
||||
func getDataBlockLen(enBlocks [][]byte, dataBlocks int) int {
|
||||
size := 0
|
||||
@@ -166,57 +105,3 @@ func writeDataBlocks(dst io.Writer, enBlocks [][]byte, dataBlocks int, offset in
|
||||
func getChunkSize(blockSize int64, dataBlocks int) int64 {
|
||||
return (blockSize + int64(dataBlocks) - 1) / int64(dataBlocks)
|
||||
}
|
||||
|
||||
// copyBuffer - copies from disk, volume, path to input writer until either EOF
|
||||
// is reached at volume, path or an error occurs. A success copyBuffer returns
|
||||
// err == nil, not err == EOF. Because copyBuffer is defined to read from path
|
||||
// until EOF. It does not treat an EOF from ReadFile an error to be reported.
|
||||
// Additionally copyBuffer stages through the provided buffer; otherwise if it
|
||||
// has zero length, returns error.
|
||||
func copyBuffer(writer io.Writer, disk StorageAPI, volume string, path string, buf []byte) error {
|
||||
// Error condition of zero length buffer.
|
||||
if buf != nil && len(buf) == 0 {
|
||||
return errors.New("empty buffer in readBuffer")
|
||||
}
|
||||
|
||||
// Starting offset for Reading the file.
|
||||
var startOffset int64
|
||||
|
||||
// Read until io.EOF.
|
||||
for {
|
||||
n, err := disk.ReadFile(volume, path, startOffset, buf)
|
||||
if n > 0 {
|
||||
m, wErr := writer.Write(buf[:n])
|
||||
if wErr != nil {
|
||||
return wErr
|
||||
}
|
||||
if int64(m) != n {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
// Progress the offset.
|
||||
startOffset += n
|
||||
}
|
||||
|
||||
// Success.
|
||||
return nil
|
||||
}
|
||||
|
||||
// bitRotVerifier - type representing bit-rot verification process for
|
||||
// a single under-lying object (currently whole files)
|
||||
type bitRotVerifier struct {
|
||||
// has the bit-rot verification been done?
|
||||
isVerified bool
|
||||
// is the data free of bit-rot?
|
||||
hasBitRot bool
|
||||
// hashing algorithm
|
||||
algo HashAlgo
|
||||
// hex-encoded expected raw-hash value
|
||||
checkSum string
|
||||
}
|
||||
|
||||
+1
-108
@@ -16,22 +16,7 @@
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test validates the number hash writers returned.
|
||||
func TestNewHashWriters(t *testing.T) {
|
||||
diskNum := 8
|
||||
hashWriters := newHashWriters(diskNum, bitRotAlgo)
|
||||
if len(hashWriters) != diskNum {
|
||||
t.Errorf("Expected %d hashWriters, but instead got %d", diskNum, len(hashWriters))
|
||||
}
|
||||
|
||||
}
|
||||
import "testing"
|
||||
|
||||
// Tests validate the output of getChunkSize.
|
||||
func TestGetChunkSize(t *testing.T) {
|
||||
@@ -66,95 +51,3 @@ func TestGetChunkSize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCopyBuffer - Tests validate the result and errors produced when `copyBuffer` is called with sample inputs.
|
||||
func TestCopyBuffer(t *testing.T) {
|
||||
// create posix test setup
|
||||
disk, diskPath, err := newPosixTestSetup()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create posix test setup, %s", err)
|
||||
}
|
||||
defer removeAll(diskPath)
|
||||
|
||||
volume := "success-vol"
|
||||
// Setup test environment.
|
||||
if err = disk.MakeVol(volume); err != nil {
|
||||
t.Fatalf("Unable to create volume, %s", err)
|
||||
}
|
||||
|
||||
// creating io.Writer for the input to copyBuffer.
|
||||
buffers := []*bytes.Buffer{
|
||||
new(bytes.Buffer),
|
||||
new(bytes.Buffer),
|
||||
new(bytes.Buffer),
|
||||
}
|
||||
|
||||
testFile := "testFile"
|
||||
testContent := []byte("hello, world")
|
||||
err = disk.AppendFile(volume, testFile, testContent)
|
||||
if err != nil {
|
||||
t.Fatalf("AppendFile failed: <ERROR> %s", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
writer io.Writer
|
||||
disk StorageAPI
|
||||
volume string
|
||||
path string
|
||||
buf []byte
|
||||
|
||||
expectedResult []byte
|
||||
// flag to indicate whether test case should pass.
|
||||
shouldPass bool
|
||||
expectedErr error
|
||||
}{
|
||||
// Test case - 1.
|
||||
// case with empty buffer.
|
||||
{nil, nil, "", "", []byte{}, nil, false, errors.New("empty buffer in readBuffer")},
|
||||
// Test case - 2.
|
||||
// Test case with empty volume.
|
||||
{buffers[0], disk, "", "", make([]byte, 5), nil, false, errInvalidArgument},
|
||||
// Test case - 3.
|
||||
// Test case with non existent volume.
|
||||
{buffers[0], disk, "abc", "", make([]byte, 5), nil, false, errVolumeNotFound},
|
||||
// Test case - 4.
|
||||
// Test case with empty filename/path.
|
||||
{buffers[0], disk, volume, "", make([]byte, 5), nil, false, errIsNotRegular},
|
||||
// Test case - 5.
|
||||
// Test case with non existent file name.
|
||||
{buffers[0], disk, volume, "abcd", make([]byte, 5), nil, false, errFileNotFound},
|
||||
// Test case - 6.
|
||||
// Test case where the writer returns EOF.
|
||||
{NewEOFWriter(buffers[0], 3), disk, volume, testFile, make([]byte, 5), nil, false, io.EOF},
|
||||
// Test case - 7.
|
||||
// Test case to produce io.ErrShortWrite, the TruncateWriter returns after writing 3 bytes.
|
||||
{TruncateWriter(buffers[1], 3), disk, volume, testFile, make([]byte, 5), nil, false, io.ErrShortWrite},
|
||||
// Teset case - 8.
|
||||
// Valid case, expected to read till EOF and write the contents into the writer.
|
||||
{buffers[2], disk, volume, testFile, make([]byte, 5), testContent, true, nil},
|
||||
}
|
||||
// iterate over the test cases and call copy Buffer with data.
|
||||
for i, testCase := range testCases {
|
||||
actualErr := copyBuffer(testCase.writer, testCase.disk, testCase.volume, testCase.path, testCase.buf)
|
||||
|
||||
if actualErr != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass but failed instead with \"%s\"", i+1, actualErr)
|
||||
}
|
||||
|
||||
if actualErr == nil && !testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to fail with error <Error> \"%v\", but instead passed", i+1, testCase.expectedErr)
|
||||
}
|
||||
// Failed as expected, but does it fail for the expected reason.
|
||||
if actualErr != nil && !testCase.shouldPass {
|
||||
if testCase.expectedErr.Error() != actualErr.Error() {
|
||||
t.Errorf("Test %d: Expected Error to be \"%v\", but instead found \"%v\" ", i+1, testCase.expectedErr, actualErr)
|
||||
}
|
||||
}
|
||||
// test passed as expected, asserting the result.
|
||||
if actualErr == nil && testCase.shouldPass {
|
||||
if !bytes.Equal(testCase.expectedResult, buffers[2].Bytes()) {
|
||||
t.Errorf("Test %d: copied buffer differs from the expected one.", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"hash"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
)
|
||||
|
||||
// OfflineDisk represents an unavailable disk.
|
||||
var OfflineDisk StorageAPI // zero value is nil
|
||||
|
||||
// ErasureFileInfo contains information about an erasure file operation (create, read, heal).
|
||||
type ErasureFileInfo struct {
|
||||
Size int64
|
||||
Algorithm BitrotAlgorithm
|
||||
Checksums [][]byte
|
||||
}
|
||||
|
||||
// ErasureStorage represents an array of disks.
|
||||
// The disks contain erasure coded and bitrot-protected data.
|
||||
type ErasureStorage struct {
|
||||
disks []StorageAPI
|
||||
erasure reedsolomon.Encoder
|
||||
dataBlocks, parityBlocks int
|
||||
}
|
||||
|
||||
// NewErasureStorage creates a new ErasureStorage. The storage erasure codes and protects all data written to
|
||||
// the disks.
|
||||
func NewErasureStorage(disks []StorageAPI, dataBlocks, parityBlocks int) (s ErasureStorage, err error) {
|
||||
erasure, err := reedsolomon.New(dataBlocks, parityBlocks)
|
||||
if err != nil {
|
||||
return s, traceErrorf("failed to create erasure coding: %v", err)
|
||||
}
|
||||
s = ErasureStorage{
|
||||
disks: make([]StorageAPI, len(disks)),
|
||||
erasure: erasure,
|
||||
dataBlocks: dataBlocks,
|
||||
parityBlocks: parityBlocks,
|
||||
}
|
||||
copy(s.disks, disks)
|
||||
return
|
||||
}
|
||||
|
||||
// ErasureEncode encodes the given data and returns the erasure-coded data.
|
||||
// It returns an error if the erasure coding failed.
|
||||
func (s *ErasureStorage) ErasureEncode(data []byte) ([][]byte, error) {
|
||||
encoded, err := s.erasure.Split(data)
|
||||
if err != nil {
|
||||
return nil, traceErrorf("failed to split data: %v", err)
|
||||
}
|
||||
if err = s.erasure.Encode(encoded); err != nil {
|
||||
return nil, traceErrorf("failed to encode data: %v", err)
|
||||
}
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
// ErasureDecodeDataBlocks decodes the given erasure-coded data.
|
||||
// It only decodes the data blocks but does not verify them.
|
||||
// It returns an error if the decoding failed.
|
||||
func (s *ErasureStorage) ErasureDecodeDataBlocks(data [][]byte) error {
|
||||
if err := s.erasure.ReconstructData(data); err != nil {
|
||||
return traceErrorf("failed to reconstruct data: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErasureDecodeDataAndParityBlocks decodes the given erasure-coded data and verifies it.
|
||||
// It returns an error if the decoding failed.
|
||||
func (s *ErasureStorage) ErasureDecodeDataAndParityBlocks(data [][]byte) error {
|
||||
if err := s.erasure.Reconstruct(data); err != nil {
|
||||
return traceErrorf("failed to reconstruct data: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewBitrotVerifier returns a new BitrotVerifier implementing the given algorithm.
|
||||
func NewBitrotVerifier(algorithm BitrotAlgorithm, checksum []byte) *BitrotVerifier {
|
||||
return &BitrotVerifier{algorithm.New(), algorithm, checksum, false}
|
||||
}
|
||||
|
||||
// BitrotVerifier can be used to verify protected data.
|
||||
type BitrotVerifier struct {
|
||||
hash.Hash
|
||||
|
||||
algorithm BitrotAlgorithm
|
||||
sum []byte
|
||||
verified bool
|
||||
}
|
||||
|
||||
// Verify returns true iff the computed checksum of the verifier matches the the checksum provided when the verifier
|
||||
// was created.
|
||||
func (v *BitrotVerifier) Verify() bool {
|
||||
v.verified = true
|
||||
return subtle.ConstantTimeCompare(v.Sum(nil), v.sum) == 1
|
||||
}
|
||||
|
||||
// IsVerified returns true iff Verify was called at least once.
|
||||
func (v *BitrotVerifier) IsVerified() bool { return v.verified }
|
||||
+71
-112
@@ -18,132 +18,91 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mustEncodeData - encodes data slice and provides encoded 2 dimensional slice.
|
||||
func mustEncodeData(data []byte, dataBlocks, parityBlocks int) [][]byte {
|
||||
encodedData, err := encodeData(data, dataBlocks, parityBlocks)
|
||||
if err != nil {
|
||||
// Upon failure panic this function.
|
||||
panic(err)
|
||||
}
|
||||
return encodedData
|
||||
var erasureDecodeTests = []struct {
|
||||
dataBlocks, parityBlocks int
|
||||
missingData, missingParity int
|
||||
reconstructParity bool
|
||||
shouldFail bool
|
||||
}{
|
||||
{dataBlocks: 2, parityBlocks: 2, missingData: 0, missingParity: 0, reconstructParity: true, shouldFail: false},
|
||||
{dataBlocks: 3, parityBlocks: 3, missingData: 1, missingParity: 0, reconstructParity: true, shouldFail: false},
|
||||
{dataBlocks: 4, parityBlocks: 4, missingData: 2, missingParity: 0, reconstructParity: false, shouldFail: false},
|
||||
{dataBlocks: 5, parityBlocks: 5, missingData: 0, missingParity: 1, reconstructParity: true, shouldFail: false},
|
||||
{dataBlocks: 6, parityBlocks: 6, missingData: 0, missingParity: 2, reconstructParity: true, shouldFail: false},
|
||||
{dataBlocks: 7, parityBlocks: 7, missingData: 1, missingParity: 1, reconstructParity: false, shouldFail: false},
|
||||
{dataBlocks: 8, parityBlocks: 8, missingData: 3, missingParity: 2, reconstructParity: false, shouldFail: false},
|
||||
{dataBlocks: 2, parityBlocks: 2, missingData: 2, missingParity: 1, reconstructParity: true, shouldFail: true},
|
||||
{dataBlocks: 4, parityBlocks: 2, missingData: 2, missingParity: 2, reconstructParity: false, shouldFail: true},
|
||||
{dataBlocks: 8, parityBlocks: 4, missingData: 2, missingParity: 2, reconstructParity: false, shouldFail: false},
|
||||
}
|
||||
|
||||
// Generates good encoded data with one parity block and data block missing.
|
||||
func getGoodEncodedData(data []byte, dataBlocks, parityBlocks int) [][]byte {
|
||||
encodedData := mustEncodeData(data, dataBlocks, parityBlocks)
|
||||
encodedData[3] = nil
|
||||
encodedData[1] = nil
|
||||
return encodedData
|
||||
}
|
||||
|
||||
// Generates bad encoded data with one parity block and data block with garbage data.
|
||||
func getBadEncodedData(data []byte, dataBlocks, parityBlocks int) [][]byte {
|
||||
encodedData := mustEncodeData(data, dataBlocks, parityBlocks)
|
||||
encodedData[3] = []byte("garbage")
|
||||
encodedData[1] = []byte("garbage")
|
||||
return encodedData
|
||||
}
|
||||
|
||||
// Generates encoded data with all data blocks missing.
|
||||
func getMissingData(data []byte, dataBlocks, parityBlocks int) [][]byte {
|
||||
encodedData := mustEncodeData(data, dataBlocks, parityBlocks)
|
||||
for i := 0; i < dataBlocks+1; i++ {
|
||||
encodedData[i] = nil
|
||||
}
|
||||
return encodedData
|
||||
}
|
||||
|
||||
// Generates encoded data with less number of blocks than expected data blocks.
|
||||
func getInsufficientData(data []byte, dataBlocks, parityBlocks int) [][]byte {
|
||||
encodedData := mustEncodeData(data, dataBlocks, parityBlocks)
|
||||
// Return half of the data.
|
||||
return encodedData[:dataBlocks/2]
|
||||
}
|
||||
|
||||
// Represents erasure encoding matrix dataBlocks and paritBlocks.
|
||||
type encodingMatrix struct {
|
||||
dataBlocks int
|
||||
parityBlocks int
|
||||
}
|
||||
|
||||
// List of encoding matrices the tests will run on.
|
||||
var encodingMatrices = []encodingMatrix{
|
||||
{3, 3}, // 3 data, 3 parity blocks.
|
||||
{4, 4}, // 4 data, 4 parity blocks.
|
||||
{5, 5}, // 5 data, 5 parity blocks.
|
||||
{6, 6}, // 6 data, 6 parity blocks.
|
||||
{7, 7}, // 7 data, 7 parity blocks.
|
||||
{8, 8}, // 8 data, 8 parity blocks.
|
||||
}
|
||||
|
||||
// Tests erasure decoding functionality for various types of inputs.
|
||||
func TestErasureDecode(t *testing.T) {
|
||||
data := []byte("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.")
|
||||
|
||||
// List of decoding cases
|
||||
// - validates bad encoded data.
|
||||
// - validates good encoded data.
|
||||
// - validates insufficient data.
|
||||
testDecodeCases := []struct {
|
||||
enFn func([]byte, int, int) [][]byte
|
||||
shouldPass bool
|
||||
}{
|
||||
// Generates bad encoded data.
|
||||
{
|
||||
enFn: getBadEncodedData,
|
||||
shouldPass: false,
|
||||
},
|
||||
// Generates good encoded data.
|
||||
{
|
||||
enFn: getGoodEncodedData,
|
||||
shouldPass: true,
|
||||
},
|
||||
// Generates missing data.
|
||||
{
|
||||
enFn: getMissingData,
|
||||
shouldPass: false,
|
||||
},
|
||||
// Generates short data.
|
||||
{
|
||||
enFn: getInsufficientData,
|
||||
shouldPass: false,
|
||||
},
|
||||
data := make([]byte, 256)
|
||||
if _, err := io.ReadFull(rand.Reader, data); err != nil {
|
||||
t.Fatalf("Failed to read random data: %v", err)
|
||||
}
|
||||
for i, test := range erasureDecodeTests {
|
||||
buffer := make([]byte, len(data), 2*len(data))
|
||||
copy(buffer, data)
|
||||
|
||||
// Validates all decode tests.
|
||||
for i, testCase := range testDecodeCases {
|
||||
for _, encodingMatrix := range encodingMatrices {
|
||||
disks := make([]StorageAPI, test.dataBlocks+test.parityBlocks)
|
||||
storage, err := NewErasureStorage(disks, test.dataBlocks, test.parityBlocks)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to create erasure storage: %v", i, err)
|
||||
}
|
||||
encoded, err := storage.ErasureEncode(buffer)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to encode data: %v", i, err)
|
||||
}
|
||||
|
||||
// Encoding matrix.
|
||||
dataBlocks := encodingMatrix.dataBlocks
|
||||
parityBlocks := encodingMatrix.parityBlocks
|
||||
for j := range encoded[:test.missingData] {
|
||||
encoded[j] = nil
|
||||
}
|
||||
for j := test.dataBlocks; j < test.dataBlocks+test.missingParity; j++ {
|
||||
encoded[j] = nil
|
||||
}
|
||||
|
||||
// Data block size.
|
||||
blockSize := len(data)
|
||||
if test.reconstructParity {
|
||||
err = storage.ErasureDecodeDataAndParityBlocks(encoded)
|
||||
} else {
|
||||
err = storage.ErasureDecodeDataBlocks(encoded)
|
||||
}
|
||||
|
||||
// Generates encoded data based on type of testCase function.
|
||||
encodedData := testCase.enFn(data, dataBlocks, parityBlocks)
|
||||
if err == nil && test.shouldFail {
|
||||
t.Errorf("Test %d: test should fail but it passed", i)
|
||||
}
|
||||
if err != nil && !test.shouldFail {
|
||||
t.Errorf("Test %d: test should pass but it failed: %v", i, err)
|
||||
}
|
||||
|
||||
// Decodes the data.
|
||||
err := decodeData(encodedData, dataBlocks, parityBlocks)
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass by failed instead with %s", i+1, err)
|
||||
decoded := encoded
|
||||
if !test.shouldFail {
|
||||
if test.reconstructParity {
|
||||
for j := range decoded {
|
||||
if decoded[j] == nil {
|
||||
t.Errorf("Test %d: failed to reconstruct shard %d", i, j)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for j := range decoded[:test.dataBlocks] {
|
||||
if decoded[j] == nil {
|
||||
t.Errorf("Test %d: failed to reconstruct data shard %d", i, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed to extract the data blocks.
|
||||
decodedDataWriter := new(bytes.Buffer)
|
||||
_, err = writeDataBlocks(decodedDataWriter, encodedData, dataBlocks, 0, int64(blockSize))
|
||||
if err != nil && testCase.shouldPass {
|
||||
t.Errorf("Test %d: Expected to pass by failed instead with %s", i+1, err)
|
||||
decodedData := new(bytes.Buffer)
|
||||
if _, err = writeDataBlocks(decodedData, decoded, test.dataBlocks, 0, int64(len(data))); err != nil {
|
||||
t.Errorf("Test %d: failed to write data blocks: %v", i, err)
|
||||
}
|
||||
|
||||
// Validate if decoded data is what we expected.
|
||||
if bytes.Equal(decodedDataWriter.Bytes(), data) != testCase.shouldPass {
|
||||
err := errUnexpected
|
||||
t.Errorf("Test %d: Expected to pass by failed instead %s", i+1, err)
|
||||
if !bytes.Equal(decodedData.Bytes(), data) {
|
||||
t.Errorf("Test %d: Decoded data does not match original data: got: %v want: %v", i, decodedData.Bytes(), data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,7 +120,7 @@ type erasureTestSetup struct {
|
||||
// Removes the temporary disk directories.
|
||||
func (e erasureTestSetup) Remove() {
|
||||
for _, path := range e.diskPaths {
|
||||
removeAll(path)
|
||||
os.RemoveAll(path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -151,3 +151,8 @@ func isErr(err error, errs ...error) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// traceErrorf behaves like fmt.traceErrorf but also traces the returned error.
|
||||
func traceErrorf(format string, args ...interface{}) error {
|
||||
return traceError(fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
+90
-44
@@ -27,6 +27,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -59,7 +60,7 @@ type internalNotifier struct {
|
||||
// Connected listeners is a map of listener ARNs to channels
|
||||
// on which the ListenBucket API handler go routine is waiting
|
||||
// for events to send to a client.
|
||||
connectedListeners map[string]chan []NotificationEvent
|
||||
connectedListeners map[string]*listenChan
|
||||
|
||||
rwMutex *sync.RWMutex
|
||||
}
|
||||
@@ -101,12 +102,7 @@ func newNotificationEvent(event eventData) NotificationEvent {
|
||||
host = localIP4.ToSlice()[0]
|
||||
}
|
||||
|
||||
scheme := httpScheme
|
||||
if globalIsSSL {
|
||||
scheme = httpsScheme
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s://%s:%s", scheme, host, globalMinioPort)
|
||||
return fmt.Sprintf("%s://%s:%s", getURLScheme(globalIsSSL), host, globalMinioPort)
|
||||
}
|
||||
|
||||
// Fetch the region.
|
||||
@@ -171,13 +167,13 @@ func newNotificationEvent(event eventData) NotificationEvent {
|
||||
|
||||
// For all other events we should set ETag and Size.
|
||||
nEvent.S3.Object = objectMeta{
|
||||
Key: escapedObj,
|
||||
ETag: event.ObjInfo.ETag,
|
||||
Size: event.ObjInfo.Size,
|
||||
ContentType: event.ObjInfo.ContentType,
|
||||
UserDefined: event.ObjInfo.UserDefined,
|
||||
VersionID: "1",
|
||||
Sequencer: uniqueID,
|
||||
Key: escapedObj,
|
||||
ETag: event.ObjInfo.ETag,
|
||||
Size: event.ObjInfo.Size,
|
||||
ContentType: event.ObjInfo.ContentType,
|
||||
UserMetadata: event.ObjInfo.UserDefined,
|
||||
VersionID: "1",
|
||||
Sequencer: uniqueID,
|
||||
}
|
||||
|
||||
// Success.
|
||||
@@ -210,7 +206,7 @@ func (en eventNotifier) GetInternalTarget(arn string) *listenerLogger {
|
||||
}
|
||||
|
||||
// Set a new sns target for an input sns ARN.
|
||||
func (en *eventNotifier) AddListenerChan(snsARN string, listenerCh chan []NotificationEvent) error {
|
||||
func (en *eventNotifier) AddListenerChan(snsARN string, listenerCh *listenChan) error {
|
||||
if listenerCh == nil {
|
||||
return errInvalidArgument
|
||||
}
|
||||
@@ -233,9 +229,9 @@ func (en *eventNotifier) SendListenerEvent(arn string, event []NotificationEvent
|
||||
en.internal.rwMutex.Lock()
|
||||
defer en.internal.rwMutex.Unlock()
|
||||
|
||||
ch, ok := en.internal.connectedListeners[arn]
|
||||
listenChan, ok := en.internal.connectedListeners[arn]
|
||||
if ok {
|
||||
ch <- event
|
||||
listenChan.sendNotificationEvent(event)
|
||||
}
|
||||
// If the channel is not present we ignore the event.
|
||||
return nil
|
||||
@@ -370,7 +366,9 @@ func loadNotificationConfig(bucket string, objAPI ObjectLayer) (*notificationCon
|
||||
|
||||
// Acquire a write lock on notification config before modifying.
|
||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, ncPath)
|
||||
objLock.RLock()
|
||||
if err := objLock.GetRLock(globalOperationTimeout); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer objLock.RUnlock()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
@@ -380,18 +378,24 @@ func loadNotificationConfig(bucket string, objAPI ObjectLayer) (*notificationCon
|
||||
// 'errNoSuchNotifications'. This is default when no
|
||||
// bucket notifications are found on the bucket.
|
||||
if isErrObjectNotFound(err) || isErrIncompleteBody(err) {
|
||||
return nil, errNoSuchNotifications
|
||||
return nil, traceError(errNoSuchNotifications)
|
||||
}
|
||||
errorIf(err, "Unable to load bucket-notification for bucket %s", bucket)
|
||||
// Returns error for other errors.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if `notifications.xml` is empty we should return NoSuchNotifications.
|
||||
if buffer.Len() == 0 {
|
||||
return nil, traceError(errNoSuchNotifications)
|
||||
}
|
||||
|
||||
// Unmarshal notification bytes.
|
||||
notificationConfigBytes := buffer.Bytes()
|
||||
notificationCfg := ¬ificationConfig{}
|
||||
if err = xml.Unmarshal(notificationConfigBytes, ¬ificationCfg); err != nil {
|
||||
return nil, err
|
||||
// Unmarshal notification bytes only if we read data.
|
||||
if err = xml.Unmarshal(notificationConfigBytes, notificationCfg); err != nil {
|
||||
return nil, traceError(err)
|
||||
}
|
||||
|
||||
// Return success.
|
||||
@@ -413,29 +417,35 @@ func loadListenerConfig(bucket string, objAPI ObjectLayer) ([]listenerConfig, er
|
||||
|
||||
// Acquire a write lock on notification config before modifying.
|
||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, lcPath)
|
||||
objLock.RLock()
|
||||
if err := objLock.GetRLock(globalOperationTimeout); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer objLock.RUnlock()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
err := objAPI.GetObject(minioMetaBucket, lcPath, 0, -1, &buffer)
|
||||
if err != nil {
|
||||
// 'notification.xml' not found return
|
||||
// 'listener.json' not found return
|
||||
// 'errNoSuchNotifications'. This is default when no
|
||||
// bucket listeners are found on the bucket.
|
||||
if isErrObjectNotFound(err) {
|
||||
return nil, errNoSuchNotifications
|
||||
// bucket listeners are found on the bucket
|
||||
if isErrObjectNotFound(err) || isErrIncompleteBody(err) {
|
||||
return nil, traceError(errNoSuchNotifications)
|
||||
}
|
||||
errorIf(err, "Unable to load bucket-listeners for bucket %s", bucket)
|
||||
// Returns error for other errors.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal notification bytes.
|
||||
// if `listener.json` is empty we should return NoSuchNotifications.
|
||||
if buffer.Len() == 0 {
|
||||
return nil, traceError(errNoSuchNotifications)
|
||||
}
|
||||
|
||||
var lCfg []listenerConfig
|
||||
lConfigBytes := buffer.Bytes()
|
||||
if err = json.Unmarshal(lConfigBytes, &lCfg); err != nil {
|
||||
errorIf(err, "Unable to unmarshal listener config from JSON.")
|
||||
return nil, err
|
||||
return nil, traceError(err)
|
||||
}
|
||||
|
||||
// Return success.
|
||||
@@ -454,12 +464,18 @@ func persistNotificationConfig(bucket string, ncfg *notificationConfig, obj Obje
|
||||
ncPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
||||
// Acquire a write lock on notification config before modifying.
|
||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, ncPath)
|
||||
objLock.Lock()
|
||||
if err = objLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer objLock.Unlock()
|
||||
|
||||
// write object to path
|
||||
sha256Sum := getSHA256Hash(buf)
|
||||
_, err = obj.PutObject(minioMetaBucket, ncPath, int64(len(buf)), bytes.NewReader(buf), nil, sha256Sum)
|
||||
hashReader, err := hash.NewReader(bytes.NewReader(buf), int64(len(buf)), "", getSHA256Hash(buf))
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to write bucket notification configuration.")
|
||||
return err
|
||||
}
|
||||
_, err = obj.PutObject(minioMetaBucket, ncPath, hashReader, nil)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to write bucket notification configuration.")
|
||||
return err
|
||||
@@ -479,16 +495,25 @@ func persistListenerConfig(bucket string, lcfg []listenerConfig, obj ObjectLayer
|
||||
lcPath := path.Join(bucketConfigPrefix, bucket, bucketListenerConfig)
|
||||
// Acquire a write lock on notification config before modifying.
|
||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, lcPath)
|
||||
objLock.Lock()
|
||||
if err = objLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer objLock.Unlock()
|
||||
|
||||
// write object to path
|
||||
sha256Sum := getSHA256Hash(buf)
|
||||
_, err = obj.PutObject(minioMetaBucket, lcPath, int64(len(buf)), bytes.NewReader(buf), nil, sha256Sum)
|
||||
hashReader, err := hash.NewReader(bytes.NewReader(buf), int64(len(buf)), "", getSHA256Hash(buf))
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to write bucket listener configuration to object layer.")
|
||||
return err
|
||||
}
|
||||
return err
|
||||
|
||||
// write object to path
|
||||
_, err = obj.PutObject(minioMetaBucket, lcPath, hashReader, nil)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to write bucket listener configuration to object layer.")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Removes notification.xml for a given bucket, only used during DeleteBucket.
|
||||
@@ -502,10 +527,11 @@ func removeNotificationConfig(bucket string, objAPI ObjectLayer) error {
|
||||
|
||||
// Acquire a write lock on notification config before modifying.
|
||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, ncPath)
|
||||
objLock.Lock()
|
||||
err := objAPI.DeleteObject(minioMetaBucket, ncPath)
|
||||
objLock.Unlock()
|
||||
return err
|
||||
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer objLock.Unlock()
|
||||
return objAPI.DeleteObject(minioMetaBucket, ncPath)
|
||||
}
|
||||
|
||||
// Remove listener configuration from storage layer. Used when a bucket is deleted.
|
||||
@@ -515,10 +541,11 @@ func removeListenerConfig(bucket string, objAPI ObjectLayer) error {
|
||||
|
||||
// Acquire a write lock on notification config before modifying.
|
||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, lcPath)
|
||||
objLock.Lock()
|
||||
err := objAPI.DeleteObject(minioMetaBucket, lcPath)
|
||||
objLock.Unlock()
|
||||
return err
|
||||
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer objLock.Unlock()
|
||||
return objAPI.DeleteObject(minioMetaBucket, lcPath)
|
||||
}
|
||||
|
||||
// Loads both notification and listener config.
|
||||
@@ -609,6 +636,25 @@ func loadAllQueueTargets() (map[string]*logrus.Logger, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Load all mqtt targets, initialize their respective loggers.
|
||||
for accountID, mqttN := range serverConfig.Notify.GetMQTT() {
|
||||
if !mqttN.Enable {
|
||||
continue
|
||||
}
|
||||
|
||||
if queueARN, err := addQueueTarget(queueTargets, accountID, queueTypeMQTT, newMQTTNotify); err != nil {
|
||||
if _, ok := err.(net.Error); ok {
|
||||
err = &net.OpError{
|
||||
Op: "Connecting to " + queueARN,
|
||||
Net: "tcp",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Load all nats targets, initialize their respective loggers.
|
||||
for accountID, natsN := range serverConfig.Notify.GetNATS() {
|
||||
if !natsN.Enable {
|
||||
@@ -787,7 +833,7 @@ func initEventNotifier(objAPI ObjectLayer) error {
|
||||
rwMutex: &sync.RWMutex{},
|
||||
targets: listenTargets,
|
||||
listenerConfigs: lConfigs,
|
||||
connectedListeners: make(map[string]chan []NotificationEvent),
|
||||
connectedListeners: make(map[string]*listenChan),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+22
-25
@@ -19,6 +19,7 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -32,7 +33,7 @@ func TestInitEventNotifierFaultyDisks(t *testing.T) {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
disks, err := getRandomDisks(4)
|
||||
if err != nil {
|
||||
@@ -61,7 +62,7 @@ func TestInitEventNotifierFaultyDisks(t *testing.T) {
|
||||
notificationXML += "</NotificationConfiguration>"
|
||||
size := int64(len([]byte(notificationXML)))
|
||||
reader := bytes.NewReader([]byte(notificationXML))
|
||||
if _, err := xl.PutObject(minioMetaBucket, bucketConfigPrefix+"/"+bucketName+"/"+bucketNotificationConfig, size, reader, nil, ""); err != nil {
|
||||
if _, err := xl.PutObject(minioMetaBucket, bucketConfigPrefix+"/"+bucketName+"/"+bucketNotificationConfig, mustGetHashReader(t, reader, size, "", ""), nil); err != nil {
|
||||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
|
||||
@@ -85,10 +86,10 @@ func TestInitEventNotifierWithPostgreSQL(t *testing.T) {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
disks, err := getRandomDisks(1)
|
||||
defer removeAll(disks[0])
|
||||
defer os.RemoveAll(disks[0])
|
||||
if err != nil {
|
||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
||||
}
|
||||
@@ -112,10 +113,10 @@ func TestInitEventNotifierWithNATS(t *testing.T) {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
disks, err := getRandomDisks(1)
|
||||
defer removeAll(disks[0])
|
||||
defer os.RemoveAll(disks[0])
|
||||
if err != nil {
|
||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
||||
}
|
||||
@@ -139,10 +140,10 @@ func TestInitEventNotifierWithWebHook(t *testing.T) {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
disks, err := getRandomDisks(1)
|
||||
defer removeAll(disks[0])
|
||||
defer os.RemoveAll(disks[0])
|
||||
if err != nil {
|
||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
||||
}
|
||||
@@ -166,10 +167,10 @@ func TestInitEventNotifierWithAMQP(t *testing.T) {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
disks, err := getRandomDisks(1)
|
||||
defer removeAll(disks[0])
|
||||
defer os.RemoveAll(disks[0])
|
||||
if err != nil {
|
||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
||||
}
|
||||
@@ -193,10 +194,10 @@ func TestInitEventNotifierWithElasticSearch(t *testing.T) {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
disks, err := getRandomDisks(1)
|
||||
defer removeAll(disks[0])
|
||||
defer os.RemoveAll(disks[0])
|
||||
if err != nil {
|
||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
||||
}
|
||||
@@ -220,10 +221,10 @@ func TestInitEventNotifierWithRedis(t *testing.T) {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer removeAll(rootPath)
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
disks, err := getRandomDisks(1)
|
||||
defer removeAll(disks[0])
|
||||
defer os.RemoveAll(disks[0])
|
||||
if err != nil {
|
||||
t.Fatal("Unable to create directories for FS backend. ", err)
|
||||
}
|
||||
@@ -258,9 +259,9 @@ func (s *TestPeerRPCServerData) Setup(t *testing.T) {
|
||||
|
||||
func (s *TestPeerRPCServerData) TearDown() {
|
||||
s.testServer.Stop()
|
||||
_ = removeAll(s.testServer.Root)
|
||||
_ = os.RemoveAll(s.testServer.Root)
|
||||
for _, d := range s.testServer.Disks {
|
||||
_ = removeAll(d.Path)
|
||||
_ = os.RemoveAll(d.Path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +313,7 @@ func TestInitEventNotifier(t *testing.T) {
|
||||
filterRules := []filterRule{
|
||||
{
|
||||
Name: "prefix",
|
||||
Value: globalMinioDefaultOwnerID,
|
||||
Value: "minio",
|
||||
},
|
||||
{
|
||||
Name: "suffix",
|
||||
@@ -461,11 +462,9 @@ func TestListenBucketNotification(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create a new notification event channel.
|
||||
nEventCh := make(chan []NotificationEvent)
|
||||
// Close the listener channel.
|
||||
defer close(nEventCh)
|
||||
nListenCh := newListenChan()
|
||||
// Add events channel for listener.
|
||||
if err := globalEventNotifier.AddListenerChan(listenARN, nEventCh); err != nil {
|
||||
if err := globalEventNotifier.AddListenerChan(listenARN, nListenCh); err != nil {
|
||||
t.Fatalf("Test Setup error: %v", err)
|
||||
}
|
||||
// Remove listen channel after the writer has closed or the
|
||||
@@ -488,7 +487,7 @@ func TestListenBucketNotification(t *testing.T) {
|
||||
// Wait for the event notification here, if nothing is received within 30 seconds,
|
||||
// test error will be fired
|
||||
select {
|
||||
case n := <-nEventCh:
|
||||
case n := <-nListenCh.dataCh:
|
||||
// Check that received event
|
||||
if len(n) == 0 {
|
||||
t.Fatal("Unexpected error occurred")
|
||||
@@ -496,9 +495,7 @@ func TestListenBucketNotification(t *testing.T) {
|
||||
if n[0].S3.Object.Key != objectName {
|
||||
t.Fatalf("Received wrong object name in notification, expected %s, received %s", n[0].S3.Object.Key, objectName)
|
||||
}
|
||||
break
|
||||
case <-time.After(3 * time.Second):
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
@@ -535,7 +532,7 @@ func TestAddRemoveBucketListenerConfig(t *testing.T) {
|
||||
filterRules := []filterRule{
|
||||
{
|
||||
Name: "prefix",
|
||||
Value: globalMinioDefaultOwnerID,
|
||||
Value: "minio",
|
||||
},
|
||||
{
|
||||
Name: "suffix",
|
||||
|
||||
+118
-253
@@ -241,48 +241,32 @@ else // No healing at this point forward, some disks are offline or dead.
|
||||
fi
|
||||
*/
|
||||
|
||||
// error returned when some disks are found to be unformatted.
|
||||
var errSomeDiskUnformatted = errors.New("some disks are found to be unformatted")
|
||||
|
||||
// error returned when some disks are offline.
|
||||
var errSomeDiskOffline = errors.New("some disks are offline")
|
||||
|
||||
// errDiskOrderMismatch - returned when disk UUID is not in consistent JBOD order.
|
||||
var errDiskOrderMismatch = errors.New("disk order mismatch")
|
||||
|
||||
// Returns error slice into understandable errors.
|
||||
func reduceFormatErrs(errs []error, diskCount int) (err error) {
|
||||
var errUnformattedDiskCount = 0
|
||||
var errDiskNotFoundCount = 0
|
||||
var errCorruptedFormatCount = 0
|
||||
for _, dErr := range errs {
|
||||
if dErr == errUnformattedDisk {
|
||||
errUnformattedDiskCount++
|
||||
} else if dErr == errDiskNotFound {
|
||||
errDiskNotFoundCount++
|
||||
} else if dErr == errCorruptedFormat {
|
||||
errCorruptedFormatCount++
|
||||
// formatErrsSummary - summarizes errors into different classes
|
||||
func formatErrsSummary(errs []error) (formatCount, unformattedDiskCount,
|
||||
diskNotFoundCount, corruptedFormatCount, otherErrCount int) {
|
||||
|
||||
for _, err := range errs {
|
||||
switch err {
|
||||
case errDiskNotFound:
|
||||
diskNotFoundCount++
|
||||
case errUnformattedDisk:
|
||||
unformattedDiskCount++
|
||||
case errCorruptedFormat:
|
||||
corruptedFormatCount++
|
||||
case nil:
|
||||
// implies that format is not nil
|
||||
formatCount++
|
||||
default:
|
||||
otherErrCount++
|
||||
}
|
||||
}
|
||||
if errCorruptedFormatCount > 0 {
|
||||
return errCorruptedFormat
|
||||
}
|
||||
// Unformatted disks found, we need to figure out if any disks are offline.
|
||||
if errUnformattedDiskCount > 0 {
|
||||
// Returns errUnformattedDisk if all disks report unFormattedDisk.
|
||||
if errUnformattedDiskCount < diskCount {
|
||||
if errDiskNotFoundCount > 0 {
|
||||
// Only some disks are fresh but some disks are offline as well.
|
||||
return errSomeDiskOffline
|
||||
}
|
||||
// Some disks are fresh disks an unformatted, not disks are offline.
|
||||
return errSomeDiskUnformatted
|
||||
}
|
||||
// All disks returned unformatted, all disks must be fresh.
|
||||
return errUnformattedDisk
|
||||
}
|
||||
// No unformatted disks found no need to handle disk not found case, return success here.
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
// loadAllFormats - load all format config from all input disks in parallel.
|
||||
@@ -469,29 +453,50 @@ func findDiskIndex(disk string, jbod []string) int {
|
||||
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
|
||||
// reorderDisks - reorder disks in JBOD order, and return reference
|
||||
// format-config. If assignUUIDs is true, it assigns UUIDs to disks
|
||||
// with missing format configurations in the reference configuration.
|
||||
func reorderDisks(bootstrapDisks []StorageAPI,
|
||||
formatConfigs []*formatConfigV1, assignUUIDs bool) (*formatConfigV1,
|
||||
[]StorageAPI, error) {
|
||||
|
||||
// Pick first non-nil format-cfg as reference
|
||||
var refCfg *formatConfigV1
|
||||
for _, formatConfig := range formatConfigs {
|
||||
if formatConfig != nil {
|
||||
refCfg = formatConfig
|
||||
break
|
||||
}
|
||||
savedJBOD = format.XL.JBOD
|
||||
break
|
||||
}
|
||||
// Pick the first JBOD list to verify the order and construct new set of disk slice.
|
||||
if refCfg == nil {
|
||||
return nil, nil, fmt.Errorf("could not find any valid config")
|
||||
}
|
||||
refJBOD := refCfg.XL.JBOD
|
||||
|
||||
// construct reordered disk slice
|
||||
var newDisks = make([]StorageAPI, len(bootstrapDisks))
|
||||
for fIndex, format := range formatConfigs {
|
||||
if format == nil {
|
||||
continue
|
||||
}
|
||||
jIndex := findDiskIndex(format.XL.Disk, savedJBOD)
|
||||
jIndex := findDiskIndex(format.XL.Disk, refJBOD)
|
||||
if jIndex == -1 {
|
||||
return nil, errors.New("Unrecognized uuid " + format.XL.Disk + " found")
|
||||
return nil, nil, errors.New("Unrecognized uuid " + format.XL.Disk + " found")
|
||||
}
|
||||
newDisks[jIndex] = bootstrapDisks[fIndex]
|
||||
}
|
||||
return newDisks, nil
|
||||
|
||||
if assignUUIDs {
|
||||
// Based on orderedDisks generate new UUIDs in the ref. config
|
||||
// for disks without format-configs.
|
||||
for index, disk := range newDisks {
|
||||
if disk == nil {
|
||||
refCfg.XL.JBOD[index] = mustGetUUID()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refCfg, newDisks, nil
|
||||
}
|
||||
|
||||
// loadFormat - loads format.json from disk.
|
||||
@@ -526,105 +531,14 @@ func loadFormat(disk StorageAPI) (format *formatConfigV1, err error) {
|
||||
return format, nil
|
||||
}
|
||||
|
||||
// isFormatNotFound - returns true if all `format.json` are not found on all disks.
|
||||
func isFormatNotFound(formats []*formatConfigV1) bool {
|
||||
for _, format := range formats {
|
||||
// One of the `format.json` is found.
|
||||
if format != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// All format.json missing, success.
|
||||
return true
|
||||
}
|
||||
|
||||
// isFormatFound - returns true if all input formats are found on all disks.
|
||||
func isFormatFound(formats []*formatConfigV1) bool {
|
||||
for _, format := range formats {
|
||||
// One of `format.json` is not found.
|
||||
if format == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// All format.json present, success.
|
||||
return true
|
||||
}
|
||||
|
||||
// Heals any missing format.json on the drives. Returns error only for unexpected errors
|
||||
// as regular errors can be ignored since there might be enough quorum to be operational.
|
||||
// Heals only fresh disks.
|
||||
func healFormatXLFreshDisks(storageDisks []StorageAPI) error {
|
||||
formatConfigs := make([]*formatConfigV1, len(storageDisks))
|
||||
var referenceConfig *formatConfigV1
|
||||
// Loads `format.json` from all disks.
|
||||
for index, disk := range storageDisks {
|
||||
// Disk not found or ignored is a valid case.
|
||||
if disk == nil {
|
||||
// Return nil, one of the disk is offline.
|
||||
return nil
|
||||
}
|
||||
formatXL, err := loadFormat(disk)
|
||||
if err != nil {
|
||||
if err == errUnformattedDisk {
|
||||
// format.json is missing, should be healed.
|
||||
continue
|
||||
} else if err == errDiskNotFound { // Is a valid case we
|
||||
// can proceed without healing.
|
||||
return nil
|
||||
}
|
||||
// Return error for unsupported errors.
|
||||
return err
|
||||
} // Success.
|
||||
formatConfigs[index] = formatXL
|
||||
}
|
||||
|
||||
// All `format.json` has been read successfully, previously completed.
|
||||
if isFormatFound(formatConfigs) {
|
||||
// Return success.
|
||||
return nil
|
||||
}
|
||||
|
||||
// All disks are fresh, format.json will be written by initFormatXL()
|
||||
if isFormatNotFound(formatConfigs) {
|
||||
return initFormatXL(storageDisks)
|
||||
}
|
||||
|
||||
// Validate format configs for consistency in JBOD and disks.
|
||||
if err := checkFormatXL(formatConfigs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if referenceConfig == nil {
|
||||
// This config will be used to update the drives missing format.json.
|
||||
for _, formatConfig := range formatConfigs {
|
||||
if formatConfig == nil {
|
||||
continue
|
||||
}
|
||||
referenceConfig = formatConfig
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Collect new JBOD.
|
||||
newJBOD := referenceConfig.XL.JBOD
|
||||
|
||||
// Reorder the disks based on the JBOD order.
|
||||
orderedDisks, err := reorderDisks(storageDisks, formatConfigs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// From ordered disks fill the UUID position.
|
||||
for index, disk := range orderedDisks {
|
||||
if disk == nil {
|
||||
newJBOD[index] = mustGetUUID()
|
||||
}
|
||||
}
|
||||
|
||||
// Collect new format configs.
|
||||
var newFormatConfigs = make([]*formatConfigV1, len(orderedDisks))
|
||||
// collectNSaveNewFormatConfigs - creates new format configs based on
|
||||
// the reference config and saves it on all disks, this is to be
|
||||
// called from healFormatXL* functions.
|
||||
func collectNSaveNewFormatConfigs(referenceConfig *formatConfigV1,
|
||||
orderedDisks []StorageAPI) error {
|
||||
|
||||
// Collect new format configs that need to be written.
|
||||
var newFormatConfigs = make([]*formatConfigV1, len(orderedDisks))
|
||||
for index := range orderedDisks {
|
||||
// New configs are generated since we are going
|
||||
// to re-populate across all disks.
|
||||
@@ -633,13 +547,35 @@ func healFormatXLFreshDisks(storageDisks []StorageAPI) error {
|
||||
Format: referenceConfig.Format,
|
||||
XL: &xlFormat{
|
||||
Version: referenceConfig.XL.Version,
|
||||
Disk: newJBOD[index],
|
||||
JBOD: newJBOD,
|
||||
Disk: referenceConfig.XL.JBOD[index],
|
||||
JBOD: referenceConfig.XL.JBOD,
|
||||
},
|
||||
}
|
||||
newFormatConfigs[index] = config
|
||||
}
|
||||
|
||||
// Initialize meta volume, if volume already exists ignores it.
|
||||
if err := initMetaVolume(orderedDisks); err != nil {
|
||||
return fmt.Errorf("Unable to initialize '.minio.sys' meta volume, %s", err)
|
||||
}
|
||||
|
||||
// Save new `format.json` across all disks, in JBOD order.
|
||||
return saveFormatXL(orderedDisks, newFormatConfigs)
|
||||
}
|
||||
|
||||
// Heals any missing format.json on the drives. Returns error only for
|
||||
// unexpected errors as regular errors can be ignored since there
|
||||
// might be enough quorum to be operational. Heals only fresh disks.
|
||||
func healFormatXLFreshDisks(storageDisks []StorageAPI,
|
||||
formatConfigs []*formatConfigV1) error {
|
||||
|
||||
// Reorder the disks based on the JBOD order.
|
||||
referenceConfig, orderedDisks, err := reorderDisks(storageDisks,
|
||||
formatConfigs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fill in the missing disk back from format configs.
|
||||
// We need to make sure we have kept the previous order
|
||||
// and allowed fresh disks to be arranged anywhere.
|
||||
@@ -658,37 +594,37 @@ func healFormatXLFreshDisks(storageDisks []StorageAPI) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize meta volume, if volume already exists ignores it.
|
||||
if err := initMetaVolume(orderedDisks); err != nil {
|
||||
return fmt.Errorf("Unable to initialize '.minio.sys' meta volume, %s", err)
|
||||
}
|
||||
|
||||
// Save new `format.json` across all disks, in JBOD order.
|
||||
return saveFormatXL(orderedDisks, newFormatConfigs)
|
||||
// apply new format config and save to all disks
|
||||
return collectNSaveNewFormatConfigs(referenceConfig, orderedDisks)
|
||||
}
|
||||
|
||||
// Disks from storageDiks are put in assignedDisks if found in orderedDisks and in unAssignedDisks otherwise
|
||||
func splitDisksByUse(storageDisks, orderedDisks []StorageAPI) (assignedDisks []StorageAPI, unAssignedDisks []StorageAPI) {
|
||||
// Populate unAssignDisks
|
||||
// collectUnAssignedDisks - collect disks unassigned to orderedDisks
|
||||
// from storageDisks and return them.
|
||||
func collectUnAssignedDisks(storageDisks, orderedDisks []StorageAPI) (
|
||||
uDisks []StorageAPI) {
|
||||
|
||||
// search for each disk from storageDisks in orderedDisks
|
||||
for i := range storageDisks {
|
||||
found := false
|
||||
for j := range orderedDisks {
|
||||
if storageDisks[i] == orderedDisks[j] {
|
||||
found = true
|
||||
assignedDisks = append(assignedDisks, storageDisks[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
unAssignedDisks = append(unAssignedDisks, storageDisks[i])
|
||||
// append not found disk to result
|
||||
uDisks = append(uDisks, storageDisks[i])
|
||||
}
|
||||
}
|
||||
return assignedDisks, unAssignedDisks
|
||||
return uDisks
|
||||
}
|
||||
|
||||
// Inspect the content of all disks to guess the right order according to the format files.
|
||||
// The right order is represented in orderedDisks
|
||||
func reorderDisksByInspection(orderedDisks, storageDisks []StorageAPI, formatConfigs []*formatConfigV1) ([]StorageAPI, error) {
|
||||
// Inspect the content of all disks to guess the right order according
|
||||
// to the format files. The right order is represented in orderedDisks
|
||||
func reorderDisksByInspection(orderedDisks, storageDisks []StorageAPI,
|
||||
formatConfigs []*formatConfigV1) ([]StorageAPI, error) {
|
||||
|
||||
for index, format := range formatConfigs {
|
||||
if format != nil {
|
||||
continue
|
||||
@@ -701,7 +637,8 @@ func reorderDisksByInspection(orderedDisks, storageDisks []StorageAPI, formatCon
|
||||
continue
|
||||
}
|
||||
volName := ""
|
||||
// Avoid picking minioMetaBucket because ListVols() returns a non ordered list
|
||||
// Avoid picking minioMetaBucket because ListVols()
|
||||
// returns a non ordered list
|
||||
for i := range vols {
|
||||
if vols[i].Name != minioMetaBucket {
|
||||
volName = vols[i].Name
|
||||
@@ -742,84 +679,28 @@ func reorderDisksByInspection(orderedDisks, storageDisks []StorageAPI, formatCon
|
||||
}
|
||||
|
||||
// Heals corrupted format json in all disks
|
||||
func healFormatXLCorruptedDisks(storageDisks []StorageAPI) error {
|
||||
formatConfigs := make([]*formatConfigV1, len(storageDisks))
|
||||
var referenceConfig *formatConfigV1
|
||||
|
||||
// Loads `format.json` from all disks.
|
||||
for index, disk := range storageDisks {
|
||||
// Disk not found or ignored is a valid case.
|
||||
if disk == nil {
|
||||
// Return nil, one of the disk is offline.
|
||||
return nil
|
||||
}
|
||||
formatXL, err := loadFormat(disk)
|
||||
if err != nil {
|
||||
if err == errUnformattedDisk || err == errCorruptedFormat {
|
||||
// format.json is missing or corrupted, should be healed.
|
||||
continue
|
||||
} else if err == errDiskNotFound { // Is a valid case we
|
||||
// can proceed without healing.
|
||||
return nil
|
||||
}
|
||||
// Return error for unsupported errors.
|
||||
return err
|
||||
} // Success.
|
||||
formatConfigs[index] = formatXL
|
||||
}
|
||||
|
||||
// All `format.json` has been read successfully, previously completed.
|
||||
if isFormatFound(formatConfigs) {
|
||||
// Return success.
|
||||
return nil
|
||||
}
|
||||
|
||||
// All disks are fresh, format.json will be written by initFormatXL()
|
||||
if isFormatNotFound(formatConfigs) {
|
||||
return initFormatXL(storageDisks)
|
||||
}
|
||||
|
||||
// Validate format configs for consistency in JBOD and disks.
|
||||
if err := checkFormatXL(formatConfigs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if referenceConfig == nil {
|
||||
// This config will be used to update the drives missing format.json.
|
||||
for _, formatConfig := range formatConfigs {
|
||||
if formatConfig == nil {
|
||||
continue
|
||||
}
|
||||
referenceConfig = formatConfig
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Collect new JBOD.
|
||||
newJBOD := referenceConfig.XL.JBOD
|
||||
func healFormatXLCorruptedDisks(storageDisks []StorageAPI,
|
||||
formatConfigs []*formatConfigV1) error {
|
||||
|
||||
// Reorder the disks based on the JBOD order.
|
||||
orderedDisks, err := reorderDisks(storageDisks, formatConfigs)
|
||||
referenceConfig, orderedDisks, err := reorderDisks(storageDisks,
|
||||
formatConfigs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// From ordered disks fill the UUID position.
|
||||
for index, disk := range orderedDisks {
|
||||
if disk == nil {
|
||||
newJBOD[index] = mustGetUUID()
|
||||
}
|
||||
}
|
||||
|
||||
// For disks with corrupted formats, inspect the disks contents to guess the disks order
|
||||
orderedDisks, err = reorderDisksByInspection(orderedDisks, storageDisks, formatConfigs)
|
||||
// For disks with corrupted formats, inspect the disks
|
||||
// contents to guess the disks order
|
||||
orderedDisks, err = reorderDisksByInspection(orderedDisks, storageDisks,
|
||||
formatConfigs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// At this stage, all disks with corrupted formats but with objects inside found their way.
|
||||
// Now take care of unformatted disks, which are the `unAssignedDisks`
|
||||
_, unAssignedDisks := splitDisksByUse(storageDisks, orderedDisks)
|
||||
// At this stage, all disks with corrupted formats but with
|
||||
// objects inside found their way. Now take care of
|
||||
// unformatted disks, which are the `unAssignedDisks`
|
||||
unAssignedDisks := collectUnAssignedDisks(storageDisks, orderedDisks)
|
||||
|
||||
// Assign unassigned disks to nil elements in orderedDisks
|
||||
for i, disk := range orderedDisks {
|
||||
@@ -829,27 +710,8 @@ func healFormatXLCorruptedDisks(storageDisks []StorageAPI) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Collect new format configs.
|
||||
var newFormatConfigs = make([]*formatConfigV1, len(orderedDisks))
|
||||
|
||||
// Collect new format configs that need to be written.
|
||||
for index := range orderedDisks {
|
||||
// New configs are generated since we are going
|
||||
// to re-populate across all disks.
|
||||
config := &formatConfigV1{
|
||||
Version: referenceConfig.Version,
|
||||
Format: referenceConfig.Format,
|
||||
XL: &xlFormat{
|
||||
Version: referenceConfig.XL.Version,
|
||||
Disk: newJBOD[index],
|
||||
JBOD: newJBOD,
|
||||
},
|
||||
}
|
||||
newFormatConfigs[index] = config
|
||||
}
|
||||
|
||||
// Save new `format.json` across all disks, in JBOD order.
|
||||
return saveFormatXL(orderedDisks, newFormatConfigs)
|
||||
// apply new format config and save to all disks
|
||||
return collectNSaveNewFormatConfigs(referenceConfig, orderedDisks)
|
||||
}
|
||||
|
||||
// loadFormatXL - loads XL `format.json` and returns back properly
|
||||
@@ -900,8 +762,11 @@ func loadFormatXL(bootstrapDisks []StorageAPI, readQuorum int) (disks []StorageA
|
||||
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)
|
||||
// Erasure code requires disks to be presented in the same
|
||||
// order each time.
|
||||
_, orderedDisks, err := reorderDisks(bootstrapDisks, formatConfigs,
|
||||
false)
|
||||
return orderedDisks, err
|
||||
}
|
||||
|
||||
func checkFormatXLValue(formatXL *formatConfigV1) error {
|
||||
|
||||
+62
-208
@@ -23,6 +23,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/lock"
|
||||
)
|
||||
|
||||
@@ -235,13 +236,16 @@ func prepareFormatXLHealFreshDisks(obj ObjectLayer) ([]StorageAPI, error) {
|
||||
|
||||
bucket := "bucket"
|
||||
object := "object"
|
||||
sha256sum := ""
|
||||
|
||||
_, err = obj.PutObject(bucket, object, int64(len("abcd")), bytes.NewReader([]byte("abcd")), nil, sha256sum)
|
||||
hashReader, err := hash.NewReader(bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", "")
|
||||
if err != nil {
|
||||
return []StorageAPI{}, err
|
||||
}
|
||||
|
||||
if _, err = obj.PutObject(bucket, object, hashReader, nil); err != nil {
|
||||
return []StorageAPI{}, err
|
||||
}
|
||||
|
||||
// Remove the content of export dir 10 but preserve .minio.sys because it is automatically
|
||||
// created when minio starts
|
||||
for i := 3; i <= 5; i++ {
|
||||
@@ -288,8 +292,12 @@ func TestFormatXLHealFreshDisks(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Attempt to load all `format.json`.
|
||||
formatConfigs, _ := loadAllFormats(storageDisks)
|
||||
|
||||
// Start healing disks
|
||||
err = healFormatXLFreshDisks(storageDisks)
|
||||
err = healFormatXLFreshDisks(storageDisks, formatConfigs)
|
||||
if err != nil {
|
||||
t.Fatal("healing corrupted disk failed: ", err)
|
||||
}
|
||||
@@ -304,42 +312,6 @@ func TestFormatXLHealFreshDisks(t *testing.T) {
|
||||
removeRoots(fsDirs)
|
||||
}
|
||||
|
||||
func TestFormatXLHealFreshDisksErrorExpected(t *testing.T) {
|
||||
nDisks := 16
|
||||
fsDirs, err := getRandomDisks(nDisks)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Create an instance of xl backend.
|
||||
obj, _, err := initObjectLayer(mustGetNewEndpointList(fsDirs...))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
storageDisks, err := prepareFormatXLHealFreshDisks(obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Prepares all disks are offline.
|
||||
prepareNOfflineDisks(storageDisks, 16, t)
|
||||
|
||||
// Load again XL format.json to validate it
|
||||
_, err = loadFormatXL(storageDisks, 8)
|
||||
if err == nil {
|
||||
t.Fatal("loading format disk error")
|
||||
}
|
||||
|
||||
storageDisks[3] = nil
|
||||
err = healFormatXLFreshDisks(storageDisks)
|
||||
if err != nil {
|
||||
t.Fatal("didn't get nil when one disk is offline")
|
||||
}
|
||||
|
||||
// Clean all
|
||||
removeRoots(fsDirs)
|
||||
}
|
||||
|
||||
// Simulate XL disks creation, delete some format.json and remove the content of
|
||||
// a given disk to test healing a corrupted disk
|
||||
func TestFormatXLHealCorruptedDisks(t *testing.T) {
|
||||
@@ -358,9 +330,8 @@ func TestFormatXLHealCorruptedDisks(t *testing.T) {
|
||||
|
||||
bucket := "bucket"
|
||||
object := "object"
|
||||
sha256sum := ""
|
||||
|
||||
_, err = obj.PutObject(bucket, object, int64(len("abcd")), bytes.NewReader([]byte("abcd")), nil, sha256sum)
|
||||
_, err = obj.PutObject(bucket, object, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -397,8 +368,10 @@ func TestFormatXLHealCorruptedDisks(t *testing.T) {
|
||||
xl.storageDisks[3], xl.storageDisks[10], xl.storageDisks[12], xl.storageDisks[9],
|
||||
xl.storageDisks[5], xl.storageDisks[11]}
|
||||
|
||||
formatConfigs, _ := loadAllFormats(permutedStorageDisks)
|
||||
|
||||
// Start healing disks
|
||||
err = healFormatXLCorruptedDisks(permutedStorageDisks)
|
||||
err = healFormatXLCorruptedDisks(permutedStorageDisks, formatConfigs)
|
||||
if err != nil {
|
||||
t.Fatal("healing corrupted disk failed: ", err)
|
||||
}
|
||||
@@ -431,9 +404,8 @@ func TestFormatXLReorderByInspection(t *testing.T) {
|
||||
|
||||
bucket := "bucket"
|
||||
object := "object"
|
||||
sha256sum := ""
|
||||
|
||||
_, err = obj.PutObject(bucket, object, int64(len("abcd")), bytes.NewReader([]byte("abcd")), nil, sha256sum)
|
||||
_, err = obj.PutObject(bucket, object, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -454,7 +426,7 @@ func TestFormatXLReorderByInspection(t *testing.T) {
|
||||
|
||||
permutedFormatConfigs, _ := loadAllFormats(permutedStorageDisks)
|
||||
|
||||
orderedDisks, err := reorderDisks(permutedStorageDisks, permutedFormatConfigs)
|
||||
_, orderedDisks, err := reorderDisks(permutedStorageDisks, permutedFormatConfigs, false)
|
||||
if err != nil {
|
||||
t.Fatal("error reordering disks\n")
|
||||
}
|
||||
@@ -629,27 +601,29 @@ func TestInitFormatXLErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test for reduceFormatErrs()
|
||||
func TestReduceFormatErrs(t *testing.T) {
|
||||
// No error founds
|
||||
if err := reduceFormatErrs([]error{nil, nil, nil, nil}, 4); err != nil {
|
||||
t.Fatal("Err should be nil, found: ", err)
|
||||
// Test formatErrsSummary()
|
||||
func TestFormatErrsSummary(t *testing.T) {
|
||||
type errSummary struct {
|
||||
fc, unfmt, ntfnd, crrptd, othr int
|
||||
}
|
||||
// One corrupted format
|
||||
if err := reduceFormatErrs([]error{nil, nil, errCorruptedFormat, nil}, 4); err != errCorruptedFormat {
|
||||
t.Fatal("Got a different error: ", err)
|
||||
|
||||
testCases := []struct {
|
||||
errs []error
|
||||
expected errSummary
|
||||
}{
|
||||
{nil, errSummary{0, 0, 0, 0, 0}},
|
||||
{[]error{errDiskNotFound, errUnformattedDisk, errCorruptedFormat, nil, errFaultyDisk},
|
||||
errSummary{1, 1, 1, 1, 1}},
|
||||
{[]error{errDiskNotFound, errDiskNotFound, errCorruptedFormat, nil, nil},
|
||||
errSummary{2, 0, 2, 1, 0}},
|
||||
}
|
||||
// All disks unformatted
|
||||
if err := reduceFormatErrs([]error{errUnformattedDisk, errUnformattedDisk, errUnformattedDisk, errUnformattedDisk}, 4); err != errUnformattedDisk {
|
||||
t.Fatal("Got a different error: ", err)
|
||||
}
|
||||
// Some disks unformatted
|
||||
if err := reduceFormatErrs([]error{nil, nil, errUnformattedDisk, errUnformattedDisk}, 4); err != errSomeDiskUnformatted {
|
||||
t.Fatal("Got a different error: ", err)
|
||||
}
|
||||
// Some disks offline
|
||||
if err := reduceFormatErrs([]error{nil, nil, errDiskNotFound, errUnformattedDisk}, 4); err != errSomeDiskOffline {
|
||||
t.Fatal("Got a different error: ", err)
|
||||
for i, testCase := range testCases {
|
||||
a, b, c, d, e := formatErrsSummary(testCase.errs)
|
||||
got := errSummary{a, b, c, d, e}
|
||||
if got != testCase.expected {
|
||||
t.Errorf("Test %d: Got wrong results: %#v %#v", i+1,
|
||||
got, testCase.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,7 +665,7 @@ func TestGenericFormatCheckXL(t *testing.T) {
|
||||
func TestFSCheckFormatFSErr(t *testing.T) {
|
||||
// Prepare for testing
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer removeAll(disk)
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
// Assign a new UUID.
|
||||
uuid := mustGetUUID()
|
||||
@@ -753,7 +727,7 @@ func TestFSCheckFormatFSErr(t *testing.T) {
|
||||
|
||||
fsFormatPath := pathJoin(disk, minioMetaBucket, formatConfigFile)
|
||||
for i, testCase := range testCases {
|
||||
lk, err := lock.LockedOpenFile(preparePath(fsFormatPath), os.O_RDWR|os.O_CREATE, 0600)
|
||||
lk, err := lock.LockedOpenFile((fsFormatPath), os.O_RDWR|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -763,7 +737,7 @@ func TestFSCheckFormatFSErr(t *testing.T) {
|
||||
t.Fatalf("Test %d: Expected nil, got %s", i+1, err)
|
||||
}
|
||||
|
||||
lk, err = lock.LockedOpenFile(preparePath(fsFormatPath), os.O_RDWR|os.O_CREATE, 0600)
|
||||
lk, err = lock.LockedOpenFile((fsFormatPath), os.O_RDWR|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -793,7 +767,7 @@ func TestFSCheckFormatFSErr(t *testing.T) {
|
||||
func TestFSCheckFormatFS(t *testing.T) {
|
||||
// Prepare for testing
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer removeAll(disk)
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
// Assign a new UUID.
|
||||
uuid := mustGetUUID()
|
||||
@@ -804,7 +778,7 @@ func TestFSCheckFormatFS(t *testing.T) {
|
||||
}
|
||||
|
||||
fsFormatPath := pathJoin(disk, minioMetaBucket, formatConfigFile)
|
||||
lk, err := lock.LockedOpenFile(preparePath(fsFormatPath), os.O_RDWR|os.O_CREATE, 0600)
|
||||
lk, err := lock.LockedOpenFile((fsFormatPath), os.O_RDWR|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -817,14 +791,14 @@ func TestFSCheckFormatFS(t *testing.T) {
|
||||
}
|
||||
|
||||
// Loading corrupted format file
|
||||
file, err := os.OpenFile(preparePath(fsFormatPath), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666)
|
||||
file, err := os.OpenFile((fsFormatPath), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
t.Fatal("Should not fail here", err)
|
||||
}
|
||||
file.Write([]byte{'b'})
|
||||
file.Close()
|
||||
|
||||
lk, err = lock.LockedOpenFile(preparePath(fsFormatPath), os.O_RDWR|os.O_CREATE, 0600)
|
||||
lk, err = lock.LockedOpenFile((fsFormatPath), os.O_RDWR|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -837,8 +811,8 @@ func TestFSCheckFormatFS(t *testing.T) {
|
||||
}
|
||||
|
||||
// Loading format file from disk not found.
|
||||
removeAll(disk)
|
||||
_, err = lock.LockedOpenFile(preparePath(fsFormatPath), os.O_RDONLY, 0600)
|
||||
os.RemoveAll(disk)
|
||||
_, err = lock.LockedOpenFile((fsFormatPath), os.O_RDONLY, 0600)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal("Should return 'format.json' does not exist, but got", err)
|
||||
}
|
||||
@@ -946,7 +920,7 @@ func TestHealFormatXLCorruptedDisksErrs(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer removeAll(root)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
nDisks := 16
|
||||
fsDirs, err := getRandomDisks(nDisks)
|
||||
@@ -959,8 +933,10 @@ func TestHealFormatXLCorruptedDisksErrs(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
xl := obj.(*xlObjects)
|
||||
if err = healFormatXLCorruptedDisks(xl.storageDisks); err != nil {
|
||||
formatConfigs, _ := loadAllFormats(xl.storageDisks)
|
||||
if err = healFormatXLCorruptedDisks(xl.storageDisks, formatConfigs); err != nil {
|
||||
t.Fatal("Got an unexpected error: ", err)
|
||||
}
|
||||
|
||||
@@ -971,25 +947,6 @@ func TestHealFormatXLCorruptedDisksErrs(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Disks 0..15 are nil
|
||||
obj, _, err = initObjectLayer(mustGetNewEndpointList(fsDirs...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
xl = obj.(*xlObjects)
|
||||
for i := 0; i <= 15; i++ {
|
||||
xl.storageDisks[i] = nil
|
||||
}
|
||||
if err = healFormatXLCorruptedDisks(xl.storageDisks); err != nil {
|
||||
t.Fatal("Got an unexpected error: ", err)
|
||||
}
|
||||
removeRoots(fsDirs)
|
||||
|
||||
fsDirs, err = getRandomDisks(nDisks)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// One disk returns Faulty Disk
|
||||
obj, _, err = initObjectLayer(mustGetNewEndpointList(fsDirs...))
|
||||
if err != nil {
|
||||
@@ -1001,45 +958,8 @@ func TestHealFormatXLCorruptedDisksErrs(t *testing.T) {
|
||||
t.Fatal("storage disk is not *retryStorage type")
|
||||
}
|
||||
xl.storageDisks[0] = newNaughtyDisk(posixDisk, nil, errFaultyDisk)
|
||||
if err = healFormatXLCorruptedDisks(xl.storageDisks); err != errFaultyDisk {
|
||||
t.Fatal("Got an unexpected error: ", err)
|
||||
}
|
||||
removeRoots(fsDirs)
|
||||
|
||||
fsDirs, err = getRandomDisks(nDisks)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// One disk is not found, heal corrupted disks should return nil
|
||||
obj, _, err = initObjectLayer(mustGetNewEndpointList(fsDirs...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
xl = obj.(*xlObjects)
|
||||
xl.storageDisks[0] = nil
|
||||
if err = healFormatXLCorruptedDisks(xl.storageDisks); err != nil {
|
||||
t.Fatal("Got an unexpected error: ", err)
|
||||
}
|
||||
removeRoots(fsDirs)
|
||||
|
||||
fsDirs, err = getRandomDisks(nDisks)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Remove format.json of all disks
|
||||
obj, _, err = initObjectLayer(mustGetNewEndpointList(fsDirs...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
xl = obj.(*xlObjects)
|
||||
for i := 0; i <= 15; i++ {
|
||||
if err = xl.storageDisks[i].DeleteFile(minioMetaBucket, formatConfigFile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err = healFormatXLCorruptedDisks(xl.storageDisks); err != nil {
|
||||
formatConfigs, _ = loadAllFormats(xl.storageDisks)
|
||||
if err = healFormatXLCorruptedDisks(xl.storageDisks, formatConfigs); err != errFaultyDisk {
|
||||
t.Fatal("Got an unexpected error: ", err)
|
||||
}
|
||||
removeRoots(fsDirs)
|
||||
@@ -1060,7 +980,8 @@ func TestHealFormatXLCorruptedDisksErrs(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err = healFormatXLCorruptedDisks(xl.storageDisks); err == nil {
|
||||
formatConfigs, _ = loadAllFormats(xl.storageDisks)
|
||||
if err = healFormatXLCorruptedDisks(xl.storageDisks, formatConfigs); err == nil {
|
||||
t.Fatal("Should get a json parsing error, ")
|
||||
}
|
||||
removeRoots(fsDirs)
|
||||
@@ -1072,7 +993,7 @@ func TestHealFormatXLFreshDisksErrs(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer removeAll(root)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
nDisks := 16
|
||||
fsDirs, err := getRandomDisks(nDisks)
|
||||
@@ -1086,26 +1007,8 @@ func TestHealFormatXLFreshDisksErrs(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
xl := obj.(*xlObjects)
|
||||
if err = healFormatXLFreshDisks(xl.storageDisks); err != nil {
|
||||
t.Fatal("Got an unexpected error: ", err)
|
||||
}
|
||||
removeRoots(fsDirs)
|
||||
|
||||
fsDirs, err = getRandomDisks(nDisks)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Disks 0..15 are nil
|
||||
obj, _, err = initObjectLayer(mustGetNewEndpointList(fsDirs...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
xl = obj.(*xlObjects)
|
||||
for i := 0; i <= 15; i++ {
|
||||
xl.storageDisks[i] = nil
|
||||
}
|
||||
if err = healFormatXLFreshDisks(xl.storageDisks); err != nil {
|
||||
formatConfigs, _ := loadAllFormats(xl.storageDisks)
|
||||
if err = healFormatXLFreshDisks(xl.storageDisks, formatConfigs); err != nil {
|
||||
t.Fatal("Got an unexpected error: ", err)
|
||||
}
|
||||
removeRoots(fsDirs)
|
||||
@@ -1126,7 +1029,8 @@ func TestHealFormatXLFreshDisksErrs(t *testing.T) {
|
||||
t.Fatal("storage disk is not *retryStorage type")
|
||||
}
|
||||
xl.storageDisks[0] = newNaughtyDisk(posixDisk, nil, errFaultyDisk)
|
||||
if err = healFormatXLFreshDisks(xl.storageDisks); err != errFaultyDisk {
|
||||
formatConfigs, _ = loadAllFormats(xl.storageDisks)
|
||||
if err = healFormatXLFreshDisks(xl.storageDisks, formatConfigs); err != errFaultyDisk {
|
||||
t.Fatal("Got an unexpected error: ", err)
|
||||
}
|
||||
removeRoots(fsDirs)
|
||||
@@ -1143,59 +1047,9 @@ func TestHealFormatXLFreshDisksErrs(t *testing.T) {
|
||||
}
|
||||
xl = obj.(*xlObjects)
|
||||
xl.storageDisks[0] = nil
|
||||
if err = healFormatXLFreshDisks(xl.storageDisks); err != nil {
|
||||
t.Fatal("Got an unexpected error: ", err)
|
||||
}
|
||||
removeRoots(fsDirs)
|
||||
|
||||
fsDirs, err = getRandomDisks(nDisks)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Remove format.json of all disks
|
||||
obj, _, err = initObjectLayer(mustGetNewEndpointList(fsDirs...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
xl = obj.(*xlObjects)
|
||||
for i := 0; i <= 15; i++ {
|
||||
if err = xl.storageDisks[i].DeleteFile(minioMetaBucket, formatConfigFile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err = healFormatXLFreshDisks(xl.storageDisks); err != nil {
|
||||
formatConfigs, _ = loadAllFormats(xl.storageDisks)
|
||||
if err = healFormatXLFreshDisks(xl.storageDisks, formatConfigs); err != nil {
|
||||
t.Fatal("Got an unexpected error: ", err)
|
||||
}
|
||||
removeRoots(fsDirs)
|
||||
}
|
||||
|
||||
// Tests for isFormatFound()
|
||||
func TestIsFormatFound(t *testing.T) {
|
||||
formats := genFormatXLValid()
|
||||
if found := isFormatFound(formats); !found {
|
||||
t.Fatal("isFormatFound() should not return false")
|
||||
}
|
||||
formats[0] = nil
|
||||
if found := isFormatFound(formats); found {
|
||||
t.Fatal("isFormatFound() should not return true")
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for isFormatNotFound()
|
||||
func TestIsFormatNotFound(t *testing.T) {
|
||||
formats := genFormatXLValid()
|
||||
if found := isFormatNotFound(formats); found {
|
||||
t.Fatal("isFormatFound() should not return true")
|
||||
}
|
||||
formats[0] = nil
|
||||
if found := isFormatNotFound(formats); found {
|
||||
t.Fatal("isFormatFound() should not return true")
|
||||
}
|
||||
for idx := range formats {
|
||||
formats[idx] = nil
|
||||
}
|
||||
if found := isFormatNotFound(formats); !found {
|
||||
t.Fatal("isFormatFound() should not return false")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ func (fs fsObjects) appendPart(bucket, object, uploadID string, part objectPartI
|
||||
tmpObjPath := pathJoin(fs.fsPath, minioMetaTmpBucket, fs.fsUUID, uploadID)
|
||||
// No need to hold a lock, this is a unique file and will be only written
|
||||
// to one one process per uploadID per minio process.
|
||||
wfile, err := os.OpenFile(preparePath(tmpObjPath), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
|
||||
wfile, err := os.OpenFile((tmpObjPath), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+91
-104
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
pathutil "path"
|
||||
@@ -35,13 +36,8 @@ func fsRemoveFile(filePath string) (err error) {
|
||||
return traceError(err)
|
||||
}
|
||||
|
||||
if err = os.Remove(preparePath(filePath)); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return traceError(errFileNotFound)
|
||||
} else if os.IsPermission(err) {
|
||||
return traceError(errFileAccessDenied)
|
||||
}
|
||||
return traceError(err)
|
||||
if err = os.Remove((filePath)); err != nil {
|
||||
return osErrToFSFileErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -58,9 +54,11 @@ func fsRemoveAll(dirPath string) (err error) {
|
||||
return traceError(err)
|
||||
}
|
||||
|
||||
if err = removeAll(dirPath); err != nil {
|
||||
if err = os.RemoveAll(dirPath); err != nil {
|
||||
if os.IsPermission(err) {
|
||||
return traceError(errVolumeAccessDenied)
|
||||
} else if isSysErrNotEmpty(err) {
|
||||
return traceError(errVolumeNotEmpty)
|
||||
}
|
||||
return traceError(err)
|
||||
}
|
||||
@@ -79,7 +77,7 @@ func fsRemoveDir(dirPath string) (err error) {
|
||||
return traceError(err)
|
||||
}
|
||||
|
||||
if err = os.Remove(preparePath(dirPath)); err != nil {
|
||||
if err = os.Remove((dirPath)); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return traceError(errVolumeNotFound)
|
||||
} else if isSysErrNotEmpty(err) {
|
||||
@@ -91,6 +89,24 @@ func fsRemoveDir(dirPath string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Creates a new directory, parent dir is also recursively created
|
||||
// if it doesn't exist.
|
||||
func fsMkdirAll(dirPath string) (err error) {
|
||||
if dirPath == "" {
|
||||
return traceError(errInvalidArgument)
|
||||
}
|
||||
|
||||
if err = checkPathLength(dirPath); err != nil {
|
||||
return traceError(err)
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(dirPath, 0777); err != nil {
|
||||
return traceError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Creates a new directory, parent dir should exist
|
||||
// otherwise returns an error. If directory already
|
||||
// exists returns an error. Windows long paths
|
||||
@@ -104,7 +120,7 @@ func fsMkdir(dirPath string) (err error) {
|
||||
return traceError(err)
|
||||
}
|
||||
|
||||
if err = os.Mkdir(preparePath(dirPath), 0777); err != nil {
|
||||
if err = os.Mkdir((dirPath), 0777); err != nil {
|
||||
if os.IsExist(err) {
|
||||
return traceError(errVolumeExists)
|
||||
} else if os.IsPermission(err) {
|
||||
@@ -123,6 +139,11 @@ func fsMkdir(dirPath string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// fsStat is a low level call which validates input arguments
|
||||
// and checks input length upto supported maximum. Does
|
||||
// not perform any higher layer interpretation of files v/s
|
||||
// directories. For higher level interpretation look at
|
||||
// fsStatFileDir, fsStatFile, fsStatDir.
|
||||
func fsStat(statLoc string) (os.FileInfo, error) {
|
||||
if statLoc == "" {
|
||||
return nil, traceError(errInvalidArgument)
|
||||
@@ -130,7 +151,7 @@ func fsStat(statLoc string) (os.FileInfo, error) {
|
||||
if err := checkPathLength(statLoc); err != nil {
|
||||
return nil, traceError(err)
|
||||
}
|
||||
fi, err := osStat(preparePath(statLoc))
|
||||
fi, err := os.Stat((statLoc))
|
||||
if err != nil {
|
||||
return nil, traceError(err)
|
||||
}
|
||||
@@ -138,10 +159,9 @@ func fsStat(statLoc string) (os.FileInfo, error) {
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
// Lookup if directory exists, returns directory
|
||||
// attributes upon success.
|
||||
func fsStatDir(statDir string) (os.FileInfo, error) {
|
||||
fi, err := fsStat(statDir)
|
||||
// Lookup if volume exists, returns volume attributes upon success.
|
||||
func fsStatVolume(volume string) (os.FileInfo, error) {
|
||||
fi, err := fsStat(volume)
|
||||
if err != nil {
|
||||
err = errorCause(err)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -159,21 +179,47 @@ func fsStatDir(statDir string) (os.FileInfo, error) {
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
// Lookup if file exists, returns file attributes upon success
|
||||
// Is a one place function which converts all os.PathError
|
||||
// into a more FS object layer friendly form, converts
|
||||
// known errors into their typed form for top level
|
||||
// interpretation.
|
||||
func osErrToFSFileErr(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = errorCause(err)
|
||||
if os.IsNotExist(err) {
|
||||
return traceError(errFileNotFound)
|
||||
}
|
||||
if os.IsPermission(err) {
|
||||
return traceError(errFileAccessDenied)
|
||||
}
|
||||
if isSysErrNotDir(err) {
|
||||
return traceError(errFileAccessDenied)
|
||||
}
|
||||
if isSysErrPathNotFound(err) {
|
||||
return traceError(errFileNotFound)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Lookup if directory exists, returns directory attributes upon success.
|
||||
func fsStatDir(statDir string) (os.FileInfo, error) {
|
||||
fi, err := fsStat(statDir)
|
||||
if err != nil {
|
||||
return nil, osErrToFSFileErr(err)
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return nil, traceError(errFileAccessDenied)
|
||||
}
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
// Lookup if file exists, returns file attributes upon success.
|
||||
func fsStatFile(statFile string) (os.FileInfo, error) {
|
||||
fi, err := fsStat(statFile)
|
||||
if err != nil {
|
||||
err = errorCause(err)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, traceError(errFileNotFound)
|
||||
} else if os.IsPermission(err) {
|
||||
return nil, traceError(errFileAccessDenied)
|
||||
} else if isSysErrNotDir(err) {
|
||||
return nil, traceError(errFileAccessDenied)
|
||||
} else if isSysErrPathNotFound(err) {
|
||||
return nil, traceError(errFileNotFound)
|
||||
}
|
||||
return nil, traceError(err)
|
||||
return nil, osErrToFSFileErr(err)
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return nil, traceError(errFileAccessDenied)
|
||||
@@ -191,24 +237,13 @@ func fsOpenFile(readPath string, offset int64) (io.ReadCloser, int64, error) {
|
||||
return nil, 0, traceError(err)
|
||||
}
|
||||
|
||||
fr, err := os.Open(preparePath(readPath))
|
||||
fr, err := os.Open((readPath))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, 0, traceError(errFileNotFound)
|
||||
} else if os.IsPermission(err) {
|
||||
return nil, 0, traceError(errFileAccessDenied)
|
||||
} else if isSysErrNotDir(err) {
|
||||
// File path cannot be verified since one of the parents is a file.
|
||||
return nil, 0, traceError(errFileAccessDenied)
|
||||
} else if isSysErrPathNotFound(err) {
|
||||
// Add specific case for windows.
|
||||
return nil, 0, traceError(errFileNotFound)
|
||||
}
|
||||
return nil, 0, traceError(err)
|
||||
return nil, 0, osErrToFSFileErr(err)
|
||||
}
|
||||
|
||||
// Stat to get the size of the file at path.
|
||||
st, err := osStat(preparePath(readPath))
|
||||
st, err := os.Stat((readPath))
|
||||
if err != nil {
|
||||
return nil, 0, traceError(err)
|
||||
}
|
||||
@@ -240,7 +275,7 @@ func fsCreateFile(filePath string, reader io.Reader, buf []byte, fallocSize int6
|
||||
return 0, traceError(err)
|
||||
}
|
||||
|
||||
if err := mkdirAll(pathutil.Dir(filePath), 0777); err != nil {
|
||||
if err := os.MkdirAll(pathutil.Dir(filePath), 0777); err != nil {
|
||||
return 0, traceError(err)
|
||||
}
|
||||
|
||||
@@ -248,13 +283,9 @@ func fsCreateFile(filePath string, reader io.Reader, buf []byte, fallocSize int6
|
||||
return 0, traceError(err)
|
||||
}
|
||||
|
||||
writer, err := os.OpenFile(preparePath(filePath), os.O_CREATE|os.O_WRONLY, 0666)
|
||||
writer, err := os.OpenFile((filePath), os.O_CREATE|os.O_WRONLY, 0666)
|
||||
if err != nil {
|
||||
// File path cannot be verified since one of the parents is a file.
|
||||
if isSysErrNotDir(err) {
|
||||
return 0, traceError(errFileAccessDenied)
|
||||
}
|
||||
return 0, err
|
||||
return 0, osErrToFSFileErr(err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
@@ -306,6 +337,7 @@ func fsRemoveUploadIDPath(basePath, uploadIDPath string) error {
|
||||
}
|
||||
}
|
||||
|
||||
fsRemoveDir(uploadIDPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -341,30 +373,22 @@ func fsRenameFile(sourcePath, destPath string) error {
|
||||
return traceError(err)
|
||||
}
|
||||
// Verify if source path exists.
|
||||
if _, err := os.Stat(preparePath(sourcePath)); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return traceError(errFileNotFound)
|
||||
} else if os.IsPermission(err) {
|
||||
return traceError(errFileAccessDenied)
|
||||
} else if isSysErrPathNotFound(err) {
|
||||
return traceError(errFileNotFound)
|
||||
} else if isSysErrNotDir(err) {
|
||||
// File path cannot be verified since one of the parents is a file.
|
||||
return traceError(errFileAccessDenied)
|
||||
if _, err := os.Stat((sourcePath)); err != nil {
|
||||
return osErrToFSFileErr(err)
|
||||
}
|
||||
if err := os.MkdirAll(pathutil.Dir(destPath), 0777); err != nil {
|
||||
return traceError(err)
|
||||
}
|
||||
if err := os.Rename((sourcePath), (destPath)); err != nil {
|
||||
if isSysErrCrossDevice(err) {
|
||||
return traceError(fmt.Errorf("%s (%s)->(%s)", errCrossDeviceLink, sourcePath, destPath))
|
||||
}
|
||||
return traceError(err)
|
||||
}
|
||||
if err := mkdirAll(pathutil.Dir(destPath), 0777); err != nil {
|
||||
return traceError(err)
|
||||
}
|
||||
if err := os.Rename(preparePath(sourcePath), preparePath(destPath)); err != nil {
|
||||
return traceError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete a file and its parent if it is empty at the destination path.
|
||||
// this function additionally protects the basePath from being deleted.
|
||||
// fsDeleteFile is a wrapper for deleteFile(), after checking the path length.
|
||||
func fsDeleteFile(basePath, deletePath string) error {
|
||||
if err := checkPathLength(basePath); err != nil {
|
||||
return traceError(err)
|
||||
@@ -374,44 +398,7 @@ func fsDeleteFile(basePath, deletePath string) error {
|
||||
return traceError(err)
|
||||
}
|
||||
|
||||
if basePath == deletePath {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify if the path exists.
|
||||
pathSt, err := osStat(preparePath(deletePath))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return traceError(errFileNotFound)
|
||||
} else if os.IsPermission(err) {
|
||||
return traceError(errFileAccessDenied)
|
||||
}
|
||||
return traceError(err)
|
||||
}
|
||||
|
||||
if pathSt.IsDir() && !isDirEmpty(deletePath) {
|
||||
// Verify if directory is empty.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Attempt to remove path.
|
||||
if err = os.Remove(preparePath(deletePath)); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return traceError(errFileNotFound)
|
||||
} else if os.IsPermission(err) {
|
||||
return traceError(errFileAccessDenied)
|
||||
} else if isSysErrNotEmpty(err) {
|
||||
return traceError(errVolumeNotEmpty)
|
||||
}
|
||||
return traceError(err)
|
||||
}
|
||||
|
||||
// Recursively go down the next path and delete again.
|
||||
if err := fsDeleteFile(basePath, pathutil.Dir(deletePath)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return deleteFile(basePath, deletePath)
|
||||
}
|
||||
|
||||
// fsRemoveMeta safely removes a locked file and takes care of Windows special case
|
||||
|
||||
+111
-15
@@ -18,6 +18,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
@@ -26,13 +27,35 @@ import (
|
||||
"github.com/minio/minio/pkg/lock"
|
||||
)
|
||||
|
||||
// Tests - fsMkdirAll()
|
||||
func TestFSMkdirAll(t *testing.T) {
|
||||
// create posix test setup
|
||||
_, path, err := newPosixTestSetup()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create posix test setup, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
if err = fsMkdirAll(""); errorCause(err) != errInvalidArgument {
|
||||
t.Fatal("Unexpected error", err)
|
||||
}
|
||||
|
||||
if err = fsMkdirAll(pathJoin(path, "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001")); errorCause(err) != errFileNameTooLong {
|
||||
t.Fatal("Unexpected error", err)
|
||||
}
|
||||
|
||||
if err = fsMkdirAll(pathJoin(path, "success-vol", "success-object")); err != nil {
|
||||
t.Fatal("Unexpected error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSRenameFile(t *testing.T) {
|
||||
// create posix test setup
|
||||
_, path, err := newPosixTestSetup()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create posix test setup, %s", err)
|
||||
}
|
||||
defer removeAll(path)
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
if err = fsMkdir(pathJoin(path, "testvolume1")); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -57,7 +80,7 @@ func TestFSStats(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create posix test setup, %s", err)
|
||||
}
|
||||
defer removeAll(path)
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
// Setup test environment.
|
||||
|
||||
@@ -172,7 +195,7 @@ func TestFSStats(t *testing.T) {
|
||||
t.Fatalf("TestPosix case %d: Expected: \"%s\", got: \"%s\"", i+1, testCase.expectedErr, err)
|
||||
}
|
||||
} else {
|
||||
if _, err := fsStatDir(pathJoin(testCase.srcFSPath, testCase.srcVol)); errorCause(err) != testCase.expectedErr {
|
||||
if _, err := fsStatVolume(pathJoin(testCase.srcFSPath, testCase.srcVol)); errorCause(err) != testCase.expectedErr {
|
||||
t.Fatalf("TestPosix case %d: Expected: \"%s\", got: \"%s\"", i+1, testCase.expectedErr, err)
|
||||
}
|
||||
}
|
||||
@@ -185,7 +208,7 @@ func TestFSCreateAndOpen(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create posix test setup, %s", err)
|
||||
}
|
||||
defer removeAll(path)
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
if err = fsMkdir(pathJoin(path, "success-vol")); err != nil {
|
||||
t.Fatalf("Unable to create directory, %s", err)
|
||||
@@ -250,7 +273,7 @@ func TestFSDeletes(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create posix test setup, %s", err)
|
||||
}
|
||||
defer removeAll(path)
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
// Setup test environment.
|
||||
if err = fsMkdir(pathJoin(path, "success-vol")); err != nil {
|
||||
@@ -263,50 +286,123 @@ func TestFSDeletes(t *testing.T) {
|
||||
t.Fatalf("Unable to create file, %s", err)
|
||||
}
|
||||
// Seek back.
|
||||
reader.Seek(0, 0)
|
||||
reader.Seek(0, io.SeekStart)
|
||||
|
||||
// folder is not empty
|
||||
err = fsMkdir(pathJoin(path, "success-vol", "not-empty"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = ioutil.WriteFile(pathJoin(path, "success-vol", "not-empty", "file"), []byte("data"), 0777)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// recursive
|
||||
if err = fsMkdir(pathJoin(path, "success-vol", "parent")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = fsMkdir(pathJoin(path, "success-vol", "parent", "dir")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
basePath string
|
||||
srcVol string
|
||||
srcPath string
|
||||
expectedErr error
|
||||
}{
|
||||
// Test case - 1.
|
||||
// valid case with existing volume and file to delete.
|
||||
{
|
||||
basePath: path,
|
||||
srcVol: "success-vol",
|
||||
srcPath: "success-file",
|
||||
expectedErr: nil,
|
||||
},
|
||||
// Test case - 2.
|
||||
// The file was deleted in the last case, so DeleteFile should fail.
|
||||
{
|
||||
basePath: path,
|
||||
srcVol: "success-vol",
|
||||
srcPath: "success-file",
|
||||
expectedErr: errFileNotFound,
|
||||
},
|
||||
// Test case - 3.
|
||||
// Test case with segment of the volume name > 255.
|
||||
{
|
||||
basePath: path,
|
||||
srcVol: "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
|
||||
srcPath: "success-file",
|
||||
expectedErr: errFileNameTooLong,
|
||||
},
|
||||
// Test case - 4.
|
||||
// Test case with src path segment > 255.
|
||||
{
|
||||
basePath: path,
|
||||
srcVol: "success-vol",
|
||||
srcPath: "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
|
||||
expectedErr: errFileNameTooLong,
|
||||
},
|
||||
// Base path is way too long.
|
||||
{
|
||||
basePath: "path03333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
|
||||
srcVol: "success-vol",
|
||||
srcPath: "object",
|
||||
expectedErr: errFileNameTooLong,
|
||||
},
|
||||
// Directory is not empty. Should give nil, but won't delete.
|
||||
{
|
||||
basePath: path,
|
||||
srcVol: "success-vol",
|
||||
srcPath: "not-empty",
|
||||
expectedErr: nil,
|
||||
},
|
||||
// Should delete recursively.
|
||||
{
|
||||
basePath: path,
|
||||
srcVol: "success-vol",
|
||||
srcPath: pathJoin("parent", "dir"),
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
if err = fsDeleteFile(path, pathJoin(path, testCase.srcVol, testCase.srcPath)); errorCause(err) != testCase.expectedErr {
|
||||
if err = fsDeleteFile(testCase.basePath, pathJoin(testCase.basePath, testCase.srcVol, testCase.srcPath)); errorCause(err) != testCase.expectedErr {
|
||||
t.Errorf("Test case %d: Expected: \"%s\", got: \"%s\"", i+1, testCase.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFSDeleteFile(b *testing.B) {
|
||||
// create posix test setup
|
||||
_, path, err := newPosixTestSetup()
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to create posix test setup, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
// Setup test environment.
|
||||
if err = fsMkdir(pathJoin(path, "benchmark")); err != nil {
|
||||
b.Fatalf("Unable to create directory, %s", err)
|
||||
}
|
||||
|
||||
benchDir := pathJoin(path, "benchmark")
|
||||
filename := pathJoin(benchDir, "file.txt")
|
||||
|
||||
b.ResetTimer()
|
||||
// We need to create and delete the file sequentially inside the benchmark.
|
||||
for i := 0; i < b.N; i++ {
|
||||
b.StopTimer()
|
||||
err = ioutil.WriteFile(filename, []byte("data"), 0777)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.StartTimer()
|
||||
|
||||
err = fsDeleteFile(benchDir, filename)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests fs removes.
|
||||
func TestFSRemoves(t *testing.T) {
|
||||
// create posix test setup
|
||||
@@ -314,7 +410,7 @@ func TestFSRemoves(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create posix test setup, %s", err)
|
||||
}
|
||||
defer removeAll(path)
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
// Setup test environment.
|
||||
if err = fsMkdir(pathJoin(path, "success-vol")); err != nil {
|
||||
@@ -431,7 +527,7 @@ func TestFSRemoveMeta(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create posix test setup, %s", err)
|
||||
}
|
||||
defer removeAll(fsPath)
|
||||
defer os.RemoveAll(fsPath)
|
||||
|
||||
// Setup test environment.
|
||||
if err = fsMkdir(pathJoin(fsPath, "success-vol")); err != nil {
|
||||
@@ -464,11 +560,11 @@ func TestFSRemoveMeta(t *testing.T) {
|
||||
t.Fatalf("Unable to remove file, %s", err)
|
||||
}
|
||||
|
||||
if _, err := osStat(preparePath(filePath)); !os.IsNotExist(err) {
|
||||
if _, err := os.Stat((filePath)); !os.IsNotExist(err) {
|
||||
t.Fatalf("`%s` file found though it should have been deleted.", filePath)
|
||||
}
|
||||
|
||||
if _, err := osStat(preparePath(path.Dir(filePath))); !os.IsNotExist(err) {
|
||||
if _, err := os.Stat((path.Dir(filePath))); !os.IsNotExist(err) {
|
||||
t.Fatalf("`%s` parent directory found though it should have been deleted.", filePath)
|
||||
}
|
||||
}
|
||||
|
||||
+18
-9
@@ -91,6 +91,11 @@ func (m fsMetaV1) ToObjectInfo(bucket, object string, fi os.FileInfo) ObjectInfo
|
||||
}
|
||||
}
|
||||
|
||||
if hasSuffix(object, slashSeparator) {
|
||||
m.Meta["etag"] = emptyETag // For directories etag is d41d8cd98f00b204e9800998ecf8427e
|
||||
m.Meta["content-type"] = "application/octet-stream"
|
||||
}
|
||||
|
||||
objInfo := ObjectInfo{
|
||||
Bucket: bucket,
|
||||
Name: object,
|
||||
@@ -101,7 +106,11 @@ func (m fsMetaV1) ToObjectInfo(bucket, object string, fi os.FileInfo) ObjectInfo
|
||||
if fi != nil {
|
||||
objInfo.ModTime = fi.ModTime()
|
||||
objInfo.Size = fi.Size()
|
||||
objInfo.IsDir = fi.IsDir()
|
||||
if fi.IsDir() {
|
||||
// Directory is always 0 bytes in S3 API, treat it as such.
|
||||
objInfo.Size = 0
|
||||
objInfo.IsDir = fi.IsDir()
|
||||
}
|
||||
}
|
||||
|
||||
// Extract etag from metadata.
|
||||
@@ -264,7 +273,7 @@ func newFSMetaV1() (fsMeta fsMetaV1) {
|
||||
func checkLockedValidFormatFS(fsPath string) (*lock.RLockedFile, error) {
|
||||
fsFormatPath := pathJoin(fsPath, minioMetaBucket, formatConfigFile)
|
||||
|
||||
rlk, err := lock.RLockedOpenFile(preparePath(fsFormatPath))
|
||||
rlk, err := lock.RLockedOpenFile((fsFormatPath))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// If format.json not found then
|
||||
@@ -296,7 +305,7 @@ func createFormatFS(fsPath string) error {
|
||||
|
||||
// Attempt a write lock on formatConfigFile `format.json`
|
||||
// file stored in minioMetaBucket(.minio.sys) directory.
|
||||
lk, err := lock.TryLockedOpenFile(preparePath(fsFormatPath), os.O_RDWR|os.O_CREATE, 0600)
|
||||
lk, err := lock.TryLockedOpenFile((fsFormatPath), os.O_RDWR|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return traceError(err)
|
||||
}
|
||||
@@ -350,15 +359,15 @@ func initFormatFS(fsPath string) (rlk *lock.RLockedFile, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Return if the part info in uploadedParts and completeParts are same.
|
||||
func isPartsSame(uploadedParts []objectPartInfo, completeParts []completePart) bool {
|
||||
if len(uploadedParts) != len(completeParts) {
|
||||
// Return if the part info in uploadedParts and CompleteParts are same.
|
||||
func isPartsSame(uploadedParts []objectPartInfo, CompleteParts []CompletePart) bool {
|
||||
if len(uploadedParts) != len(CompleteParts) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range completeParts {
|
||||
if uploadedParts[i].Number != completeParts[i].PartNumber ||
|
||||
uploadedParts[i].ETag != completeParts[i].ETag {
|
||||
for i := range CompleteParts {
|
||||
if uploadedParts[i].Number != CompleteParts[i].PartNumber ||
|
||||
uploadedParts[i].ETag != CompleteParts[i].ETag {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
@@ -40,7 +41,7 @@ func TestFSV1MetadataObjInfo(t *testing.T) {
|
||||
// TestReadFSMetadata - readFSMetadata testing with a healthy and faulty disk
|
||||
func TestReadFSMetadata(t *testing.T) {
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer removeAll(disk)
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
obj := initFSObjects(disk, t)
|
||||
fs := obj.(*fsObjects)
|
||||
@@ -51,9 +52,7 @@ func TestReadFSMetadata(t *testing.T) {
|
||||
if err := obj.MakeBucketWithLocation(bucketName, ""); err != nil {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
sha256sum := ""
|
||||
if _, err := obj.PutObject(bucketName, objectName, int64(len("abcd")), bytes.NewReader([]byte("abcd")),
|
||||
map[string]string{"X-Amz-Meta-AppId": "a"}, sha256sum); err != nil {
|
||||
if _, err := obj.PutObject(bucketName, objectName, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil); err != nil {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
|
||||
@@ -77,7 +76,7 @@ func TestReadFSMetadata(t *testing.T) {
|
||||
// TestWriteFSMetadata - tests of writeFSMetadata with healthy disk.
|
||||
func TestWriteFSMetadata(t *testing.T) {
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer removeAll(disk)
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
obj := initFSObjects(disk, t)
|
||||
fs := obj.(*fsObjects)
|
||||
@@ -88,9 +87,7 @@ func TestWriteFSMetadata(t *testing.T) {
|
||||
if err := obj.MakeBucketWithLocation(bucketName, ""); err != nil {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
sha256sum := ""
|
||||
if _, err := obj.PutObject(bucketName, objectName, int64(len("abcd")), bytes.NewReader([]byte("abcd")),
|
||||
map[string]string{"X-Amz-Meta-AppId": "a"}, sha256sum); err != nil {
|
||||
if _, err := obj.PutObject(bucketName, objectName, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil); err != nil {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
|
||||
|
||||
+281
-158
@@ -17,18 +17,23 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"os"
|
||||
pathutil "path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/lock"
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
const (
|
||||
// Expiry duration after which the multipart uploads are deemed stale.
|
||||
fsMultipartExpiry = time.Hour * 24 * 14 // 2 weeks.
|
||||
// Cleanup interval when the stale multipart cleanup is initiated.
|
||||
fsMultipartCleanupInterval = time.Hour * 24 // 24 hrs.
|
||||
)
|
||||
|
||||
// Returns if the prefix is a multipart upload.
|
||||
@@ -36,7 +41,7 @@ func (fs fsObjects) isMultipartUpload(bucket, prefix string) bool {
|
||||
uploadsIDPath := pathJoin(fs.fsPath, bucket, prefix, uploadsJSONFile)
|
||||
_, err := fsStatFile(uploadsIDPath)
|
||||
if err != nil {
|
||||
if err == errFileNotFound {
|
||||
if errorCause(err) == errFileNotFound {
|
||||
return false
|
||||
}
|
||||
errorIf(err, "Unable to access uploads.json "+uploadsIDPath)
|
||||
@@ -58,11 +63,11 @@ func (fs fsObjects) deleteUploadsJSON(bucket, object, uploadID string) error {
|
||||
|
||||
// Removes the uploadID, called either by CompleteMultipart of AbortMultipart. If the resuling uploads
|
||||
// slice is empty then we remove/purge the file.
|
||||
func (fs fsObjects) removeUploadID(bucket, object, uploadID string, rwlk *lock.LockedFile) error {
|
||||
func (fs fsObjects) removeUploadID(bucket, object, uploadID string, rwlk *lock.LockedFile) (bool, error) {
|
||||
uploadIDs := uploadsV1{}
|
||||
_, err := uploadIDs.ReadFrom(rwlk)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Removes upload id from the uploads list.
|
||||
@@ -71,12 +76,12 @@ func (fs fsObjects) removeUploadID(bucket, object, uploadID string, rwlk *lock.L
|
||||
// Check this is the last entry.
|
||||
if uploadIDs.IsEmpty() {
|
||||
// No more uploads left, so we delete `uploads.json` file.
|
||||
return fs.deleteUploadsJSON(bucket, object, uploadID)
|
||||
return true, fs.deleteUploadsJSON(bucket, object, uploadID)
|
||||
} // else not empty
|
||||
|
||||
// Write update `uploads.json`.
|
||||
_, err = uploadIDs.WriteTo(rwlk)
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Adds a new uploadID if no previous `uploads.json` is
|
||||
@@ -105,13 +110,15 @@ func (fs fsObjects) addUploadID(bucket, object, uploadID string, initiated time.
|
||||
}
|
||||
|
||||
// listMultipartUploadIDs - list all the upload ids from a marker up to 'count'.
|
||||
func (fs fsObjects) listMultipartUploadIDs(bucketName, objectName, uploadIDMarker string, count int) ([]uploadMetadata, bool, error) {
|
||||
var uploads []uploadMetadata
|
||||
func (fs fsObjects) listMultipartUploadIDs(bucketName, objectName, uploadIDMarker string, count int) ([]MultipartInfo, bool, error) {
|
||||
var uploads []MultipartInfo
|
||||
|
||||
// Hold the lock so that two parallel complete-multipart-uploads
|
||||
// do not leave a stale uploads.json behind.
|
||||
objectMPartPathLock := globalNSMutex.NewNSLock(minioMetaMultipartBucket, pathJoin(bucketName, objectName))
|
||||
objectMPartPathLock.RLock()
|
||||
if err := objectMPartPathLock.GetRLock(globalListingTimeout); err != nil {
|
||||
return nil, false, traceError(err)
|
||||
}
|
||||
defer objectMPartPathLock.RUnlock()
|
||||
|
||||
uploadsPath := pathJoin(bucketName, objectName, uploadsJSONFile)
|
||||
@@ -142,7 +149,7 @@ func (fs fsObjects) listMultipartUploadIDs(bucketName, objectName, uploadIDMarke
|
||||
}
|
||||
|
||||
for index < len(uploadIDs.Uploads) {
|
||||
uploads = append(uploads, uploadMetadata{
|
||||
uploads = append(uploads, MultipartInfo{
|
||||
Object: objectName,
|
||||
UploadID: uploadIDs.Uploads[index].UploadID,
|
||||
Initiated: uploadIDs.Uploads[index].Initiated,
|
||||
@@ -158,8 +165,8 @@ func (fs fsObjects) listMultipartUploadIDs(bucketName, objectName, uploadIDMarke
|
||||
return uploads, end, nil
|
||||
}
|
||||
|
||||
// listMultipartUploads - lists all multipart uploads.
|
||||
func (fs fsObjects) listMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (ListMultipartsInfo, error) {
|
||||
// listMultipartUploadsCleanup - lists all multipart uploads. Called by fs.cleanupStaleMultipartUpload()
|
||||
func (fs fsObjects) listMultipartUploadsCleanup(bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (lmi ListMultipartsInfo, e error) {
|
||||
result := ListMultipartsInfo{}
|
||||
recursive := true
|
||||
if delimiter == slashSeparator {
|
||||
@@ -184,14 +191,14 @@ func (fs fsObjects) listMultipartUploads(bucket, prefix, keyMarker, uploadIDMark
|
||||
multipartMarkerPath = pathJoin(bucket, keyMarker)
|
||||
}
|
||||
|
||||
var uploads []uploadMetadata
|
||||
var uploads []MultipartInfo
|
||||
var err error
|
||||
var eof bool
|
||||
|
||||
if uploadIDMarker != "" {
|
||||
uploads, _, err = fs.listMultipartUploadIDs(bucket, keyMarker, uploadIDMarker, maxUploads)
|
||||
if err != nil {
|
||||
return ListMultipartsInfo{}, err
|
||||
return lmi, err
|
||||
}
|
||||
maxUploads = maxUploads - len(uploads)
|
||||
}
|
||||
@@ -232,12 +239,12 @@ func (fs fsObjects) listMultipartUploads(bucket, prefix, keyMarker, uploadIDMark
|
||||
eof = true
|
||||
break
|
||||
}
|
||||
return ListMultipartsInfo{}, walkResult.err
|
||||
return lmi, walkResult.err
|
||||
}
|
||||
|
||||
entry := strings.TrimPrefix(walkResult.entry, retainSlash(bucket))
|
||||
if hasSuffix(walkResult.entry, slashSeparator) {
|
||||
uploads = append(uploads, uploadMetadata{
|
||||
uploads = append(uploads, MultipartInfo{
|
||||
Object: entry,
|
||||
})
|
||||
maxUploads--
|
||||
@@ -250,13 +257,13 @@ func (fs fsObjects) listMultipartUploads(bucket, prefix, keyMarker, uploadIDMark
|
||||
continue
|
||||
}
|
||||
|
||||
var tmpUploads []uploadMetadata
|
||||
var tmpUploads []MultipartInfo
|
||||
var end bool
|
||||
uploadIDMarker = ""
|
||||
|
||||
tmpUploads, end, err = fs.listMultipartUploadIDs(bucket, entry, uploadIDMarker, maxUploads)
|
||||
if err != nil {
|
||||
return ListMultipartsInfo{}, err
|
||||
return lmi, err
|
||||
}
|
||||
|
||||
uploads = append(uploads, tmpUploads...)
|
||||
@@ -302,25 +309,46 @@ func (fs fsObjects) listMultipartUploads(bucket, prefix, keyMarker, uploadIDMark
|
||||
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) {
|
||||
if err := checkListMultipartArgs(bucket, prefix, keyMarker, uploadIDMarker, delimiter, fs); err != nil {
|
||||
return ListMultipartsInfo{}, err
|
||||
// ListMultipartUploads - lists all the uploadIDs for the specified object.
|
||||
// We do not support prefix based listing.
|
||||
func (fs fsObjects) ListMultipartUploads(bucket, object, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (lmi ListMultipartsInfo, e error) {
|
||||
if err := checkListMultipartArgs(bucket, object, keyMarker, uploadIDMarker, delimiter, fs); err != nil {
|
||||
return lmi, err
|
||||
}
|
||||
|
||||
if _, err := fs.statBucketDir(bucket); err != nil {
|
||||
return ListMultipartsInfo{}, toObjectErr(err, bucket)
|
||||
return lmi, toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
return fs.listMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||
result := ListMultipartsInfo{}
|
||||
|
||||
result.IsTruncated = true
|
||||
result.MaxUploads = maxUploads
|
||||
result.KeyMarker = keyMarker
|
||||
result.Prefix = object
|
||||
result.Delimiter = delimiter
|
||||
|
||||
uploads, _, err := fs.listMultipartUploadIDs(bucket, object, uploadIDMarker, maxUploads)
|
||||
if err != nil {
|
||||
return lmi, err
|
||||
}
|
||||
|
||||
result.NextKeyMarker = object
|
||||
// Loop through all the received uploads fill in the multiparts result.
|
||||
for _, upload := range uploads {
|
||||
uploadID := upload.UploadID
|
||||
result.Uploads = append(result.Uploads, upload)
|
||||
result.NextUploadIDMarker = uploadID
|
||||
}
|
||||
|
||||
result.IsTruncated = len(uploads) == maxUploads
|
||||
|
||||
if !result.IsTruncated {
|
||||
result.NextKeyMarker = ""
|
||||
result.NextUploadIDMarker = ""
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// newMultipartUpload - wrapper for initializing a new multipart
|
||||
@@ -387,7 +415,9 @@ func (fs fsObjects) NewMultipartUpload(bucket, object string, meta map[string]st
|
||||
// Hold the lock so that two parallel complete-multipart-uploads
|
||||
// do not leave a stale uploads.json behind.
|
||||
objectMPartPathLock := globalNSMutex.NewNSLock(minioMetaMultipartBucket, pathJoin(bucket, object))
|
||||
objectMPartPathLock.Lock()
|
||||
if err := objectMPartPathLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer objectMPartPathLock.Unlock()
|
||||
|
||||
return fs.newMultipartUpload(bucket, object, meta)
|
||||
@@ -412,9 +442,9 @@ func partToAppend(fsMeta fsMetaV1, fsAppendMeta fsMetaV1) (part objectPartInfo,
|
||||
// CopyObjectPart - similar to PutObjectPart but reads data from an existing
|
||||
// object. Internally incoming data is written to '.minio.sys/tmp' location
|
||||
// and safely renamed to '.minio.sys/multipart' for reach parts.
|
||||
func (fs fsObjects) CopyObjectPart(srcBucket, srcObject, dstBucket, dstObject, uploadID string, partID int, startOffset int64, length int64) (PartInfo, error) {
|
||||
func (fs fsObjects) CopyObjectPart(srcBucket, srcObject, dstBucket, dstObject, uploadID string, partID int, startOffset int64, length int64) (pi PartInfo, e error) {
|
||||
if err := checkNewMultipartArgs(srcBucket, srcObject, fs); err != nil {
|
||||
return PartInfo{}, err
|
||||
return pi, err
|
||||
}
|
||||
|
||||
// Initialize pipe.
|
||||
@@ -429,9 +459,14 @@ func (fs fsObjects) CopyObjectPart(srcBucket, srcObject, dstBucket, dstObject, u
|
||||
pipeWriter.Close() // Close writer explicitly signalling we wrote all data.
|
||||
}()
|
||||
|
||||
partInfo, err := fs.PutObjectPart(dstBucket, dstObject, uploadID, partID, length, pipeReader, "", "")
|
||||
hashReader, err := hash.NewReader(pipeReader, length, "", "")
|
||||
if err != nil {
|
||||
return PartInfo{}, toObjectErr(err, dstBucket, dstObject)
|
||||
return pi, toObjectErr(err, dstBucket, dstObject)
|
||||
}
|
||||
|
||||
partInfo, err := fs.PutObjectPart(dstBucket, dstObject, uploadID, partID, hashReader)
|
||||
if err != nil {
|
||||
return pi, toObjectErr(err, dstBucket, dstObject)
|
||||
}
|
||||
|
||||
// Explicitly close the reader.
|
||||
@@ -444,28 +479,35 @@ func (fs fsObjects) CopyObjectPart(srcBucket, srcObject, dstBucket, dstObject, u
|
||||
// an ongoing multipart transaction. Internally incoming data is
|
||||
// written to '.minio.sys/tmp' location and safely renamed to
|
||||
// '.minio.sys/multipart' for reach parts.
|
||||
func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, size int64, data io.Reader, md5Hex string, sha256sum string) (PartInfo, error) {
|
||||
func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, data *hash.Reader) (pi PartInfo, e error) {
|
||||
if err := checkPutObjectPartArgs(bucket, object, fs); err != nil {
|
||||
return PartInfo{}, err
|
||||
return pi, err
|
||||
}
|
||||
|
||||
if _, err := fs.statBucketDir(bucket); err != nil {
|
||||
return PartInfo{}, toObjectErr(err, bucket)
|
||||
return pi, toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// Validate input data size and it can never be less than zero.
|
||||
if data.Size() < 0 {
|
||||
return pi, toObjectErr(traceError(errInvalidArgument))
|
||||
}
|
||||
|
||||
// Hold the lock so that two parallel complete-multipart-uploads
|
||||
// do not leave a stale uploads.json behind.
|
||||
objectMPartPathLock := globalNSMutex.NewNSLock(minioMetaMultipartBucket, pathJoin(bucket, object))
|
||||
objectMPartPathLock.Lock()
|
||||
if err := objectMPartPathLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return pi, err
|
||||
}
|
||||
defer objectMPartPathLock.Unlock()
|
||||
|
||||
// Disallow any parallel abort or complete multipart operations.
|
||||
uploadsPath := pathJoin(fs.fsPath, minioMetaMultipartBucket, bucket, object, uploadsJSONFile)
|
||||
if _, err := fs.rwPool.Open(uploadsPath); err != nil {
|
||||
if err == errFileNotFound || err == errFileAccessDenied {
|
||||
return PartInfo{}, traceError(InvalidUploadID{UploadID: uploadID})
|
||||
return pi, traceError(InvalidUploadID{UploadID: uploadID})
|
||||
}
|
||||
return PartInfo{}, toObjectErr(traceError(err), bucket, object)
|
||||
return pi, toObjectErr(traceError(err), bucket, object)
|
||||
}
|
||||
defer fs.rwPool.Close(uploadsPath)
|
||||
|
||||
@@ -476,61 +518,39 @@ func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, s
|
||||
rwlk, err := fs.rwPool.Write(fsMetaPath)
|
||||
if err != nil {
|
||||
if err == errFileNotFound || err == errFileAccessDenied {
|
||||
return PartInfo{}, traceError(InvalidUploadID{UploadID: uploadID})
|
||||
return pi, traceError(InvalidUploadID{UploadID: uploadID})
|
||||
}
|
||||
return PartInfo{}, toObjectErr(traceError(err), bucket, object)
|
||||
return pi, toObjectErr(traceError(err), bucket, object)
|
||||
}
|
||||
defer rwlk.Close()
|
||||
|
||||
fsMeta := fsMetaV1{}
|
||||
_, err = fsMeta.ReadFrom(rwlk)
|
||||
if err != nil {
|
||||
return PartInfo{}, toObjectErr(err, minioMetaMultipartBucket, fsMetaPath)
|
||||
return pi, toObjectErr(err, minioMetaMultipartBucket, fsMetaPath)
|
||||
}
|
||||
|
||||
partSuffix := fmt.Sprintf("object%d", partID)
|
||||
tmpPartPath := uploadID + "." + mustGetUUID() + "." + partSuffix
|
||||
|
||||
// Initialize md5 writer.
|
||||
md5Writer := md5.New()
|
||||
hashWriters := []io.Writer{md5Writer}
|
||||
|
||||
var sha256Writer hash.Hash
|
||||
if sha256sum != "" {
|
||||
sha256Writer = sha256.New()
|
||||
hashWriters = append(hashWriters, sha256Writer)
|
||||
}
|
||||
multiWriter := io.MultiWriter(hashWriters...)
|
||||
|
||||
// Limit the reader to its provided size if specified.
|
||||
var limitDataReader io.Reader
|
||||
if size > 0 {
|
||||
// This is done so that we can avoid erroneous clients sending more data than the set content size.
|
||||
limitDataReader = io.LimitReader(data, size)
|
||||
} else {
|
||||
// else we read till EOF.
|
||||
limitDataReader = data
|
||||
}
|
||||
|
||||
teeReader := io.TeeReader(limitDataReader, multiWriter)
|
||||
bufSize := int64(readSizeV1)
|
||||
if size > 0 && bufSize > size {
|
||||
if size := data.Size(); size > 0 && bufSize > size {
|
||||
bufSize = size
|
||||
}
|
||||
buf := make([]byte, int(bufSize))
|
||||
buf := make([]byte, bufSize)
|
||||
|
||||
fsPartPath := pathJoin(fs.fsPath, minioMetaTmpBucket, fs.fsUUID, tmpPartPath)
|
||||
bytesWritten, cErr := fsCreateFile(fsPartPath, teeReader, buf, size)
|
||||
bytesWritten, cErr := fsCreateFile(fsPartPath, data, buf, data.Size())
|
||||
if cErr != nil {
|
||||
fsRemoveFile(fsPartPath)
|
||||
return PartInfo{}, toObjectErr(cErr, minioMetaTmpBucket, tmpPartPath)
|
||||
return pi, toObjectErr(cErr, minioMetaTmpBucket, tmpPartPath)
|
||||
}
|
||||
|
||||
// Should return IncompleteBody{} error when reader has fewer
|
||||
// bytes than specified in request header.
|
||||
if bytesWritten < size {
|
||||
if bytesWritten < data.Size() {
|
||||
fsRemoveFile(fsPartPath)
|
||||
return PartInfo{}, traceError(IncompleteBody{})
|
||||
return pi, traceError(IncompleteBody{})
|
||||
}
|
||||
|
||||
// Delete temporary part in case of failure. If
|
||||
@@ -538,43 +558,33 @@ func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, s
|
||||
// delete.
|
||||
defer fsRemoveFile(fsPartPath)
|
||||
|
||||
newMD5Hex := hex.EncodeToString(md5Writer.Sum(nil))
|
||||
if md5Hex != "" {
|
||||
if newMD5Hex != md5Hex {
|
||||
return PartInfo{}, traceError(BadDigest{md5Hex, newMD5Hex})
|
||||
}
|
||||
}
|
||||
|
||||
if sha256sum != "" {
|
||||
newSHA256sum := hex.EncodeToString(sha256Writer.Sum(nil))
|
||||
if newSHA256sum != sha256sum {
|
||||
return PartInfo{}, traceError(SHA256Mismatch{})
|
||||
}
|
||||
}
|
||||
|
||||
partPath := pathJoin(bucket, object, uploadID, partSuffix)
|
||||
// Lock the part so that another part upload with same part-number gets blocked
|
||||
// while the part is getting appended in the background.
|
||||
partLock := globalNSMutex.NewNSLock(minioMetaMultipartBucket, partPath)
|
||||
partLock.Lock()
|
||||
if err = partLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return pi, err
|
||||
}
|
||||
|
||||
fsNSPartPath := pathJoin(fs.fsPath, minioMetaMultipartBucket, partPath)
|
||||
if err = fsRenameFile(fsPartPath, fsNSPartPath); err != nil {
|
||||
partLock.Unlock()
|
||||
return PartInfo{}, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
||||
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
||||
}
|
||||
|
||||
md5hex := hex.EncodeToString(data.MD5Current())
|
||||
|
||||
// Save the object part info in `fs.json`.
|
||||
fsMeta.AddObjectPart(partID, partSuffix, newMD5Hex, size)
|
||||
fsMeta.AddObjectPart(partID, partSuffix, md5hex, data.Size())
|
||||
if _, err = fsMeta.WriteTo(rwlk); err != nil {
|
||||
partLock.Unlock()
|
||||
return PartInfo{}, toObjectErr(err, minioMetaMultipartBucket, uploadIDPath)
|
||||
return pi, toObjectErr(err, minioMetaMultipartBucket, uploadIDPath)
|
||||
}
|
||||
|
||||
partNamePath := pathJoin(fs.fsPath, minioMetaMultipartBucket, uploadIDPath, partSuffix)
|
||||
fi, err := fsStatFile(partNamePath)
|
||||
if err != nil {
|
||||
return PartInfo{}, toObjectErr(err, minioMetaMultipartBucket, partSuffix)
|
||||
return pi, toObjectErr(err, minioMetaMultipartBucket, partSuffix)
|
||||
}
|
||||
|
||||
// Append the part in background.
|
||||
@@ -591,7 +601,7 @@ func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, s
|
||||
return PartInfo{
|
||||
PartNumber: partID,
|
||||
LastModified: fi.ModTime(),
|
||||
ETag: newMD5Hex,
|
||||
ETag: md5hex,
|
||||
Size: fi.Size(),
|
||||
}, nil
|
||||
}
|
||||
@@ -599,7 +609,7 @@ func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, s
|
||||
// listObjectParts - wrapper scanning through
|
||||
// '.minio.sys/multipart/bucket/object/UPLOADID'. Lists all the parts
|
||||
// saved inside '.minio.sys/multipart/bucket/object/UPLOADID'.
|
||||
func (fs fsObjects) listObjectParts(bucket, object, uploadID string, partNumberMarker, maxParts int) (ListPartsInfo, error) {
|
||||
func (fs fsObjects) listObjectParts(bucket, object, uploadID string, partNumberMarker, maxParts int) (lpi ListPartsInfo, e error) {
|
||||
result := ListPartsInfo{}
|
||||
|
||||
uploadIDPath := pathJoin(bucket, object, uploadID)
|
||||
@@ -608,16 +618,16 @@ func (fs fsObjects) listObjectParts(bucket, object, uploadID string, partNumberM
|
||||
if err != nil {
|
||||
if err == errFileNotFound || err == errFileAccessDenied {
|
||||
// On windows oddly this is returned.
|
||||
return ListPartsInfo{}, traceError(InvalidUploadID{UploadID: uploadID})
|
||||
return lpi, traceError(InvalidUploadID{UploadID: uploadID})
|
||||
}
|
||||
return ListPartsInfo{}, toObjectErr(traceError(err), bucket, object)
|
||||
return lpi, toObjectErr(traceError(err), bucket, object)
|
||||
}
|
||||
defer fs.rwPool.Close(fsMetaPath)
|
||||
|
||||
fsMeta := fsMetaV1{}
|
||||
_, err = fsMeta.ReadFrom(metaFile.LockedFile)
|
||||
if err != nil {
|
||||
return ListPartsInfo{}, toObjectErr(err, minioMetaBucket, fsMetaPath)
|
||||
return lpi, toObjectErr(err, minioMetaBucket, fsMetaPath)
|
||||
}
|
||||
|
||||
// Only parts with higher part numbers will be listed.
|
||||
@@ -633,7 +643,7 @@ func (fs fsObjects) listObjectParts(bucket, object, uploadID string, partNumberM
|
||||
partNamePath := pathJoin(fs.fsPath, minioMetaMultipartBucket, uploadIDPath, part.Name)
|
||||
fi, err = fsStatFile(partNamePath)
|
||||
if err != nil {
|
||||
return ListPartsInfo{}, toObjectErr(err, minioMetaMultipartBucket, partNamePath)
|
||||
return lpi, toObjectErr(err, minioMetaMultipartBucket, partNamePath)
|
||||
}
|
||||
result.Parts = append(result.Parts, PartInfo{
|
||||
PartNumber: part.Number,
|
||||
@@ -671,27 +681,29 @@ func (fs fsObjects) listObjectParts(bucket, object, uploadID string, partNumberM
|
||||
// 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) {
|
||||
func (fs fsObjects) ListObjectParts(bucket, object, uploadID string, partNumberMarker, maxParts int) (lpi ListPartsInfo, e error) {
|
||||
if err := checkListPartsArgs(bucket, object, fs); err != nil {
|
||||
return ListPartsInfo{}, err
|
||||
return lpi, err
|
||||
}
|
||||
|
||||
// Check if bucket exists
|
||||
if _, err := fs.statBucketDir(bucket); err != nil {
|
||||
return ListPartsInfo{}, toObjectErr(err, bucket)
|
||||
return lpi, toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// Hold the lock so that two parallel complete-multipart-uploads
|
||||
// do not leave a stale uploads.json behind.
|
||||
objectMPartPathLock := globalNSMutex.NewNSLock(minioMetaMultipartBucket, pathJoin(bucket, object))
|
||||
objectMPartPathLock.RLock()
|
||||
if err := objectMPartPathLock.GetRLock(globalListingTimeout); err != nil {
|
||||
return lpi, traceError(err)
|
||||
}
|
||||
defer objectMPartPathLock.RUnlock()
|
||||
|
||||
listPartsInfo, err := fs.listObjectParts(bucket, object, uploadID, partNumberMarker, maxParts)
|
||||
if err != nil {
|
||||
return ListPartsInfo{}, toObjectErr(err, bucket, object)
|
||||
return lpi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
// Success.
|
||||
return listPartsInfo, nil
|
||||
}
|
||||
|
||||
@@ -701,41 +713,58 @@ func (fs fsObjects) ListObjectParts(bucket, object, uploadID string, partNumberM
|
||||
// md5sums of all the parts.
|
||||
//
|
||||
// Implements S3 compatible Complete multipart API.
|
||||
func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, uploadID string, parts []completePart) (ObjectInfo, error) {
|
||||
func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, uploadID string, parts []CompletePart) (oi ObjectInfo, e error) {
|
||||
if err := checkCompleteMultipartArgs(bucket, object, fs); err != nil {
|
||||
return ObjectInfo{}, err
|
||||
return oi, err
|
||||
}
|
||||
|
||||
// Check if an object is present as one of the parent dir.
|
||||
if fs.parentDirIsObject(bucket, pathutil.Dir(object)) {
|
||||
return ObjectInfo{}, toObjectErr(traceError(errFileAccessDenied), bucket, object)
|
||||
return oi, toObjectErr(traceError(errFileAccessDenied), bucket, object)
|
||||
}
|
||||
|
||||
if _, err := fs.statBucketDir(bucket); err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, bucket)
|
||||
return oi, toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// Calculate s3 compatible md5sum for complete multipart.
|
||||
s3MD5, err := getCompleteMultipartMD5(parts)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, err
|
||||
return oi, err
|
||||
}
|
||||
|
||||
uploadIDPath := pathJoin(bucket, object, uploadID)
|
||||
var removeObjectDir bool
|
||||
|
||||
// Hold the lock so that two parallel complete-multipart-uploads
|
||||
// do not leave a stale uploads.json behind.
|
||||
objectMPartPathLock := globalNSMutex.NewNSLock(minioMetaMultipartBucket, pathJoin(bucket, object))
|
||||
objectMPartPathLock.Lock()
|
||||
defer objectMPartPathLock.Unlock()
|
||||
if err = objectMPartPathLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return oi, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if removeObjectDir {
|
||||
basePath := pathJoin(fs.fsPath, minioMetaMultipartBucket, bucket)
|
||||
derr := fsDeleteFile(basePath, pathJoin(basePath, object))
|
||||
if derr = errorCause(derr); derr != nil {
|
||||
// In parallel execution, CompleteMultipartUpload could have deleted temporary
|
||||
// state files/directory, it is safe to ignore errFileNotFound
|
||||
if derr != errFileNotFound {
|
||||
errorIf(derr, "unable to remove %s in %s", pathJoin(basePath, object), basePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
objectMPartPathLock.Unlock()
|
||||
}()
|
||||
|
||||
fsMetaPathMultipart := pathJoin(fs.fsPath, minioMetaMultipartBucket, uploadIDPath, fsMetaJSONFile)
|
||||
rlk, err := fs.rwPool.Open(fsMetaPathMultipart)
|
||||
if err != nil {
|
||||
if err == errFileNotFound || err == errFileAccessDenied {
|
||||
return ObjectInfo{}, traceError(InvalidUploadID{UploadID: uploadID})
|
||||
return oi, traceError(InvalidUploadID{UploadID: uploadID})
|
||||
}
|
||||
return ObjectInfo{}, toObjectErr(traceError(err), bucket, object)
|
||||
return oi, toObjectErr(traceError(err), bucket, object)
|
||||
}
|
||||
|
||||
// Disallow any parallel abort or complete multipart operations.
|
||||
@@ -743,9 +772,9 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
||||
if err != nil {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
if err == errFileNotFound || err == errFileAccessDenied {
|
||||
return ObjectInfo{}, traceError(InvalidUploadID{UploadID: uploadID})
|
||||
return oi, traceError(InvalidUploadID{UploadID: uploadID})
|
||||
}
|
||||
return ObjectInfo{}, toObjectErr(traceError(err), bucket, object)
|
||||
return oi, toObjectErr(traceError(err), bucket, object)
|
||||
}
|
||||
defer rwlk.Close()
|
||||
|
||||
@@ -754,7 +783,46 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
||||
_, err = fsMeta.ReadFrom(rlk.LockedFile)
|
||||
if err != nil {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return ObjectInfo{}, toObjectErr(err, minioMetaMultipartBucket, fsMetaPathMultipart)
|
||||
return oi, toObjectErr(err, minioMetaMultipartBucket, fsMetaPathMultipart)
|
||||
}
|
||||
|
||||
partSize := int64(-1) // Used later to ensure that all parts sizes are same.
|
||||
// Validate all parts and then commit to disk.
|
||||
for i, part := range parts {
|
||||
partIdx := fsMeta.ObjectPartIndex(part.PartNumber)
|
||||
if partIdx == -1 {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return oi, traceError(InvalidPart{})
|
||||
}
|
||||
|
||||
if fsMeta.Parts[partIdx].ETag != part.ETag {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return oi, traceError(InvalidPart{})
|
||||
}
|
||||
|
||||
// All parts except the last part has to be atleast 5MB.
|
||||
if (i < len(parts)-1) && !isMinAllowedPartSize(fsMeta.Parts[partIdx].Size) {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return oi, traceError(PartTooSmall{
|
||||
PartNumber: part.PartNumber,
|
||||
PartSize: fsMeta.Parts[partIdx].Size,
|
||||
PartETag: part.ETag,
|
||||
})
|
||||
}
|
||||
if partSize == -1 {
|
||||
partSize = fsMeta.Parts[partIdx].Size
|
||||
}
|
||||
// TODO: Make necessary changes in future as explained in the below comment.
|
||||
// All parts except the last part has to be of same size. We are introducing this
|
||||
// check to see if any clients break. If clients do not break then we can optimize
|
||||
// multipart PutObjectPart by writing the part at the right offset using pwrite()
|
||||
// so that we don't need to do background append at all. i.e by the time we get
|
||||
// CompleteMultipartUpload we already have the full file available which can be
|
||||
// renamed to the main name-space.
|
||||
if (i < len(parts)-1) && partSize != fsMeta.Parts[partIdx].Size {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return oi, traceError(PartsSizeUnequal{})
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for any competing PutObject() operation on bucket/object, since same namespace
|
||||
@@ -763,7 +831,7 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
||||
metaFile, err := fs.rwPool.Create(fsMetaPath)
|
||||
if err != nil {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return ObjectInfo{}, toObjectErr(traceError(err), bucket, object)
|
||||
return oi, toObjectErr(traceError(err), bucket, object)
|
||||
}
|
||||
defer metaFile.Close()
|
||||
|
||||
@@ -780,7 +848,7 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
||||
fsTmpObjPath := pathJoin(fs.fsPath, minioMetaTmpBucket, fs.fsUUID, uploadID)
|
||||
if err = fsRenameFile(fsTmpObjPath, fsNSObjPath); err != nil {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return ObjectInfo{}, toObjectErr(err, minioMetaTmpBucket, uploadID)
|
||||
return oi, toObjectErr(err, minioMetaTmpBucket, uploadID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -798,29 +866,7 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
||||
// Allocate staging buffer.
|
||||
var buf = make([]byte, readSizeV1)
|
||||
|
||||
// Validate all parts and then commit to disk.
|
||||
for i, part := range parts {
|
||||
partIdx := fsMeta.ObjectPartIndex(part.PartNumber)
|
||||
if partIdx == -1 {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return ObjectInfo{}, traceError(InvalidPart{})
|
||||
}
|
||||
|
||||
if fsMeta.Parts[partIdx].ETag != part.ETag {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return ObjectInfo{}, traceError(BadDigest{})
|
||||
}
|
||||
|
||||
// All parts except the last part has to be atleast 5MB.
|
||||
if (i < len(parts)-1) && !isMinAllowedPartSize(fsMeta.Parts[partIdx].Size) {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return ObjectInfo{}, traceError(PartTooSmall{
|
||||
PartNumber: part.PartNumber,
|
||||
PartSize: fsMeta.Parts[partIdx].Size,
|
||||
PartETag: part.ETag,
|
||||
})
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
// Construct part suffix.
|
||||
partSuffix := fmt.Sprintf("object%d", part.PartNumber)
|
||||
multipartPartFile := pathJoin(fs.fsPath, minioMetaMultipartBucket, uploadIDPath, partSuffix)
|
||||
@@ -831,19 +877,19 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
||||
if err != nil {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
if err == errFileNotFound {
|
||||
return ObjectInfo{}, traceError(InvalidPart{})
|
||||
return oi, traceError(InvalidPart{})
|
||||
}
|
||||
return ObjectInfo{}, toObjectErr(traceError(err), minioMetaMultipartBucket, partSuffix)
|
||||
return oi, toObjectErr(traceError(err), minioMetaMultipartBucket, partSuffix)
|
||||
}
|
||||
|
||||
// No need to hold a lock, this is a unique file and will be only written
|
||||
// to one one process per uploadID per minio process.
|
||||
var wfile *os.File
|
||||
wfile, err = os.OpenFile(preparePath(fsTmpObjPath), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
|
||||
wfile, err = os.OpenFile((fsTmpObjPath), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
|
||||
if err != nil {
|
||||
reader.Close()
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return ObjectInfo{}, toObjectErr(traceError(err), bucket, object)
|
||||
return oi, toObjectErr(traceError(err), bucket, object)
|
||||
}
|
||||
|
||||
_, err = io.CopyBuffer(wfile, reader, buf)
|
||||
@@ -851,7 +897,7 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
||||
wfile.Close()
|
||||
reader.Close()
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return ObjectInfo{}, toObjectErr(traceError(err), bucket, object)
|
||||
return oi, toObjectErr(traceError(err), bucket, object)
|
||||
}
|
||||
|
||||
wfile.Close()
|
||||
@@ -860,7 +906,7 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
||||
|
||||
if err = fsRenameFile(fsTmpObjPath, fsNSObjPath); err != nil {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return ObjectInfo{}, toObjectErr(err, minioMetaTmpBucket, uploadID)
|
||||
return oi, toObjectErr(err, minioMetaTmpBucket, uploadID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -876,7 +922,7 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
||||
// Write all the set metadata.
|
||||
if _, err = fsMeta.WriteTo(metaFile); err != nil {
|
||||
fs.rwPool.Close(fsMetaPathMultipart)
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
return oi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
// Close lock held on bucket/object/uploadid/fs.json,
|
||||
@@ -888,17 +934,18 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
||||
multipartObjectDir := pathJoin(fs.fsPath, minioMetaMultipartBucket, bucket, object)
|
||||
multipartUploadIDDir := pathJoin(multipartObjectDir, uploadID)
|
||||
if err = fsRemoveUploadIDPath(multipartObjectDir, multipartUploadIDDir); err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
return oi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
// Remove entry from `uploads.json`.
|
||||
if err = fs.removeUploadID(bucket, object, uploadID, rwlk); err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, minioMetaMultipartBucket, pathutil.Join(bucket, object))
|
||||
removeObjectDir, err = fs.removeUploadID(bucket, object, uploadID, rwlk)
|
||||
if err != nil {
|
||||
return oi, toObjectErr(err, minioMetaMultipartBucket, pathutil.Join(bucket, object))
|
||||
}
|
||||
|
||||
fi, err := fsStatFile(fsNSObjPath)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
return oi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
// Return object info.
|
||||
@@ -927,13 +974,30 @@ func (fs fsObjects) AbortMultipartUpload(bucket, object, uploadID string) error
|
||||
}
|
||||
|
||||
uploadIDPath := pathJoin(bucket, object, uploadID)
|
||||
var removeObjectDir bool
|
||||
|
||||
// Hold the lock so that two parallel complete-multipart-uploads
|
||||
// do not leave a stale uploads.json behind.
|
||||
objectMPartPathLock := globalNSMutex.NewNSLock(minioMetaMultipartBucket,
|
||||
pathJoin(bucket, object))
|
||||
objectMPartPathLock.Lock()
|
||||
defer objectMPartPathLock.Unlock()
|
||||
if err := objectMPartPathLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if removeObjectDir {
|
||||
basePath := pathJoin(fs.fsPath, minioMetaMultipartBucket, bucket)
|
||||
derr := fsDeleteFile(basePath, pathJoin(basePath, object))
|
||||
if derr = errorCause(derr); derr != nil {
|
||||
// In parallel execution, AbortMultipartUpload could have deleted temporary
|
||||
// state files/directory, it is safe to ignore errFileNotFound
|
||||
if derr != errFileNotFound {
|
||||
errorIf(derr, "unable to remove %s in %s", pathJoin(basePath, object), basePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
objectMPartPathLock.Unlock()
|
||||
}()
|
||||
|
||||
fsMetaPath := pathJoin(fs.fsPath, minioMetaMultipartBucket, uploadIDPath, fsMetaJSONFile)
|
||||
if _, err := fs.rwPool.Open(fsMetaPath); err != nil {
|
||||
@@ -970,9 +1034,68 @@ func (fs fsObjects) AbortMultipartUpload(bucket, object, uploadID string) error
|
||||
}
|
||||
|
||||
// Remove entry from `uploads.json`.
|
||||
if err = fs.removeUploadID(bucket, object, uploadID, rwlk); err != nil {
|
||||
removeObjectDir, err = fs.removeUploadID(bucket, object, uploadID, rwlk)
|
||||
if err != nil {
|
||||
return toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Removes multipart uploads if any older than `expiry` duration in a given bucket.
|
||||
func (fs fsObjects) cleanupStaleMultipartUpload(bucket string, expiry time.Duration) (err error) {
|
||||
var lmi ListMultipartsInfo
|
||||
var st os.FileInfo
|
||||
for {
|
||||
// List multipart uploads in a bucket 1000 at a time
|
||||
prefix := ""
|
||||
lmi, err = fs.listMultipartUploadsCleanup(bucket, prefix, lmi.KeyMarker, lmi.UploadIDMarker, "", 1000)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to list uploads")
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove uploads (and its parts) older than expiry duration.
|
||||
for _, upload := range lmi.Uploads {
|
||||
uploadIDPath := pathJoin(fs.fsPath, minioMetaMultipartBucket, bucket, upload.Object, upload.UploadID)
|
||||
if st, err = fsStatDir(uploadIDPath); err != nil {
|
||||
errorIf(err, "Failed to lookup uploads directory path %s", uploadIDPath)
|
||||
continue
|
||||
}
|
||||
if time.Since(st.ModTime()) > expiry {
|
||||
fs.AbortMultipartUpload(bucket, upload.Object, upload.UploadID)
|
||||
}
|
||||
}
|
||||
|
||||
// No more incomplete uploads remain, break and return.
|
||||
if !lmi.IsTruncated {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Removes multipart uploads if any older than `expiry` duration
|
||||
// on all buckets for every `cleanupInterval`, this function is
|
||||
// blocking and should be run in a go-routine.
|
||||
func (fs fsObjects) cleanupStaleMultipartUploads(cleanupInterval, expiry time.Duration, doneCh chan struct{}) {
|
||||
ticker := time.NewTicker(cleanupInterval)
|
||||
for {
|
||||
select {
|
||||
case <-doneCh:
|
||||
// Stop the timer.
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
bucketInfos, err := fs.ListBuckets()
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to list buckets")
|
||||
continue
|
||||
}
|
||||
for _, bucketInfo := range bucketInfos {
|
||||
fs.cleanupStaleMultipartUpload(bucketInfo.Name, expiry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+160
-16
@@ -18,17 +18,96 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
obj := initFSObjects(disk, t)
|
||||
fs := obj.(*fsObjects)
|
||||
|
||||
// Close the go-routine, we are going to
|
||||
// manually start it and test in this test case.
|
||||
globalServiceDoneCh <- struct{}{}
|
||||
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
|
||||
obj.MakeBucketWithLocation(bucketName, "")
|
||||
uploadID, err := obj.NewMultipartUpload(bucketName, objectName, nil)
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
|
||||
go fs.cleanupStaleMultipartUploads(20*time.Millisecond, 0, globalServiceDoneCh)
|
||||
|
||||
// Wait for 40ms such that - we have given enough time for
|
||||
// cleanup routine to kick in.
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
|
||||
// Close the routine we do not need it anymore.
|
||||
globalServiceDoneCh <- struct{}{}
|
||||
|
||||
// Check if upload id was already purged.
|
||||
if err = obj.AbortMultipartUpload(bucketName, objectName, uploadID); err != nil {
|
||||
err = errorCause(err)
|
||||
if _, ok := err.(InvalidUploadID); !ok {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests cleanup of stale upload ids.
|
||||
func TestFSCleanupMultipartUpload(t *testing.T) {
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
obj := initFSObjects(disk, t)
|
||||
fs := obj.(*fsObjects)
|
||||
|
||||
// Close the multipart cleanup go-routine.
|
||||
// In this test we are going to manually call
|
||||
// the function which actually cleans the stale
|
||||
// uploads.
|
||||
globalServiceDoneCh <- struct{}{}
|
||||
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
|
||||
obj.MakeBucketWithLocation(bucketName, "")
|
||||
uploadID, err := obj.NewMultipartUpload(bucketName, objectName, nil)
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
|
||||
if err = fs.cleanupStaleMultipartUpload(bucketName, 0); err != nil {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
|
||||
// Check if upload id was already purged.
|
||||
if err = obj.AbortMultipartUpload(bucketName, objectName, uploadID); err != nil {
|
||||
err = errorCause(err)
|
||||
if _, ok := err.(InvalidUploadID); !ok {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFSWriteUploadJSON - tests for writeUploadJSON for FS
|
||||
func TestFSWriteUploadJSON(t *testing.T) {
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer removeAll(disk)
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
obj := initFSObjects(disk, t)
|
||||
fs := obj.(*fsObjects)
|
||||
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
@@ -40,7 +119,7 @@ func TestFSWriteUploadJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
// newMultipartUpload will fail.
|
||||
removeAll(disk) // Remove disk.
|
||||
fs.fsPath = filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
_, err = obj.NewMultipartUpload(bucketName, objectName, nil)
|
||||
if err != nil {
|
||||
if _, ok := errorCause(err).(BucketNotFound); !ok {
|
||||
@@ -53,7 +132,7 @@ func TestFSWriteUploadJSON(t *testing.T) {
|
||||
func TestNewMultipartUploadFaultyDisk(t *testing.T) {
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer removeAll(disk)
|
||||
defer os.RemoveAll(disk)
|
||||
obj := initFSObjects(disk, t)
|
||||
|
||||
fs := obj.(*fsObjects)
|
||||
@@ -65,7 +144,7 @@ func TestNewMultipartUploadFaultyDisk(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test with disk removed.
|
||||
removeAll(disk) // remove disk.
|
||||
fs.fsPath = filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
if _, err := fs.NewMultipartUpload(bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"}); err != nil {
|
||||
if !isSameType(errorCause(err), BucketNotFound{}) {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
@@ -79,11 +158,11 @@ func TestPutObjectPartFaultyDisk(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer removeAll(root)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer removeAll(disk)
|
||||
defer os.RemoveAll(disk)
|
||||
obj := initFSObjects(disk, t)
|
||||
fs := obj.(*fsObjects)
|
||||
bucketName := "bucket"
|
||||
@@ -103,8 +182,8 @@ func TestPutObjectPartFaultyDisk(t *testing.T) {
|
||||
md5Hex := getMD5Hash(data)
|
||||
sha256sum := ""
|
||||
|
||||
removeAll(disk) // Disk not found.
|
||||
_, err = fs.PutObjectPart(bucketName, objectName, uploadID, 1, dataLen, bytes.NewReader(data), md5Hex, sha256sum)
|
||||
fs.fsPath = filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
_, err = fs.PutObjectPart(bucketName, objectName, uploadID, 1, mustGetHashReader(t, bytes.NewReader(data), dataLen, md5Hex, sha256sum))
|
||||
if !isSameType(errorCause(err), BucketNotFound{}) {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
@@ -114,7 +193,7 @@ func TestPutObjectPartFaultyDisk(t *testing.T) {
|
||||
func TestCompleteMultipartUploadFaultyDisk(t *testing.T) {
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer removeAll(disk)
|
||||
defer os.RemoveAll(disk)
|
||||
obj := initFSObjects(disk, t)
|
||||
|
||||
fs := obj.(*fsObjects)
|
||||
@@ -132,15 +211,14 @@ func TestCompleteMultipartUploadFaultyDisk(t *testing.T) {
|
||||
}
|
||||
|
||||
md5Hex := getMD5Hash(data)
|
||||
sha256sum := ""
|
||||
|
||||
if _, err := fs.PutObjectPart(bucketName, objectName, uploadID, 1, 5, bytes.NewReader(data), md5Hex, sha256sum); err != nil {
|
||||
if _, err := fs.PutObjectPart(bucketName, objectName, uploadID, 1, mustGetHashReader(t, bytes.NewReader(data), 5, md5Hex, "")); err != nil {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
|
||||
parts := []completePart{{PartNumber: 1, ETag: md5Hex}}
|
||||
parts := []CompletePart{{PartNumber: 1, ETag: md5Hex}}
|
||||
|
||||
removeAll(disk) // Disk not found.
|
||||
fs.fsPath = filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
if _, err := fs.CompleteMultipartUpload(bucketName, objectName, uploadID, parts); err != nil {
|
||||
if !isSameType(errorCause(err), BucketNotFound{}) {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
@@ -148,11 +226,77 @@ func TestCompleteMultipartUploadFaultyDisk(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCompleteMultipartUploadFaultyDisk - test CompleteMultipartUpload with faulty disks
|
||||
func TestCompleteMultipartUpload(t *testing.T) {
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer os.RemoveAll(disk)
|
||||
obj := initFSObjects(disk, t)
|
||||
|
||||
fs := obj.(*fsObjects)
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
data := []byte("12345")
|
||||
|
||||
if err := obj.MakeBucketWithLocation(bucketName, ""); err != nil {
|
||||
t.Fatal("Cannot create bucket, err: ", err)
|
||||
}
|
||||
|
||||
uploadID, err := fs.NewMultipartUpload(bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"})
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
|
||||
md5Hex := getMD5Hash(data)
|
||||
|
||||
if _, err := fs.PutObjectPart(bucketName, objectName, uploadID, 1, mustGetHashReader(t, bytes.NewReader(data), 5, md5Hex, "")); err != nil {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
|
||||
parts := []CompletePart{{PartNumber: 1, ETag: md5Hex}}
|
||||
|
||||
if _, err := fs.CompleteMultipartUpload(bucketName, objectName, uploadID, parts); err != nil {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCompleteMultipartUploadFaultyDisk - test CompleteMultipartUpload with faulty disks
|
||||
func TestAbortMultipartUpload(t *testing.T) {
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer os.RemoveAll(disk)
|
||||
obj := initFSObjects(disk, t)
|
||||
|
||||
fs := obj.(*fsObjects)
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
data := []byte("12345")
|
||||
|
||||
if err := obj.MakeBucketWithLocation(bucketName, ""); err != nil {
|
||||
t.Fatal("Cannot create bucket, err: ", err)
|
||||
}
|
||||
|
||||
uploadID, err := fs.NewMultipartUpload(bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"})
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
|
||||
md5Hex := getMD5Hash(data)
|
||||
|
||||
if _, err := fs.PutObjectPart(bucketName, objectName, uploadID, 1, mustGetHashReader(t, bytes.NewReader(data), 5, md5Hex, "")); err != nil {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
|
||||
if err := fs.AbortMultipartUpload(bucketName, objectName, uploadID); err != nil {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListMultipartUploadsFaultyDisk - test ListMultipartUploads with faulty disks
|
||||
func TestListMultipartUploadsFaultyDisk(t *testing.T) {
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer removeAll(disk)
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
obj := initFSObjects(disk, t)
|
||||
|
||||
@@ -173,11 +317,11 @@ func TestListMultipartUploadsFaultyDisk(t *testing.T) {
|
||||
md5Hex := getMD5Hash(data)
|
||||
sha256sum := ""
|
||||
|
||||
if _, err := fs.PutObjectPart(bucketName, objectName, uploadID, 1, 5, bytes.NewReader(data), md5Hex, sha256sum); err != nil {
|
||||
if _, err := fs.PutObjectPart(bucketName, objectName, uploadID, 1, mustGetHashReader(t, bytes.NewReader(data), 5, md5Hex, sha256sum)); err != nil {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
|
||||
removeAll(disk) // Disk not found.
|
||||
fs.fsPath = filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
if _, err := fs.ListMultipartUploads(bucketName, objectName, "", "", "", 1000); err != nil {
|
||||
if !isSameType(errorCause(err), BucketNotFound{}) {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
|
||||
+54
-49
@@ -31,24 +31,16 @@ type fsIOPool struct {
|
||||
readersMap map[string]*lock.RLockedFile
|
||||
}
|
||||
|
||||
// Open is a wrapper call to read locked file which
|
||||
// returns a ReadAtCloser.
|
||||
// lookupToRead - looks up an fd from readers map and
|
||||
// returns read locked fd for caller to read from, if
|
||||
// fd found increments the reference count. If the fd
|
||||
// is found to be closed then purges it from the
|
||||
// readersMap and returns nil instead.
|
||||
//
|
||||
// ReaderAt is provided so that the fd is non seekable, since
|
||||
// we are sharing fd's with concurrent threads, we don't want
|
||||
// all readers to change offsets on each other during such
|
||||
// concurrent operations. Using ReadAt allows us to read from
|
||||
// any offsets.
|
||||
//
|
||||
// Closer is implemented to track total readers and to close
|
||||
// only when there no more readers, the fd is purged if the lock
|
||||
// count has reached zero.
|
||||
func (fsi *fsIOPool) Open(path string) (*lock.RLockedFile, error) {
|
||||
if err := checkPathLength(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fsi.Lock()
|
||||
// NOTE: this function is not protected and it is callers
|
||||
// responsibility to lock this call to be thread safe. For
|
||||
// implementation ideas look at the usage inside Open() call.
|
||||
func (fsi *fsIOPool) lookupToRead(path string) (*lock.RLockedFile, bool) {
|
||||
rlkFile, ok := fsi.readersMap[path]
|
||||
// File reference exists on map, validate if its
|
||||
// really closed and we are safe to purge it.
|
||||
@@ -69,15 +61,33 @@ func (fsi *fsIOPool) Open(path string) (*lock.RLockedFile, error) {
|
||||
rlkFile.IncLockRef()
|
||||
}
|
||||
}
|
||||
fsi.Unlock()
|
||||
return rlkFile, ok
|
||||
}
|
||||
|
||||
// Locked path reference doesn't exist, freshly open the file in
|
||||
// read lock mode.
|
||||
// Open is a wrapper call to read locked file which
|
||||
// returns a ReadAtCloser.
|
||||
//
|
||||
// ReaderAt is provided so that the fd is non seekable, since
|
||||
// we are sharing fd's with concurrent threads, we don't want
|
||||
// all readers to change offsets on each other during such
|
||||
// concurrent operations. Using ReadAt allows us to read from
|
||||
// any offsets.
|
||||
//
|
||||
// Closer is implemented to track total readers and to close
|
||||
// only when there no more readers, the fd is purged if the lock
|
||||
// count has reached zero.
|
||||
func (fsi *fsIOPool) Open(path string) (*lock.RLockedFile, error) {
|
||||
if err := checkPathLength(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fsi.Lock()
|
||||
rlkFile, ok := fsi.lookupToRead(path)
|
||||
fsi.Unlock()
|
||||
// Locked path reference doesn't exist, acquire a read lock again on the file.
|
||||
if !ok {
|
||||
var err error
|
||||
var newRlkFile *lock.RLockedFile
|
||||
// Open file for reading.
|
||||
newRlkFile, err = lock.RLockedOpenFile(preparePath(path))
|
||||
// Open file for reading with read lock.
|
||||
newRlkFile, err := lock.RLockedOpenFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, errFileNotFound
|
||||
@@ -93,34 +103,29 @@ func (fsi *fsIOPool) Open(path string) (*lock.RLockedFile, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save new reader on the map.
|
||||
/// Save new reader on the map.
|
||||
|
||||
// It is possible by this time due to concurrent
|
||||
// i/o we might have another lock present. Lookup
|
||||
// again to check for such a possibility. If no such
|
||||
// file exists save the newly opened fd, if not
|
||||
// reuse the existing fd and close the newly opened
|
||||
// file
|
||||
fsi.Lock()
|
||||
rlkFile, ok = fsi.readersMap[path]
|
||||
if ok && rlkFile != nil {
|
||||
// If the file is closed and not removed from map is a bug.
|
||||
if rlkFile.IsClosed() {
|
||||
// Log this as an error.
|
||||
errorIf(errUnexpected, "Unexpected entry found on the map %s", path)
|
||||
|
||||
// Purge the cached lock path from map.
|
||||
delete(fsi.readersMap, path)
|
||||
|
||||
// Save the newly acquired read locked file.
|
||||
rlkFile = newRlkFile
|
||||
} else {
|
||||
// Increment the lock ref, since the file is not closed yet
|
||||
// and caller requested to read the file again.
|
||||
rlkFile.IncLockRef()
|
||||
newRlkFile.Close()
|
||||
}
|
||||
rlkFile, ok = fsi.lookupToRead(path)
|
||||
if ok {
|
||||
// Close the new fd, since we already seem to have
|
||||
// an active reference.
|
||||
newRlkFile.Close()
|
||||
} else {
|
||||
// Save the newly acquired read locked file.
|
||||
// Save the new rlk file.
|
||||
rlkFile = newRlkFile
|
||||
}
|
||||
|
||||
// Save the rlkFile back on the map.
|
||||
// Save the new fd on the map.
|
||||
fsi.readersMap[path] = rlkFile
|
||||
fsi.Unlock()
|
||||
|
||||
}
|
||||
|
||||
// Success.
|
||||
@@ -137,7 +142,7 @@ func (fsi *fsIOPool) Write(path string) (wlk *lock.LockedFile, err error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wlk, err = lock.LockedOpenFile(preparePath(path), os.O_RDWR, 0666)
|
||||
wlk, err = lock.LockedOpenFile(path, os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, errFileNotFound
|
||||
@@ -159,7 +164,7 @@ func (fsi *fsIOPool) Create(path string) (wlk *lock.LockedFile, err error) {
|
||||
}
|
||||
|
||||
// Creates parent if missing.
|
||||
if err = mkdirAll(pathutil.Dir(path), 0777); err != nil {
|
||||
if err = os.MkdirAll(pathutil.Dir(path), 0777); err != nil {
|
||||
if os.IsPermission(err) {
|
||||
return nil, errFileAccessDenied
|
||||
} else if isSysErrNotDir(err) {
|
||||
@@ -169,7 +174,7 @@ func (fsi *fsIOPool) Create(path string) (wlk *lock.LockedFile, err error) {
|
||||
}
|
||||
|
||||
// Attempt to create the file.
|
||||
wlk, err = lock.LockedOpenFile(preparePath(path), os.O_RDWR|os.O_CREATE, 0666)
|
||||
wlk, err = lock.LockedOpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
return nil, errFileAccessDenied
|
||||
@@ -182,7 +187,7 @@ func (fsi *fsIOPool) Create(path string) (wlk *lock.LockedFile, err error) {
|
||||
}
|
||||
|
||||
// Success.
|
||||
return wlk, err
|
||||
return wlk, nil
|
||||
}
|
||||
|
||||
// Close implements closing the path referenced by the reader in such
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
@@ -50,7 +51,7 @@ func TestRWPool(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create posix test setup, %s", err)
|
||||
}
|
||||
defer removeAll(path)
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
rwPool := &fsIOPool{
|
||||
readersMap: make(map[string]*lock.RLockedFile),
|
||||
|
||||
+177
-133
@@ -17,10 +17,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@@ -29,8 +27,8 @@ import (
|
||||
"sort"
|
||||
"syscall"
|
||||
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/lock"
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
// fsObjects - Implements fs object layer.
|
||||
@@ -62,17 +60,17 @@ func initMetaVolumeFS(fsPath, fsUUID string) error {
|
||||
// optimizing all other calls. Create minio meta volume,
|
||||
// if it doesn't exist yet.
|
||||
metaBucketPath := pathJoin(fsPath, minioMetaBucket)
|
||||
if err := mkdirAll(metaBucketPath, 0777); err != nil {
|
||||
if err := os.MkdirAll(metaBucketPath, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metaTmpPath := pathJoin(fsPath, minioMetaTmpBucket, fsUUID)
|
||||
if err := mkdirAll(metaTmpPath, 0777); err != nil {
|
||||
if err := os.MkdirAll(metaTmpPath, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metaMultipartPath := pathJoin(fsPath, minioMetaMultipartBucket)
|
||||
return mkdirAll(metaMultipartPath, 0777)
|
||||
return os.MkdirAll(metaMultipartPath, 0777)
|
||||
|
||||
}
|
||||
|
||||
@@ -89,7 +87,7 @@ func newFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := osStat(preparePath(fsPath))
|
||||
fi, err := os.Stat((fsPath))
|
||||
if err == nil {
|
||||
if !fi.IsDir() {
|
||||
return nil, syscall.ENOTDIR
|
||||
@@ -97,12 +95,22 @@ func newFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
// Disk not found create it.
|
||||
err = mkdirAll(fsPath, 0777)
|
||||
err = os.MkdirAll(fsPath, 0777)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
di, err := getDiskInfo((fsPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if disk has minimum required total space.
|
||||
if err = checkDiskMinTotal(di); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Assign a new UUID for FS minio mode. Each server instance
|
||||
// gets its own UUID for temporary file transaction.
|
||||
fsUUID := mustGetUUID()
|
||||
@@ -138,30 +146,33 @@ func newFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
||||
fs.fsFormatRlk = rlk
|
||||
|
||||
// Initialize and load bucket policies.
|
||||
err = initBucketPolicies(fs)
|
||||
if err != nil {
|
||||
if err = initBucketPolicies(fs); err != nil {
|
||||
return nil, fmt.Errorf("Unable to load all bucket policies. %s", err)
|
||||
}
|
||||
|
||||
// Initialize a new event notifier.
|
||||
err = initEventNotifier(fs)
|
||||
if err != nil {
|
||||
if err = initEventNotifier(fs); err != nil {
|
||||
return nil, fmt.Errorf("Unable to initialize event notification. %s", err)
|
||||
}
|
||||
|
||||
// Start background process to cleanup old files in `.minio.sys`.
|
||||
go fs.cleanupStaleMultipartUploads(fsMultipartCleanupInterval, fsMultipartExpiry, globalServiceDoneCh)
|
||||
|
||||
// Return successfully initialized object layer.
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
// Should be called when process shuts down.
|
||||
func (fs fsObjects) Shutdown() error {
|
||||
fs.fsFormatRlk.Close()
|
||||
|
||||
// Cleanup and delete tmp uuid.
|
||||
return fsRemoveAll(pathJoin(fs.fsPath, minioMetaTmpBucket, fs.fsUUID))
|
||||
}
|
||||
|
||||
// StorageInfo - returns underlying storage statistics.
|
||||
func (fs fsObjects) StorageInfo() StorageInfo {
|
||||
info, err := getDiskInfo(preparePath(fs.fsPath))
|
||||
info, err := getDiskInfo((fs.fsPath))
|
||||
errorIf(err, "Unable to get disk info %#v", fs.fsPath)
|
||||
storageInfo := StorageInfo{
|
||||
Total: info.Total,
|
||||
@@ -191,7 +202,7 @@ func (fs fsObjects) statBucketDir(bucket string) (os.FileInfo, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
st, err := fsStatDir(bucketDir)
|
||||
st, err := fsStatVolume(bucketDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -214,13 +225,13 @@ func (fs fsObjects) MakeBucketWithLocation(bucket, location string) error {
|
||||
}
|
||||
|
||||
// GetBucketInfo - fetch bucket metadata info.
|
||||
func (fs fsObjects) GetBucketInfo(bucket string) (BucketInfo, error) {
|
||||
func (fs fsObjects) GetBucketInfo(bucket string) (bi BucketInfo, e error) {
|
||||
st, err := fs.statBucketDir(bucket)
|
||||
if err != nil {
|
||||
return BucketInfo{}, toObjectErr(err, bucket)
|
||||
return bi, toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// As osStat() doesn't carry other than ModTime(), use ModTime() as CreatedTime.
|
||||
// As os.Stat() doesn't carry other than ModTime(), use ModTime() as CreatedTime.
|
||||
createdTime := st.ModTime()
|
||||
return BucketInfo{
|
||||
Name: bucket,
|
||||
@@ -234,7 +245,7 @@ func (fs fsObjects) ListBuckets() ([]BucketInfo, error) {
|
||||
return nil, traceError(err)
|
||||
}
|
||||
var bucketInfos []BucketInfo
|
||||
entries, err := readDir(preparePath(fs.fsPath))
|
||||
entries, err := readDir((fs.fsPath))
|
||||
if err != nil {
|
||||
return nil, toObjectErr(traceError(errDiskNotFound))
|
||||
}
|
||||
@@ -245,21 +256,18 @@ func (fs fsObjects) ListBuckets() ([]BucketInfo, error) {
|
||||
continue
|
||||
}
|
||||
var fi os.FileInfo
|
||||
fi, err = fsStatDir(pathJoin(fs.fsPath, entry))
|
||||
fi, err = fsStatVolume(pathJoin(fs.fsPath, entry))
|
||||
// There seems like no practical reason to check for errors
|
||||
// at this point, if there are indeed errors we can simply
|
||||
// just ignore such buckets and list only those which
|
||||
// return proper Stat information instead.
|
||||
if err != nil {
|
||||
// If the directory does not exist, skip the entry.
|
||||
if errorCause(err) == errVolumeNotFound {
|
||||
continue
|
||||
} else if errorCause(err) == errVolumeAccessDenied {
|
||||
// Skip the entry if its a file.
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
// Ignore any errors returned here.
|
||||
continue
|
||||
}
|
||||
|
||||
bucketInfos = append(bucketInfos, BucketInfo{
|
||||
Name: fi.Name(),
|
||||
// As osStat() doesnt carry CreatedTime, use ModTime() as CreatedTime.
|
||||
// As os.Stat() doesnt carry CreatedTime, use ModTime() as CreatedTime.
|
||||
Created: fi.ModTime(),
|
||||
})
|
||||
}
|
||||
@@ -304,15 +312,15 @@ func (fs fsObjects) DeleteBucket(bucket string) error {
|
||||
// CopyObject - copy object source object to destination object.
|
||||
// if source object and destination object are same we only
|
||||
// update metadata.
|
||||
func (fs fsObjects) CopyObject(srcBucket, srcObject, dstBucket, dstObject string, metadata map[string]string) (ObjectInfo, error) {
|
||||
func (fs fsObjects) CopyObject(srcBucket, srcObject, dstBucket, dstObject string, metadata map[string]string) (oi ObjectInfo, e error) {
|
||||
if _, err := fs.statBucketDir(srcBucket); err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, srcBucket)
|
||||
return oi, toObjectErr(err, srcBucket)
|
||||
}
|
||||
|
||||
// Stat the file to get file size.
|
||||
fi, err := fsStatFile(pathJoin(fs.fsPath, srcBucket, srcObject))
|
||||
if err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, srcBucket, srcObject)
|
||||
return oi, toObjectErr(err, srcBucket, srcObject)
|
||||
}
|
||||
|
||||
// Check if this request is only metadata update.
|
||||
@@ -322,7 +330,7 @@ func (fs fsObjects) CopyObject(srcBucket, srcObject, dstBucket, dstObject string
|
||||
var wlk *lock.LockedFile
|
||||
wlk, err = fs.rwPool.Write(fsMetaPath)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, toObjectErr(traceError(err), srcBucket, srcObject)
|
||||
return oi, toObjectErr(traceError(err), srcBucket, srcObject)
|
||||
}
|
||||
// This close will allow for locks to be synchronized on `fs.json`.
|
||||
defer wlk.Close()
|
||||
@@ -331,7 +339,7 @@ func (fs fsObjects) CopyObject(srcBucket, srcObject, dstBucket, dstObject string
|
||||
fsMeta := newFSMetaV1()
|
||||
fsMeta.Meta = metadata
|
||||
if _, err = fsMeta.WriteTo(wlk); err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, srcBucket, srcObject)
|
||||
return oi, toObjectErr(err, srcBucket, srcObject)
|
||||
}
|
||||
|
||||
// Return the new object info.
|
||||
@@ -354,9 +362,14 @@ func (fs fsObjects) CopyObject(srcBucket, srcObject, dstBucket, dstObject string
|
||||
pipeWriter.Close() // Close writer explicitly signalling we wrote all data.
|
||||
}()
|
||||
|
||||
objInfo, err := fs.PutObject(dstBucket, dstObject, length, pipeReader, metadata, "")
|
||||
hashReader, err := hash.NewReader(pipeReader, length, "", "")
|
||||
if err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, dstBucket, dstObject)
|
||||
return oi, toObjectErr(err, dstBucket, dstObject)
|
||||
}
|
||||
|
||||
objInfo, err := fs.PutObject(dstBucket, dstObject, hashReader, metadata)
|
||||
if err != nil {
|
||||
return oi, toObjectErr(err, dstBucket, dstObject)
|
||||
}
|
||||
|
||||
// Explicitly close the reader.
|
||||
@@ -372,7 +385,7 @@ func (fs fsObjects) CopyObject(srcBucket, srcObject, dstBucket, dstObject string
|
||||
// startOffset indicates the starting read location of the object.
|
||||
// length indicates the total length of the object.
|
||||
func (fs fsObjects) GetObject(bucket, object string, offset int64, length int64, writer io.Writer) (err error) {
|
||||
if err = checkGetObjArgs(bucket, object); err != nil {
|
||||
if err = checkBucketAndObjectNamesFS(bucket, object); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -390,6 +403,12 @@ func (fs fsObjects) GetObject(bucket, object string, offset int64, length int64,
|
||||
return toObjectErr(traceError(errUnexpected), bucket, object)
|
||||
}
|
||||
|
||||
// If its a directory request, we return an empty body.
|
||||
if hasSuffix(object, slashSeparator) {
|
||||
_, err = writer.Write([]byte(""))
|
||||
return toObjectErr(traceError(err), bucket, object)
|
||||
}
|
||||
|
||||
if bucket != minioMetaBucket {
|
||||
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, bucket, object, fsMetaJSONFile)
|
||||
_, err = fs.rwPool.Open(fsMetaPath)
|
||||
@@ -431,8 +450,21 @@ func (fs fsObjects) GetObject(bucket, object string, offset int64, length int64,
|
||||
}
|
||||
|
||||
// getObjectInfo - wrapper for reading object metadata and constructs ObjectInfo.
|
||||
func (fs fsObjects) getObjectInfo(bucket, object string) (ObjectInfo, error) {
|
||||
func (fs fsObjects) getObjectInfo(bucket, object string) (oi ObjectInfo, e error) {
|
||||
fsMeta := fsMetaV1{}
|
||||
fi, err := fsStatDir(pathJoin(fs.fsPath, bucket, object))
|
||||
if err != nil && errorCause(err) != errFileAccessDenied {
|
||||
return oi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
if fi != nil {
|
||||
// If file found and request was with object ending with "/", consider it
|
||||
// as directory and return object info
|
||||
if hasSuffix(object, slashSeparator) {
|
||||
return fsMeta.ToObjectInfo(bucket, object, fi), nil
|
||||
}
|
||||
return oi, toObjectErr(errFileNotFound, bucket, object)
|
||||
}
|
||||
|
||||
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, bucket, object, fsMetaJSONFile)
|
||||
|
||||
// Read `fs.json` to perhaps contend with
|
||||
@@ -446,33 +478,49 @@ func (fs fsObjects) getObjectInfo(bucket, object string) (ObjectInfo, error) {
|
||||
// PutObject() transaction, if we arrive at such
|
||||
// a situation we just ignore and continue.
|
||||
if errorCause(rerr) != io.EOF {
|
||||
return ObjectInfo{}, toObjectErr(rerr, bucket, object)
|
||||
return oi, toObjectErr(rerr, bucket, object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore if `fs.json` is not available, this is true for pre-existing data.
|
||||
if err != nil && err != errFileNotFound {
|
||||
return ObjectInfo{}, toObjectErr(traceError(err), bucket, object)
|
||||
return oi, toObjectErr(traceError(err), bucket, object)
|
||||
}
|
||||
|
||||
// Stat the file to get file size.
|
||||
fi, err := fsStatFile(pathJoin(fs.fsPath, bucket, object))
|
||||
fi, err = fsStatFile(pathJoin(fs.fsPath, bucket, object))
|
||||
if err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
return oi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
return fsMeta.ToObjectInfo(bucket, object, fi), nil
|
||||
}
|
||||
|
||||
// Checks bucket and object name validity, returns nil if both are valid.
|
||||
func checkBucketAndObjectNamesFS(bucket, object string) error {
|
||||
// Verify if bucket is valid.
|
||||
if !IsValidBucketName(bucket) {
|
||||
return traceError(BucketNameInvalid{Bucket: bucket})
|
||||
}
|
||||
// Verify if object is valid.
|
||||
if len(object) == 0 {
|
||||
return traceError(ObjectNameInvalid{Bucket: bucket, Object: object})
|
||||
}
|
||||
if !IsValidObjectPrefix(object) {
|
||||
return traceError(ObjectNameInvalid{Bucket: bucket, Object: object})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetObjectInfo - reads object metadata and replies back ObjectInfo.
|
||||
func (fs fsObjects) GetObjectInfo(bucket, object string) (ObjectInfo, error) {
|
||||
if err := checkGetObjArgs(bucket, object); err != nil {
|
||||
return ObjectInfo{}, err
|
||||
func (fs fsObjects) GetObjectInfo(bucket, object string) (oi ObjectInfo, e error) {
|
||||
if err := checkBucketAndObjectNamesFS(bucket, object); err != nil {
|
||||
return oi, err
|
||||
}
|
||||
|
||||
if _, err := fs.statBucketDir(bucket); err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, bucket)
|
||||
return oi, toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
return fs.getObjectInfo(bucket, object)
|
||||
@@ -484,13 +532,14 @@ func (fs fsObjects) GetObjectInfo(bucket, object string) (ObjectInfo, error) {
|
||||
func (fs fsObjects) parentDirIsObject(bucket, parent string) bool {
|
||||
var isParentDirObject func(string) bool
|
||||
isParentDirObject = func(p string) bool {
|
||||
if p == "." {
|
||||
if p == "." || p == "/" {
|
||||
return false
|
||||
}
|
||||
if _, err := fsStatFile(pathJoin(fs.fsPath, bucket, p)); err == nil {
|
||||
// If there is already a file at prefix "p" return error.
|
||||
// If there is already a file at prefix "p", return true.
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if there is a file as one of the parent paths.
|
||||
return isParentDirObject(path.Dir(p))
|
||||
}
|
||||
@@ -501,7 +550,12 @@ func (fs fsObjects) parentDirIsObject(bucket, parent string) bool {
|
||||
// until EOF, writes data directly to configured filesystem path.
|
||||
// Additionally writes `fs.json` which carries the necessary metadata
|
||||
// for future object operations.
|
||||
func (fs fsObjects) PutObject(bucket string, object string, size int64, data io.Reader, metadata map[string]string, sha256sum string) (objInfo ObjectInfo, retErr error) {
|
||||
func (fs fsObjects) PutObject(bucket string, object string, data *hash.Reader, metadata map[string]string) (objInfo ObjectInfo, retErr error) {
|
||||
// No metadata is set, allocate a new one.
|
||||
if metadata == nil {
|
||||
metadata = make(map[string]string)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// Validate if bucket name is valid and exists.
|
||||
@@ -509,15 +563,25 @@ func (fs fsObjects) PutObject(bucket string, object string, size int64, data io.
|
||||
return ObjectInfo{}, toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
fsMeta := newFSMetaV1()
|
||||
fsMeta.Meta = metadata
|
||||
|
||||
// This is a special case with size as '0' and object ends
|
||||
// with a slash separator, we treat it like a valid operation
|
||||
// and return success.
|
||||
if isObjectDir(object, size) {
|
||||
if isObjectDir(object, data.Size()) {
|
||||
// Check if an object is present as one of the parent dir.
|
||||
if fs.parentDirIsObject(bucket, path.Dir(object)) {
|
||||
return ObjectInfo{}, toObjectErr(traceError(errFileAccessDenied), bucket, object)
|
||||
}
|
||||
return dirObjectInfo(bucket, object, size, metadata), nil
|
||||
if err = fsMkdirAll(pathJoin(fs.fsPath, bucket, object)); err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
var fi os.FileInfo
|
||||
if fi, err = fsStatDir(pathJoin(fs.fsPath, bucket, object)); err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
return fsMeta.ToObjectInfo(bucket, object, fi), nil
|
||||
}
|
||||
|
||||
if err = checkPutObjectArgs(bucket, object, fs); err != nil {
|
||||
@@ -529,14 +593,11 @@ func (fs fsObjects) PutObject(bucket string, object string, size int64, data io.
|
||||
return ObjectInfo{}, toObjectErr(traceError(errFileAccessDenied), bucket, object)
|
||||
}
|
||||
|
||||
// No metadata is set, allocate a new one.
|
||||
if metadata == nil {
|
||||
metadata = make(map[string]string)
|
||||
// Validate input data size and it can never be less than zero.
|
||||
if data.Size() < 0 {
|
||||
return ObjectInfo{}, traceError(errInvalidArgument)
|
||||
}
|
||||
|
||||
fsMeta := newFSMetaV1()
|
||||
fsMeta.Meta = metadata
|
||||
|
||||
var wlk *lock.LockedFile
|
||||
if bucket != minioMetaBucket {
|
||||
bucketMetaDir := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix)
|
||||
@@ -561,46 +622,26 @@ func (fs fsObjects) PutObject(bucket string, object string, size int64, data io.
|
||||
// so that cleaning it up will be easy if the server goes down.
|
||||
tempObj := mustGetUUID()
|
||||
|
||||
// Initialize md5 writer.
|
||||
md5Writer := md5.New()
|
||||
|
||||
hashWriters := []io.Writer{md5Writer}
|
||||
|
||||
var sha256Writer hash.Hash
|
||||
if sha256sum != "" {
|
||||
sha256Writer = sha256.New()
|
||||
hashWriters = append(hashWriters, sha256Writer)
|
||||
}
|
||||
multiWriter := io.MultiWriter(hashWriters...)
|
||||
|
||||
// Limit the reader to its provided size if specified.
|
||||
var limitDataReader io.Reader
|
||||
if size > 0 {
|
||||
// This is done so that we can avoid erroneous clients sending more data than the set content size.
|
||||
limitDataReader = io.LimitReader(data, size)
|
||||
} else {
|
||||
// else we read till EOF.
|
||||
limitDataReader = data
|
||||
}
|
||||
|
||||
// Allocate a buffer to Read() from request body
|
||||
bufSize := int64(readSizeV1)
|
||||
if size > 0 && bufSize > size {
|
||||
if size := data.Size(); size > 0 && bufSize > size {
|
||||
bufSize = size
|
||||
}
|
||||
|
||||
buf := make([]byte, int(bufSize))
|
||||
teeReader := io.TeeReader(limitDataReader, multiWriter)
|
||||
fsTmpObjPath := pathJoin(fs.fsPath, minioMetaTmpBucket, fs.fsUUID, tempObj)
|
||||
bytesWritten, err := fsCreateFile(fsTmpObjPath, teeReader, buf, size)
|
||||
bytesWritten, err := fsCreateFile(fsTmpObjPath, data, buf, data.Size())
|
||||
if err != nil {
|
||||
fsRemoveFile(fsTmpObjPath)
|
||||
errorIf(err, "Failed to create object %s/%s", bucket, object)
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
metadata["etag"] = hex.EncodeToString(data.MD5Current())
|
||||
|
||||
// Should return IncompleteBody{} error when reader has fewer
|
||||
// bytes than specified in request header.
|
||||
if bytesWritten < size {
|
||||
if bytesWritten < data.Size() {
|
||||
fsRemoveFile(fsTmpObjPath)
|
||||
return ObjectInfo{}, traceError(IncompleteBody{})
|
||||
}
|
||||
@@ -610,28 +651,6 @@ func (fs fsObjects) PutObject(bucket string, object string, size int64, data io.
|
||||
// nothing to delete.
|
||||
defer fsRemoveFile(fsTmpObjPath)
|
||||
|
||||
newMD5Hex := hex.EncodeToString(md5Writer.Sum(nil))
|
||||
// Update the md5sum if not set with the newly calculated one.
|
||||
if len(metadata["etag"]) == 0 {
|
||||
metadata["etag"] = newMD5Hex
|
||||
}
|
||||
|
||||
// md5Hex representation.
|
||||
md5Hex := metadata["etag"]
|
||||
if md5Hex != "" {
|
||||
if newMD5Hex != md5Hex {
|
||||
// Returns md5 mismatch.
|
||||
return ObjectInfo{}, traceError(BadDigest{md5Hex, newMD5Hex})
|
||||
}
|
||||
}
|
||||
|
||||
if sha256sum != "" {
|
||||
newSHA256sum := hex.EncodeToString(sha256Writer.Sum(nil))
|
||||
if newSHA256sum != sha256sum {
|
||||
return ObjectInfo{}, traceError(SHA256Mismatch{})
|
||||
}
|
||||
}
|
||||
|
||||
// Entire object was written to the temp location, now it's safe to rename it to the actual location.
|
||||
fsNSObjPath := pathJoin(fs.fsPath, bucket, object)
|
||||
if err = fsRenameFile(fsTmpObjPath, fsNSObjPath); err != nil {
|
||||
@@ -658,7 +677,7 @@ func (fs fsObjects) PutObject(bucket string, object string, size int64, data io.
|
||||
// DeleteObject - deletes an object from a bucket, this operation is destructive
|
||||
// and there are no rollbacks supported.
|
||||
func (fs fsObjects) DeleteObject(bucket, object string) error {
|
||||
if err := checkDelObjArgs(bucket, object); err != nil {
|
||||
if err := checkBucketAndObjectNamesFS(bucket, object); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -739,14 +758,24 @@ func (fs fsObjects) getObjectETag(bucket, entry string) (string, error) {
|
||||
// Read from fs metadata only if it exists.
|
||||
defer fs.rwPool.Close(fsMetaPath)
|
||||
|
||||
fsMetaBuf, err := ioutil.ReadAll(rlk.LockedFile)
|
||||
// Fetch the size of the underlying file.
|
||||
fi, err := rlk.LockedFile.Stat()
|
||||
if err != nil {
|
||||
// `fs.json` can be empty due to previously failed
|
||||
// PutObject() transaction, if we arrive at such
|
||||
// a situation we just ignore and continue.
|
||||
if errorCause(err) != io.EOF {
|
||||
return "", toObjectErr(err, bucket, entry)
|
||||
}
|
||||
return "", toObjectErr(traceError(err), bucket, entry)
|
||||
}
|
||||
|
||||
// `fs.json` can be empty due to previously failed
|
||||
// PutObject() transaction, if we arrive at such
|
||||
// a situation we just ignore and continue.
|
||||
if fi.Size() == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Wrap the locked file in a ReadAt() backend section reader to
|
||||
// make sure the underlying offsets don't move.
|
||||
fsMetaBuf, err := ioutil.ReadAll(io.NewSectionReader(rlk.LockedFile, 0, fi.Size()))
|
||||
if err != nil {
|
||||
return "", traceError(err)
|
||||
}
|
||||
|
||||
// Check if FS metadata is valid, if not return error.
|
||||
@@ -759,18 +788,18 @@ func (fs fsObjects) getObjectETag(bucket, entry string) (string, error) {
|
||||
|
||||
// ListObjects - list all objects at prefix upto maxKeys., optionally delimited by '/'. Maintains the list pool
|
||||
// state for future re-entrant list requests.
|
||||
func (fs fsObjects) ListObjects(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
||||
func (fs fsObjects) ListObjects(bucket, prefix, marker, delimiter string, maxKeys int) (loi ListObjectsInfo, e error) {
|
||||
if err := checkListObjsArgs(bucket, prefix, marker, delimiter, fs); err != nil {
|
||||
return ListObjectsInfo{}, err
|
||||
return loi, err
|
||||
}
|
||||
|
||||
if _, err := fs.statBucketDir(bucket); err != nil {
|
||||
return ListObjectsInfo{}, err
|
||||
return loi, err
|
||||
}
|
||||
|
||||
// With max keys of zero we have reached eof, return right here.
|
||||
if maxKeys == 0 {
|
||||
return ListObjectsInfo{}, nil
|
||||
return loi, nil
|
||||
}
|
||||
|
||||
// For delimiter and prefix as '/' we do not list anything at all
|
||||
@@ -779,7 +808,7 @@ func (fs fsObjects) ListObjects(bucket, prefix, marker, delimiter string, maxKey
|
||||
// as '/' we don't have any entries, since all the keys are
|
||||
// of form 'keyName/...'
|
||||
if delimiter == slashSeparator && prefix == slashSeparator {
|
||||
return ListObjectsInfo{}, nil
|
||||
return loi, nil
|
||||
}
|
||||
|
||||
// Over flowing count - reset to maxObjectList.
|
||||
@@ -795,16 +824,30 @@ func (fs fsObjects) ListObjects(bucket, prefix, marker, delimiter string, maxKey
|
||||
|
||||
// Convert entry to ObjectInfo
|
||||
entryToObjectInfo := func(entry string) (objInfo ObjectInfo, err error) {
|
||||
if hasSuffix(entry, slashSeparator) {
|
||||
// Object name needs to be full path.
|
||||
objInfo.Name = entry
|
||||
objInfo.IsDir = true
|
||||
return
|
||||
// Protect the entry from concurrent deletes, or renames.
|
||||
objectLock := globalNSMutex.NewNSLock(bucket, entry)
|
||||
if err = objectLock.GetRLock(globalListingTimeout); err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
|
||||
if hasSuffix(entry, slashSeparator) {
|
||||
var fi os.FileInfo
|
||||
fi, err = fsStatDir(pathJoin(fs.fsPath, bucket, entry))
|
||||
objectLock.RUnlock()
|
||||
if err != nil {
|
||||
return objInfo, err
|
||||
}
|
||||
// Success.
|
||||
return ObjectInfo{
|
||||
// Object name needs to be full path.
|
||||
Name: entry,
|
||||
Bucket: bucket,
|
||||
Size: fi.Size(),
|
||||
ModTime: fi.ModTime(),
|
||||
IsDir: fi.IsDir(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Protect reading `fs.json`.
|
||||
objectLock := globalNSMutex.NewNSLock(bucket, entry)
|
||||
objectLock.RLock()
|
||||
var etag string
|
||||
etag, err = fs.getObjectETag(bucket, entry)
|
||||
objectLock.RUnlock()
|
||||
@@ -860,13 +903,14 @@ func (fs fsObjects) ListObjects(bucket, prefix, marker, delimiter string, maxKey
|
||||
if walkResult.err != nil {
|
||||
// File not found is a valid case.
|
||||
if errorCause(walkResult.err) == errFileNotFound {
|
||||
return ListObjectsInfo{}, nil
|
||||
return loi, nil
|
||||
}
|
||||
return ListObjectsInfo{}, toObjectErr(walkResult.err, bucket, prefix)
|
||||
return loi, toObjectErr(walkResult.err, bucket, prefix)
|
||||
}
|
||||
objInfo, err := entryToObjectInfo(walkResult.entry)
|
||||
if err != nil {
|
||||
return ListObjectsInfo{}, nil
|
||||
errorIf(err, "Unable to fetch object info for %s", walkResult.entry)
|
||||
return loi, nil
|
||||
}
|
||||
nextMarker = objInfo.Name
|
||||
objInfos = append(objInfos, objInfo)
|
||||
@@ -908,8 +952,8 @@ func (fs fsObjects) HealBucket(bucket string) error {
|
||||
}
|
||||
|
||||
// ListObjectsHeal - list all objects to be healed. Valid only for XL
|
||||
func (fs fsObjects) ListObjectsHeal(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
||||
return ListObjectsInfo{}, traceError(NotImplemented{})
|
||||
func (fs fsObjects) ListObjectsHeal(bucket, prefix, marker, delimiter string, maxKeys int) (loi ListObjectsInfo, e error) {
|
||||
return loi, traceError(NotImplemented{})
|
||||
}
|
||||
|
||||
// ListBucketsHeal - list all buckets to be healed. Valid only for XL
|
||||
@@ -918,6 +962,6 @@ func (fs fsObjects) ListBucketsHeal() ([]BucketInfo, error) {
|
||||
}
|
||||
|
||||
func (fs fsObjects) ListUploadsHeal(bucket, prefix, marker, uploadIDMarker,
|
||||
delimiter string, maxUploads int) (ListMultipartsInfo, error) {
|
||||
return ListMultipartsInfo{}, traceError(NotImplemented{})
|
||||
delimiter string, maxUploads int) (lmi ListMultipartsInfo, e error) {
|
||||
return lmi, traceError(NotImplemented{})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user