mirror of
https://github.com/pgsty/minio.git
synced 2026-10-18 23:46:11 +03:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d44e38b462 | |||
| 8948c972ee | |||
| 29b49f9343 | |||
| f8e4700a11 | |||
| 4c5c00c640 | |||
| 0be44133d4 | |||
| 817e763416 | |||
| 0abab5305f | |||
| e6e855a7cf | |||
| 6fca1d164c | |||
| 9b76eba37e | |||
| 03714d4809 | |||
| 3a73ea3f54 | |||
| 9625629fc7 | |||
| 8507ce2a87 |
@@ -26,8 +26,8 @@
|
||||
|
||||
## Your Environment
|
||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||
* Version used (`minio version`):
|
||||
* Version used:
|
||||
* Environment name and version (e.g. nginx 1.9.1):
|
||||
* Server type and version:
|
||||
* Operating System and version (`uname -a`):
|
||||
* Operating System and version:
|
||||
* Link to your project:
|
||||
|
||||
@@ -24,6 +24,5 @@
|
||||
- [ ] My code follows the code style of this project.
|
||||
- [ ] My change requires a change to the documentation.
|
||||
- [ ] I have updated the documentation accordingly.
|
||||
- [ ] I have added unit tests to cover my changes.
|
||||
- [ ] I have added/updated functional tests in [mint](https://github.com/minio/mint). (If yes, add `mint` PR # here: )
|
||||
- [ ] I have added tests to cover my changes.
|
||||
- [ ] All new and existing tests passed.
|
||||
+1
-10
@@ -9,19 +9,10 @@ site/
|
||||
/.idea/
|
||||
/Minio.iml
|
||||
**/access.log
|
||||
build/
|
||||
build
|
||||
vendor/**/*.js
|
||||
vendor/**/*.json
|
||||
release
|
||||
.DS_Store
|
||||
*.syso
|
||||
coverage.txt
|
||||
.vscode/
|
||||
.snap
|
||||
*.tar.bz2
|
||||
parts/
|
||||
prime/
|
||||
snap/.snapcraft/
|
||||
stage/
|
||||
.sia_temp/
|
||||
buildcoveragecoverage.txt
|
||||
+3
-8
@@ -1,9 +1,6 @@
|
||||
go_import_path: github.com/minio/minio
|
||||
sudo: required
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
dist: trusty
|
||||
|
||||
language: go
|
||||
@@ -13,17 +10,15 @@ os:
|
||||
|
||||
env:
|
||||
- ARCH=x86_64
|
||||
- ARCH=i686
|
||||
|
||||
script:
|
||||
## Run all the tests
|
||||
- make
|
||||
- diff -au <(gofmt -s -d cmd) <(printf "")
|
||||
- diff -au <(gofmt -s -d pkg) <(printf "")
|
||||
- make test GOFLAGS="-timeout 15m -race -v"
|
||||
- make test GOFLAGS="-timeout 20m -race -v"
|
||||
- make coverage
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
go:
|
||||
- 1.9.4
|
||||
- 1.7.4
|
||||
|
||||
+49
-49
@@ -1,14 +1,9 @@
|
||||
# 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)
|
||||
### 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:
|
||||
If you do not have a working Golang environment setup please follow [Golang Installation Guide](https://docs.minio.io/docs/how-to-install-golang).
|
||||
|
||||
### Setup your Minio Github Repository
|
||||
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).
|
||||
|
||||
Fork [Minio upstream](https://github.com/minio/minio/fork) source repository to your own personal repository. Copy the URL for minio from your personal github repo (you will need it for the `git clone` command below).
|
||||
```sh
|
||||
$ mkdir -p $GOPATH/src/github.com/minio
|
||||
$ cd $GOPATH/src/github.com/minio
|
||||
@@ -16,56 +11,61 @@ $ git clone <paste saved URL for personal forked minio repo>
|
||||
$ cd minio
|
||||
```
|
||||
|
||||
### Set up git remote as ``upstream``
|
||||
### 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``
|
||||
```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
|
||||
...
|
||||
```
|
||||
|
||||
### Create your feature branch
|
||||
Before making code changes, make sure you create a separate branch for these changes
|
||||
### 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
|
||||
|
||||
```
|
||||
$ git checkout -b my-new-feature
|
||||
```
|
||||
* 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
|
||||
|
||||
### Test Minio server changes
|
||||
After your code changes, make sure
|
||||
* 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
|
||||
|
||||
- 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.
|
||||
* 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.
|
||||
|
||||
### 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).
|
||||
* 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
|
||||
|
||||
+11
-28
@@ -1,34 +1,17 @@
|
||||
FROM golang:1.9.4-alpine3.6
|
||||
FROM golang:1.7-alpine
|
||||
|
||||
MAINTAINER Minio Inc <dev@minio.io>
|
||||
WORKDIR /go/src/app
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV PATH $PATH:$GOPATH/bin
|
||||
ENV CGO_ENABLED 0
|
||||
ENV MINIO_UPDATE off
|
||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
MINIO_SECRET_KEY_FILE=secret_key
|
||||
COPY . /go/src/app
|
||||
|
||||
WORKDIR /go/src/github.com/minio/
|
||||
|
||||
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates curl && \
|
||||
apk add --no-cache --virtual .build-deps git && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
go get -v -d github.com/minio/minio && \
|
||||
cd /go/src/github.com/minio/minio && \
|
||||
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
|
||||
rm -rf /go/pkg /go/src /usr/local/go && apk del .build-deps
|
||||
RUN \
|
||||
apk add --no-cache git && \
|
||||
go-wrapper download && \
|
||||
go-wrapper install -ldflags "-X github.com/minio/minio/cmd.Version=2017-02-15T22:55:24Z -X github.com/minio/minio/cmd.ReleaseTag=RELEASE.2017-02-15T22-55-24Z -X github.com/minio/minio/cmd.CommitID=3e770defae48e86f4406209d3942dcac85cba73a" && \
|
||||
mkdir -p /export/docker && \
|
||||
rm -rf /go/pkg /go/src && \
|
||||
apk del git
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
|
||||
ENTRYPOINT ["minio"]
|
||||
VOLUME ["/export"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s \
|
||||
CMD /usr/bin/healthcheck.sh
|
||||
|
||||
CMD ["minio"]
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
FROM golang:1.9.4-alpine3.6
|
||||
|
||||
MAINTAINER Minio Inc <dev@minio.io>
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV PATH $PATH:$GOPATH/bin
|
||||
ENV CGO_ENABLED 0
|
||||
ENV MINIO_UPDATE off
|
||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
MINIO_SECRET_KEY_FILE=secret_key
|
||||
|
||||
WORKDIR /go/src/github.com/minio/
|
||||
|
||||
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||
|
||||
COPY . /go/src/github.com/minio/minio
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates curl && \
|
||||
apk add --no-cache --virtual .build-deps git && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
cd /go/src/github.com/minio/minio && \
|
||||
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
|
||||
rm -rf /go/pkg /go/src /usr/local/go && apk del .build-deps
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
|
||||
VOLUME ["/export"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s \
|
||||
CMD /usr/bin/healthcheck.sh
|
||||
|
||||
CMD ["minio"]
|
||||
@@ -1,30 +0,0 @@
|
||||
FROM alpine:3.6
|
||||
|
||||
MAINTAINER Minio Inc <dev@minio.io>
|
||||
|
||||
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||
|
||||
ENV MINIO_UPDATE off
|
||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
MINIO_SECRET_KEY_FILE=secret_key
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates && \
|
||||
apk add --no-cache --virtual .build-deps curl && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
curl https://dl.minio.io/server/minio/release/linux-amd64/minio > /usr/bin/minio && \
|
||||
chmod +x /usr/bin/minio && \
|
||||
chmod +x /usr/bin/docker-entrypoint.sh && \
|
||||
chmod +x /usr/bin/healthcheck.sh
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
|
||||
VOLUME ["/export"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s \
|
||||
CMD /usr/bin/healthcheck.sh
|
||||
|
||||
CMD ["minio"]
|
||||
|
||||
@@ -1,100 +1,146 @@
|
||||
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
|
||||
|
||||
all: build
|
||||
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
|
||||
|
||||
checks:
|
||||
@echo "Checking dependencies"
|
||||
@echo "Checking deps:"
|
||||
@(env bash $(PWD)/buildscripts/checkdeps.sh)
|
||||
@echo "Checking for project in GOPATH"
|
||||
@(env bash $(PWD)/buildscripts/checkgopath.sh)
|
||||
|
||||
getdeps:
|
||||
@echo "Installing golint" && go get -u github.com/golang/lint/golint
|
||||
@echo "Installing gocyclo" && go get -u github.com/fzipp/gocyclo
|
||||
@echo "Installing deadcode" && go get -u github.com/remyoudompheng/go-misc/deadcode
|
||||
@echo "Installing misspell" && go get -u github.com/client9/misspell/cmd/misspell
|
||||
@echo "Installing ineffassign" && go get -u github.com/gordonklaus/ineffassign
|
||||
getdeps: checks
|
||||
@echo "Installing golint:" && go get -u github.com/golang/lint/golint
|
||||
@echo "Installing gocyclo:" && go get -u github.com/fzipp/gocyclo
|
||||
@echo "Installing deadcode:" && go get -u github.com/remyoudompheng/go-misc/deadcode
|
||||
@echo "Installing misspell:" && go get -u github.com/client9/misspell/cmd/misspell
|
||||
@echo "Installing ineffassign:" && go get -u github.com/gordonklaus/ineffassign
|
||||
|
||||
verifiers: getdeps vet fmt lint cyclo deadcode spelling
|
||||
verifiers: vet fmt lint cyclo spelling
|
||||
|
||||
vet:
|
||||
@echo "Running $@"
|
||||
@go tool vet -atomic -bool -copylocks -nilfunc -printf -shadow -rangeloops -unreachable -unsafeptr -unusedresult cmd
|
||||
@go tool vet -atomic -bool -copylocks -nilfunc -printf -shadow -rangeloops -unreachable -unsafeptr -unusedresult pkg
|
||||
@echo "Running $@:"
|
||||
@go tool vet -all ./cmd
|
||||
@go tool vet -all ./pkg
|
||||
@go tool vet -shadow=true ./cmd
|
||||
@go tool vet -shadow=true ./pkg
|
||||
|
||||
fmt:
|
||||
@echo "Running $@"
|
||||
@gofmt -d cmd
|
||||
@gofmt -d pkg
|
||||
@echo "Running $@:"
|
||||
@gofmt -s -l cmd
|
||||
@gofmt -s -l pkg
|
||||
|
||||
lint:
|
||||
@echo "Running $@"
|
||||
@echo "Running $@:"
|
||||
@${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/cmd...
|
||||
@${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/pkg...
|
||||
|
||||
ineffassign:
|
||||
@echo "Running $@"
|
||||
@echo "Running $@:"
|
||||
@${GOPATH}/bin/ineffassign .
|
||||
|
||||
cyclo:
|
||||
@echo "Running $@"
|
||||
@echo "Running $@:"
|
||||
@${GOPATH}/bin/gocyclo -over 100 cmd
|
||||
@${GOPATH}/bin/gocyclo -over 100 pkg
|
||||
|
||||
build: getdeps verifiers $(UI_ASSETS)
|
||||
|
||||
deadcode:
|
||||
@echo "Running $@"
|
||||
@${GOPATH}/bin/deadcode -test $(shell go list ./...) || true
|
||||
@${GOPATH}/bin/deadcode
|
||||
|
||||
spelling:
|
||||
@${GOPATH}/bin/misspell -error `find cmd/`
|
||||
@${GOPATH}/bin/misspell -error `find pkg/`
|
||||
@${GOPATH}/bin/misspell -error `find docs/`
|
||||
|
||||
# Builds minio, runs the verifiers then runs the tests.
|
||||
check: test
|
||||
test: verifiers build
|
||||
@echo "Running unit tests"
|
||||
@go test $(GOFLAGS) ./...
|
||||
@echo "Verifying build"
|
||||
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
||||
test: build
|
||||
@echo "Running all minio testing:"
|
||||
@go test $(GOFLAGS) .
|
||||
@go test $(GOFLAGS) github.com/minio/minio/cmd...
|
||||
@go test $(GOFLAGS) github.com/minio/minio/pkg...
|
||||
|
||||
coverage: build
|
||||
@echo "Running all coverage for minio"
|
||||
@(env bash $(PWD)/buildscripts/go-coverage.sh)
|
||||
@echo "Running all coverage for minio:"
|
||||
@./buildscripts/go-coverage.sh
|
||||
|
||||
# Builds minio locally.
|
||||
build: checks
|
||||
@echo "Building minio binary to './minio'"
|
||||
@CGO_ENABLED=0 go build --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
|
||||
gomake-all: build
|
||||
@echo "Installing minio:"
|
||||
@go build --ldflags $(BUILD_LDFLAGS) -o $(GOPATH)/bin/minio
|
||||
|
||||
pkg-add:
|
||||
@echo "Adding new package $(PKG)"
|
||||
@${GOPATH}/bin/govendor add $(PKG)
|
||||
${GOPATH}/bin/govendor add $(PKG)
|
||||
|
||||
pkg-update:
|
||||
@echo "Updating new package $(PKG)"
|
||||
@${GOPATH}/bin/govendor update $(PKG)
|
||||
${GOPATH}/bin/govendor update $(PKG)
|
||||
|
||||
pkg-remove:
|
||||
@echo "Remove new package $(PKG)"
|
||||
@${GOPATH}/bin/govendor remove $(PKG)
|
||||
${GOPATH}/bin/govendor remove $(PKG)
|
||||
|
||||
pkg-list:
|
||||
@$(GOPATH)/bin/govendor list
|
||||
|
||||
# Builds minio and installs it to $GOPATH/bin.
|
||||
install: build
|
||||
@echo "Installing minio binary to '$(GOPATH)/bin/minio'"
|
||||
@cp $(PWD)/minio $(GOPATH)/bin/minio
|
||||
@echo "Installation successful. To learn more, try \"minio --help\"."
|
||||
install: gomake-all
|
||||
|
||||
release: verifiers
|
||||
@MINIO_RELEASE=RELEASE ./buildscripts/build.sh
|
||||
|
||||
experimental: verifiers
|
||||
@MINIO_RELEASE=EXPERIMENTAL ./buildscripts/build.sh
|
||||
|
||||
clean:
|
||||
@echo "Cleaning up all the generated files"
|
||||
@echo "Cleaning up all the generated files:"
|
||||
@find . -name '*.test' | xargs rm -fv
|
||||
@rm -rvf minio
|
||||
@rm -rvf build
|
||||
@rm -rvf release
|
||||
@rm -rf build
|
||||
@rm -rf release
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# 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)
|
||||
|
||||
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.
|
||||
|
||||
@@ -9,87 +8,78 @@ 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 /data
|
||||
docker run -p 9000:9000 minio/minio server /export
|
||||
```
|
||||
|
||||
### Edge
|
||||
```
|
||||
docker pull minio/minio:edge
|
||||
docker run -p 9000:9000 minio/minio:edge server /data
|
||||
docker run -p 9000:9000 minio/minio:edge server /export
|
||||
```
|
||||
Please visit Minio Docker quickstart guide for more [here](https://docs.minio.io/docs/minio-docker-quickstart-guide)
|
||||
|
||||
## macOS
|
||||
## OS X
|
||||
### Homebrew
|
||||
Install minio packages using [Homebrew](http://brew.sh/)
|
||||
```sh
|
||||
brew install minio/stable/minio
|
||||
minio server /data
|
||||
```
|
||||
|
||||
> NOTE: If you previously installed minio using `brew install minio` then it is recommended that you reinstall minio from `minio/stable/minio` official repo instead.
|
||||
```sh
|
||||
brew uninstall minio
|
||||
brew install minio/stable/minio
|
||||
brew install minio
|
||||
minio server ~/Photos
|
||||
```
|
||||
|
||||
### Binary Download
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|Apple macOS|64-bit Intel|https://dl.minio.io/server/minio/release/darwin-amd64/minio |
|
||||
|Apple OS X|64-bit Intel|https://dl.minio.io/server/minio/release/darwin-amd64/minio|
|
||||
```sh
|
||||
chmod 755 minio
|
||||
./minio server /data
|
||||
./minio server ~/Photos
|
||||
```
|
||||
|
||||
## GNU/Linux
|
||||
### Binary Download
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio |
|
||||
|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
|
||||
wget https://dl.minio.io/server/minio/release/linux-amd64/minio
|
||||
chmod +x minio
|
||||
./minio server /data
|
||||
```
|
||||
|
||||
### Snap
|
||||
Install minio using [Snap](https://snapcraft.io)
|
||||
```sh
|
||||
sudo snap install minio --edge
|
||||
```
|
||||
|
||||
Start minio using `snap run` command
|
||||
```sh
|
||||
sudo snap connect minio:mount-observe
|
||||
sudo snap run minio server /data
|
||||
./minio server ~/Photos
|
||||
```
|
||||
|
||||
## Microsoft Windows
|
||||
### Binary Download
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|Microsoft Windows|64-bit|https://dl.minio.io/server/minio/release/windows-amd64/minio.exe |
|
||||
|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
|
||||
```
|
||||
|
||||
## FreeBSD
|
||||
### Port
|
||||
Install minio packages using [pkg](https://github.com/freebsd/pkg)
|
||||
|
||||
### Binary Download
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|FreeBSD|64-bit|https://dl.minio.io/server/minio/release/freebsd-amd64/minio|
|
||||
```sh
|
||||
pkg install minio
|
||||
sysrc minio_enable=yes
|
||||
sysrc minio_disks=/home/user/Photos
|
||||
service minio start
|
||||
chmod 755 minio
|
||||
./minio server ~/Photos
|
||||
```
|
||||
|
||||
You can run Minio on FreeBSD with FreeNAS storage-backend - see [here](https://docs.minio.io/docs/how-to-run-minio-in-freenas) for more details.
|
||||
|
||||
## Install from Source
|
||||
|
||||
Source installation is only intended for developers and advanced users. If you do not have a working Golang environment, please follow [How to install Golang](https://docs.minio.io/docs/how-to-install-golang).
|
||||
|
||||
```sh
|
||||
|
||||
go get -u github.com/minio/minio
|
||||
|
||||
```
|
||||
|
||||
## Test using Minio Browser
|
||||
@@ -100,11 +90,6 @@ Minio Server comes with an embedded web based object browser. Point your web bro
|
||||
## Test using Minio Client `mc`
|
||||
`mc` provides a modern alternative to UNIX commands like ls, cat, cp, mirror, diff etc. It supports filesystems and Amazon S3 compatible cloud storage services. Follow the Minio Client [Quickstart Guide](https://docs.minio.io/docs/minio-client-quickstart-guide) for further instructions.
|
||||
|
||||
## Pre-existing data
|
||||
When deployed on a single drive, Minio server lets clients access any pre-existing data in the data directory. For example, if Minio is started with the command `minio server /mnt/data`, any pre-existing data in the `/mnt/data` directory would be accessible to the clients.
|
||||
|
||||
The above statement is also valid for all gateway backends.
|
||||
|
||||
## Explore Further
|
||||
- [Minio Erasure Code QuickStart Guide](https://docs.minio.io/docs/minio-erasure-code-quickstart-guide)
|
||||
- [Use `mc` with Minio Server](https://docs.minio.io/docs/minio-client-quickstart-guide)
|
||||
@@ -115,7 +100,3 @@ The above statement is also valid for all gateway backends.
|
||||
|
||||
## Contribute to Minio Project
|
||||
Please follow Minio [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
|
||||
|
||||
|
||||
## License
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fminio%2Fminio?ref=badge_large)
|
||||
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
# Minio 快速入门 [](https://slack.minio.io) [](https://goreportcard.com/report/minio/minio) [](https://codecov.io/gh/minio/minio)
|
||||
|
||||
Minio是一个对象存储服务,基于Apache License v2.0协议. 它完全兼容亚马逊的S3云储存服务,非常适合于存储很多非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
|
||||
|
||||
## 1. 下载
|
||||
|
||||
Minio是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS, Redis 或者 MySQL.
|
||||
|
||||
| Platform| Architecture | URL|
|
||||
| ----------| -------- | ------|
|
||||
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio|
|
||||
||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 安装
|
||||
|
||||
使用[Homebrew](http://brew.sh/) 来安装minio
|
||||
|
||||
```sh
|
||||
$ brew install minio
|
||||
$ minio --help
|
||||
```
|
||||
|
||||
### 源码安装
|
||||
|
||||
源码安装只针对开发者和一些高级用户,如果你还没有golang的环境,请安装golang官网安装[How to install Golang](https://docs.minio.io/docs/zh-CN/how-to-install-golang).
|
||||
|
||||
```sh
|
||||
|
||||
$ go get -u github.com/minio/minio
|
||||
|
||||
|
||||
```
|
||||
|
||||
## 2. 运行Minio服务
|
||||
|
||||
|
||||
### GNU/Linux
|
||||
|
||||
```sh
|
||||
|
||||
$ chmod +x minio
|
||||
$ ./minio --help
|
||||
$ ./minio server ~/Photos
|
||||
|
||||
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
区域: us-east-1
|
||||
|
||||
浏览器访问入口:
|
||||
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
|
||||
命令行访问: https://docs.minio.io/docs/zh-CN/minio-client-quickstart-guide
|
||||
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
|
||||
对象操作API (兼容Amazon S3):
|
||||
Go: https://docs.minio.io/docs/zh-CN/golang-client-quickstart-guide
|
||||
Java: https://docs.minio.io/docs/zh-CN/java-client-quickstart-guide
|
||||
Python: https://docs.minio.io/docs/zh-CN/python-client-quickstart-guide
|
||||
JavaScript: https://docs.minio.io/docs/zh-CN/javascript-client-quickstart-guide
|
||||
|
||||
```
|
||||
|
||||
### OS X
|
||||
|
||||
|
||||
```sh
|
||||
|
||||
$ chmod 755 minio
|
||||
$ ./minio --help
|
||||
$ ./minio server ~/Photos
|
||||
|
||||
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
区域: us-east-1
|
||||
|
||||
浏览器访问入口:
|
||||
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
|
||||
命令行访问: https://docs.minio.io/docs/zh-CN/minio-client-quickstart-guide
|
||||
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
|
||||
对象操作API (兼容Amazon S3):
|
||||
Go: https://docs.minio.io/docs/zh-CN/golang-client-quickstart-guide
|
||||
Java: https://docs.minio.io/docs/zh-CN/java-client-quickstart-guide
|
||||
Python: https://docs.minio.io/docs/zh-CN/python-client-quickstart-guide
|
||||
JavaScript: https://docs.minio.io/docs/zh-CN/javascript-client-quickstart-guide
|
||||
|
||||
```
|
||||
|
||||
### Microsoft Windows
|
||||
|
||||
```sh
|
||||
|
||||
C:\Users\Username\Downloads> minio.exe --help
|
||||
C:\Users\Username\Downloads> minio.exe server D:\Photos
|
||||
|
||||
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
区域: us-east-1
|
||||
|
||||
浏览器访问入口:
|
||||
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
|
||||
命令行访问: https://docs.minio.io/docs/zh-CN/minio-client-quickstart-guide
|
||||
$ mc.exe config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
|
||||
对象操作API (兼容Amazon S3):
|
||||
Go: https://docs.minio.io/docs/zh-CN/golang-client-quickstart-guide
|
||||
Java: https://docs.minio.io/docs/zh-CN/java-client-quickstart-guide
|
||||
Python: https://docs.minio.io/docs/zh-CN/python-client-quickstart-guide
|
||||
JavaScript: https://docs.minio.io/docs/zh-CN/javascript-client-quickstart-guide
|
||||
|
||||
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```sh
|
||||
|
||||
$ docker pull minio/minio
|
||||
$ docker run -p 9000:9000 minio/minio
|
||||
|
||||
```
|
||||
访问minio的docker入门指南获得更多内容 [here](https://docs.minio.io/docs/zh-CN/minio-docker-quickstart-guide)
|
||||
|
||||
### FreeBSD
|
||||
|
||||
```sh
|
||||
|
||||
$ chmod 755 minio
|
||||
$ ./minio --help
|
||||
$ ./minio server ~/Photos
|
||||
|
||||
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
区域: us-east-1
|
||||
|
||||
浏览器访问入口:
|
||||
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||
|
||||
命令行访问: https://docs.minio.io/docs/zh-CN/minio-client-quickstart-guide
|
||||
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||
|
||||
对象操作API (兼容Amazon S3):
|
||||
Go: https://docs.minio.io/docs/zh-CN/golang-client-quickstart-guide
|
||||
Java: https://docs.minio.io/docs/zh-CN/java-client-quickstart-guide
|
||||
Python: https://docs.minio.io/docs/zh-CN/python-client-quickstart-guide
|
||||
JavaScript: https://docs.minio.io/docs/zh-CN/javascript-client-quickstart-guide
|
||||
|
||||
|
||||
```
|
||||
请访问FreeBSD的官网指南获取更多详细信息[here](https://www.freebsd.org/doc/handbook/zfs-quickstart.html)
|
||||
|
||||
## 3. 使用浏览器测试minio服务
|
||||
|
||||
打开浏览器并输入 http://127.0.0.1:9000 查看在minio服务器上面的所有bucket
|
||||
|
||||

|
||||
|
||||
|
||||
## 4. 使用`mc`测试minio服务
|
||||
|
||||
|
||||
按照 [这个](https://docs.minio.io/docs/minio-client-quickstart-guide) 安装mc. 使用 `mc ls` 命令显示所有在minio服务上面的bucket.
|
||||
|
||||
```sh
|
||||
|
||||
$ mc ls myminio/
|
||||
[2015-08-05 08:13:22 IST] 0B andoria/
|
||||
[2015-08-05 06:14:26 IST] 0B deflector/
|
||||
[2015-08-05 08:13:11 IST] 0B ferenginar/
|
||||
[2016-03-08 14:56:35 IST] 0B jarjarbing/
|
||||
[2016-01-20 16:07:41 IST] 0B my.minio.io/
|
||||
|
||||
```
|
||||
|
||||
查看更多的例子请访问 [Minio Client Complete Guide](https://docs.minio.io/docs/zh-CN/minio-client-complete-guide).
|
||||
|
||||
|
||||
## 5. 更多内容
|
||||
|
||||
- [Minio Erasure Code 快速入门](https://docs.minio.io/docs/zh-CN/minio-erasure-code-quickstart-guide)
|
||||
- [Minio Docker 快速入门](https://docs.minio.io/docs/zh-CN/minio-docker-quickstart-guide)
|
||||
- [使用`mc`测试 Minio Server](https://docs.minio.io/docs/zh-CN/minio-client-quickstart-guide)
|
||||
- [使用 `aws-cli` 测试 Minio Server](https://docs.minio.io/docs/zh-CN/aws-cli-with-minio)
|
||||
- [使用 `s3cmd` 测试 Minio Server](https://docs.minio.io/docs/zh-CN/s3cmd-with-minio)
|
||||
- [使用 `minio-go` SDK ce's测试 Minio Server](https://docs.minio.io/docs/zh-CN/golang-client-quickstart-guide)
|
||||
|
||||
|
||||
## 6. 给Minio项目贡献
|
||||
请按照Minio [贡献者指导手册](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
|
||||
-121
@@ -1,121 +0,0 @@
|
||||
# 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 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
|
||||
|
||||
Minio是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS, Redis 或者 MySQL。
|
||||
|
||||
## Docker 容器
|
||||
### 稳定版
|
||||
```
|
||||
docker pull minio/minio
|
||||
docker run -p 9000:9000 minio/minio server /data
|
||||
```
|
||||
|
||||
### 尝鲜版
|
||||
```
|
||||
docker pull minio/minio:edge
|
||||
docker run -p 9000:9000 minio/minio:edge server /data
|
||||
```
|
||||
更多Docker部署信息请访问 [这里](https://docs.minio.io/docs/minio-docker-quickstart-guide)
|
||||
|
||||
## macOS
|
||||
### Homebrew
|
||||
使用 [Homebrew](http://brew.sh/)安装minio
|
||||
|
||||
```sh
|
||||
brew install minio/stable/minio
|
||||
minio server /data
|
||||
```
|
||||
#### Note
|
||||
如果你之前使用 `brew install minio`安装过minio, 可以用 `minio/stable/minio` 官方镜像进行重装. 由于golang 1.8的bug,homebrew版本不太稳定。
|
||||
|
||||
```
|
||||
brew uninstall minio
|
||||
brew install minio/stable/minio
|
||||
```
|
||||
|
||||
### 下载二进制文件
|
||||
| 操作系统| CPU架构 | 地址|
|
||||
| ----------| -------- | ------|
|
||||
|Apple macOS|64-bit Intel|https://dl.minio.io/server/minio/release/darwin-amd64/minio |
|
||||
```sh
|
||||
chmod 755 minio
|
||||
./minio server /data
|
||||
```
|
||||
|
||||
## GNU/Linux
|
||||
### 下载二进制文件
|
||||
| 操作系统| CPU架构 | 地址|
|
||||
| ----------| -------- | ------|
|
||||
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio |
|
||||
```sh
|
||||
chmod +x minio
|
||||
./minio server /data
|
||||
```
|
||||
|
||||
### 快照版
|
||||
你可以下载最新版 `minio` [快照](https://snapcraft.io), 并且帮助我们一起验证master分支上[所有支持的Linux发行版](https://snapcraft.io/docs/core/install) 的一些最新修改:
|
||||
|
||||
```sh
|
||||
sudo snap install minio --edge
|
||||
```
|
||||
|
||||
每次有最新的 `minio` 被推送到服务器,你都会自动更新下来.
|
||||
|
||||
你需要允许minio snap来观察其安装:
|
||||
|
||||
```sh
|
||||
sudo snap connect minio:mount-observe
|
||||
```
|
||||
|
||||
## 微软Windows系统
|
||||
### 下载二进制文件
|
||||
| 操作系统| CPU架构 | 地址|
|
||||
| ----------| -------- | ------|
|
||||
|微软Windows系统|64位|https://dl.minio.io/server/minio/release/windows-amd64/minio.exe |
|
||||
```sh
|
||||
minio.exe server D:\Photos
|
||||
```
|
||||
|
||||
## FreeBSD
|
||||
### Port
|
||||
使用 [pkg](https://github.com/freebsd/pkg)进行安装。
|
||||
|
||||
```sh
|
||||
pkg install minio
|
||||
sysrc minio_enable=yes
|
||||
sysrc minio_disks=/home/user/Photos
|
||||
service minio start
|
||||
```
|
||||
|
||||
## 使用源码安装
|
||||
|
||||
采用源码安装仅供开发人员和高级用户使用,如果你还没有Golang环境, 请参考 [How to install Golang](https://docs.minio.io/docs/how-to-install-golang).
|
||||
|
||||
```sh
|
||||
go get -u github.com/minio/minio
|
||||
```
|
||||
|
||||
## 使用Minio浏览器进行验证
|
||||
安装后使用浏览器访问[http://127.0.0.1:9000](http://127.0.0.1:9000),如果可以访问,则表示minio已经安装成功。
|
||||
|
||||

|
||||
|
||||
## 使用Minio客户端 `mc`进行验证
|
||||
`mc` 提供了一些UNIX常用命令的替代品,像ls, cat, cp, mirror, diff这些。 它支持文件系统和亚马逊S3云存储服务。 更多信息请参考 [mc快速入门](https://docs.minio.io/docs/minio-client-quickstart-guide) 。
|
||||
|
||||
## 已经存在的数据
|
||||
当在单块磁盘上部署Minio server,Minio server允许客户端访问数据目录下已经存在的数据。比如,如果Minio使用`minio server /mnt/data`启动,那么所有已经在`/mnt/data`目录下的数据都可以被客户端访问到。
|
||||
|
||||
上述描述对所有网关后端同样有效。
|
||||
|
||||
## 了解更多
|
||||
- [Minio纠删码入门](https://docs.minio.io/docs/minio-erasure-code-quickstart-guide)
|
||||
- [`mc`快速入门](https://docs.minio.io/docs/minio-client-quickstart-guide)
|
||||
- [使用 `aws-cli`](https://docs.minio.io/docs/aws-cli-with-minio)
|
||||
- [使用 `s3cmd`](https://docs.minio.io/docs/s3cmd-with-minio)
|
||||
- [使用 `minio-go` SDK](https://docs.minio.io/docs/golang-client-quickstart-guide)
|
||||
- [Minio文档](https://docs.minio.io)
|
||||
|
||||
## 如何参与到Minio项目
|
||||
请参考 [贡献者指南](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)。欢迎各位中国程序员加到Minio项目中。
|
||||
+4
-6
@@ -12,11 +12,10 @@ clone_folder: c:\gopath\src\github.com\minio\minio
|
||||
# Environment variables
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
GOROOT: c:\go19
|
||||
|
||||
# scripts that run after cloning repository
|
||||
install:
|
||||
- set PATH=%GOPATH%\bin;%GOROOT%\bin;%PATH%
|
||||
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
|
||||
- go version
|
||||
- go env
|
||||
- python --version
|
||||
@@ -24,8 +23,6 @@ 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,8 +35,9 @@ test_script:
|
||||
# Unit tests
|
||||
- ps: Add-AppveyorTest "Unit Tests" -Outcome Running
|
||||
- mkdir build\coverage
|
||||
- for /f "" %%G in ('go list github.com/minio/minio/... ^| find /i /v "browser/"') do ( go test -v -timeout 20m -race %%G )
|
||||
- go test -v -timeout 20m -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd
|
||||
- go test -timeout 20m -v -race github.com/minio/minio/cmd...
|
||||
- go test -v -race github.com/minio/minio/pkg...
|
||||
- go test -timeout 15m -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd
|
||||
- ps: Update-AppveyorTest "Unit Tests" -Outcome Passed
|
||||
|
||||
after_test:
|
||||
|
||||
+7
-8
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread"
|
||||
]
|
||||
}
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react"
|
||||
],
|
||||
|
||||
"plugins": ["transform-object-rest-spread"]
|
||||
}
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ go get github.com/elazarl/go-bindata-assetfs/...
|
||||
yarn release
|
||||
```
|
||||
|
||||
This generates ui-assets.go in the current directory. Now do `make` in the parent directory to build the minio binary with the newly generated ``ui-assets.go``
|
||||
This generates ui-assets.go in the current direcotry. Now do `make` in the parent directory to build the minio binary with the newly generated ``ui-assets.go``
|
||||
|
||||
### Run Minio Browser with live reload
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Minio Browser</title>
|
||||
<link rel="stylesheet" href="loader.css" type="text/css">
|
||||
<link rel="stylesheet" href="/minio/loader.css" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page-load">
|
||||
<div class="pl-inner">
|
||||
<img src="logo.svg" alt="">
|
||||
<img src="/minio/logo.svg" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div id="root"></div>
|
||||
@@ -27,19 +27,19 @@
|
||||
<ul>
|
||||
<li>
|
||||
<a href="http://www.google.com/chrome/">
|
||||
<img src="chrome.png" alt="">
|
||||
<img src="/minio/chrome.png" alt="">
|
||||
<div>Chrome</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.mozilla.org/en-US/firefox/new/">
|
||||
<img src="firefox.png" alt="">
|
||||
<img src="/minio/firefox.png" alt="">
|
||||
<div>Firefox</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.apple.com/safari/">
|
||||
<img src="safari.png" alt="">
|
||||
<img src="/minio/safari.png" alt="">
|
||||
<div>Safari</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -51,6 +51,6 @@
|
||||
<![endif]-->
|
||||
|
||||
<script>currentUiVersion = 'MINIO_UI_VERSION'</script>
|
||||
<script src="index_bundle.js"></script>
|
||||
<script src="/minio/index_bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import 'babel-polyfill'
|
||||
|
||||
import './less/main.less'
|
||||
|
||||
@@ -41,12 +40,11 @@ import _Login from './js/components/Login.js'
|
||||
import _Browse from './js/components/Browse.js'
|
||||
import fontAwesome from 'font-awesome/css/font-awesome.css'
|
||||
|
||||
import MaterialDesignIconicFonts from 'material-design-iconic-font/dist/css/material-design-iconic-font.min.css'
|
||||
|
||||
import Web from './js/web'
|
||||
window.Web = Web
|
||||
|
||||
import storage from 'local-storage-fallback'
|
||||
|
||||
const store = applyMiddleware(thunkMiddleware)(createStore)(reducer)
|
||||
const Browse = connect(state => state)(_Browse)
|
||||
const Login = connect(state => state)(_Login)
|
||||
@@ -83,7 +81,7 @@ ReactDOM.render((
|
||||
<Provider store={ store } web={ web }>
|
||||
<Router history={ browserHistory }>
|
||||
<Route path='/' component={ App }>
|
||||
<Route path={ minioBrowserPrefix } component={ App }>
|
||||
<Route path='minio' component={ App }>
|
||||
<IndexRoute component={ Browse } onEnter={ authNeeded } />
|
||||
<Route path='login' component={ Login } onEnter={ authNotNeeded } />
|
||||
<Route path=':bucket' component={ Browse } onEnter={ authNeeded } />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
+28
-158
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,9 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import url from 'url'
|
||||
import Moment from 'moment'
|
||||
import browserHistory from 'react-router/lib/browserHistory'
|
||||
import web from './web'
|
||||
import * as utils from './utils'
|
||||
import storage from 'local-storage-fallback'
|
||||
|
||||
import { minioBrowserPrefix } from './constants'
|
||||
|
||||
export const SET_WEB = 'SET_WEB'
|
||||
@@ -24,12 +28,9 @@ export const SET_CURRENT_BUCKET = 'SET_CURRENT_BUCKET'
|
||||
export const SET_CURRENT_PATH = 'SET_CURRENT_PATH'
|
||||
export const SET_BUCKETS = 'SET_BUCKETS'
|
||||
export const ADD_BUCKET = 'ADD_BUCKET'
|
||||
export const REMOVE_BUCKET = 'REMOVE_BUCKET'
|
||||
export const SHOW_BUCKET_DROPDOWN = 'SHOW_BUCKET_DROPDOWN'
|
||||
export const ADD_OBJECT = 'ADD_OBJECT'
|
||||
export const SET_VISIBLE_BUCKETS = 'SET_VISIBLE_BUCKETS'
|
||||
export const SET_OBJECTS = 'SET_OBJECTS'
|
||||
export const APPEND_OBJECTS = 'APPEND_OBJECTS'
|
||||
export const RESET_OBJECTS = 'RESET_OBJECTS'
|
||||
export const SET_STORAGE_INFO = 'SET_STORAGE_INFO'
|
||||
export const SET_SERVER_INFO = 'SET_SERVER_INFO'
|
||||
export const SHOW_MAKEBUCKET_MODAL = 'SHOW_MAKEBUCKET_MODAL'
|
||||
@@ -56,9 +57,6 @@ export const SET_SHARE_OBJECT = 'SET_SHARE_OBJECT'
|
||||
export const DELETE_CONFIRMATION = 'DELETE_CONFIRMATION'
|
||||
export const SET_PREFIX_WRITABLE = 'SET_PREFIX_WRITABLE'
|
||||
export const REMOVE_OBJECT = 'REMOVE_OBJECT'
|
||||
export const CHECKED_OBJECTS_ADD = 'CHECKED_OBJECTS_ADD'
|
||||
export const CHECKED_OBJECTS_REMOVE = 'CHECKED_OBJECTS_REMOVE'
|
||||
export const CHECKED_OBJECTS_RESET = 'CHECKED_OBJECTS_RESET'
|
||||
|
||||
export const showDeleteConfirmation = (object) => {
|
||||
return {
|
||||
@@ -80,12 +78,11 @@ export const hideDeleteConfirmation = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const showShareObject = (object, url) => {
|
||||
export const showShareObject = url => {
|
||||
return {
|
||||
type: SET_SHARE_OBJECT,
|
||||
shareObject: {
|
||||
object,
|
||||
url,
|
||||
url: url,
|
||||
show: true
|
||||
}
|
||||
}
|
||||
@@ -101,17 +98,15 @@ export const hideShareObject = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const shareObject = (object, days, hours, minutes) => (dispatch, getState) => {
|
||||
export const shareObject = (object, expiry) => (dispatch, getState) => {
|
||||
const {currentBucket, web} = getState()
|
||||
let host = location.host
|
||||
let bucket = currentBucket
|
||||
|
||||
if (!web.LoggedIn()) {
|
||||
dispatch(showShareObject(object, `${host}/${bucket}/${object}`))
|
||||
dispatch(showShareObject(`${host}/${bucket}/${object}`))
|
||||
return
|
||||
}
|
||||
|
||||
let expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60
|
||||
web.PresignedGet({
|
||||
host,
|
||||
bucket,
|
||||
@@ -119,11 +114,7 @@ export const shareObject = (object, days, hours, minutes) => (dispatch, getState
|
||||
expiry
|
||||
})
|
||||
.then(obj => {
|
||||
dispatch(showShareObject(object, obj.url))
|
||||
dispatch(showAlert({
|
||||
type: 'success',
|
||||
message: `Object shared. Expires in ${days} days ${hours} hours ${minutes} minutes.`
|
||||
}))
|
||||
dispatch(showShareObject(obj.url))
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(showAlert({
|
||||
@@ -175,27 +166,6 @@ export const addBucket = bucket => {
|
||||
}
|
||||
}
|
||||
|
||||
export const removeBucket = bucket => {
|
||||
return {
|
||||
type: REMOVE_BUCKET,
|
||||
bucket
|
||||
}
|
||||
}
|
||||
|
||||
export const showBucketDropdown = bucket => {
|
||||
return {
|
||||
type: SHOW_BUCKET_DROPDOWN,
|
||||
showBucketDropdown: true
|
||||
}
|
||||
}
|
||||
|
||||
export const hideBucketDropdown = bucket => {
|
||||
return {
|
||||
type: SHOW_BUCKET_DROPDOWN,
|
||||
showBucketDropdown: false
|
||||
}
|
||||
}
|
||||
|
||||
export const showMakeBucketModal = () => {
|
||||
return {
|
||||
type: SHOW_MAKEBUCKET_MODAL,
|
||||
@@ -265,25 +235,12 @@ export const setVisibleBuckets = visibleBuckets => {
|
||||
}
|
||||
}
|
||||
|
||||
const appendObjects = (objects, marker, istruncated) => {
|
||||
return {
|
||||
type: APPEND_OBJECTS,
|
||||
objects,
|
||||
marker,
|
||||
istruncated
|
||||
}
|
||||
}
|
||||
|
||||
export const setObjects = (objects) => {
|
||||
export const setObjects = (objects, marker, istruncated) => {
|
||||
return {
|
||||
type: SET_OBJECTS,
|
||||
objects,
|
||||
}
|
||||
}
|
||||
|
||||
export const resetObjects = () => {
|
||||
return {
|
||||
type: RESET_OBJECTS
|
||||
marker,
|
||||
istruncated
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,49 +294,24 @@ export const selectBucket = (newCurrentBucket, prefix) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteBucket = (bucket) => {
|
||||
return (dispatch, getState) => {
|
||||
// DeleteBucket() RPC call will ONLY delete a bucket if it is empty of
|
||||
// objects. This means a call can just be sent, as it is entirely reversable
|
||||
// and won't do any permanent damage.
|
||||
web.DeleteBucket({
|
||||
bucketName: bucket
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(showAlert({
|
||||
type: 'info',
|
||||
message: `Bucket '${bucket}' has been deleted.`
|
||||
}))
|
||||
dispatch(removeBucket(bucket))
|
||||
})
|
||||
.catch(err => {
|
||||
let message = err.message
|
||||
dispatch(showAlert({
|
||||
type: 'danger',
|
||||
message: message
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const listObjects = () => {
|
||||
return (dispatch, getState) => {
|
||||
const {buckets, currentBucket, currentPath, marker, objects, istruncated, web} = getState()
|
||||
if (!istruncated || buckets.length === 0) return
|
||||
const {currentBucket, currentPath, marker, objects, istruncated, web} = getState()
|
||||
if (!istruncated) return
|
||||
web.ListObjects({
|
||||
bucketName: currentBucket,
|
||||
prefix: currentPath,
|
||||
marker: marker
|
||||
})
|
||||
.then(res => {
|
||||
let objects = res.objects
|
||||
let objects = res.objects
|
||||
if (!objects)
|
||||
objects = []
|
||||
objects = objects.map(object => {
|
||||
object.name = object.name.replace(`${currentPath}`, '');
|
||||
return object
|
||||
})
|
||||
dispatch(appendObjects(objects, res.nextmarker, res.istruncated))
|
||||
object.name = object.name.replace(`${currentPath}`, '');
|
||||
return object
|
||||
})
|
||||
dispatch(setObjects(objects, res.nextmarker, res.istruncated))
|
||||
dispatch(setPrefixWritable(res.writable))
|
||||
dispatch(setLoadBucket(''))
|
||||
dispatch(setLoadPath(''))
|
||||
@@ -400,6 +332,7 @@ export const listObjects = () => {
|
||||
export const selectPrefix = prefix => {
|
||||
return (dispatch, getState) => {
|
||||
const {currentBucket, web} = getState()
|
||||
dispatch(setObjects([], "", false))
|
||||
dispatch(setLoadPath(prefix))
|
||||
web.ListObjects({
|
||||
bucketName: currentBucket,
|
||||
@@ -411,11 +344,10 @@ export const selectPrefix = prefix => {
|
||||
if (!objects)
|
||||
objects = []
|
||||
objects = objects.map(object => {
|
||||
object.name = object.name.replace(`${prefix}`, '');
|
||||
return object
|
||||
})
|
||||
dispatch(resetObjects())
|
||||
dispatch(appendObjects(
|
||||
object.name = object.name.replace(`${prefix}`, '');
|
||||
return object
|
||||
})
|
||||
dispatch(setObjects(
|
||||
objects,
|
||||
res.nextmarker,
|
||||
res.istruncated
|
||||
@@ -478,42 +410,11 @@ export const setLoginError = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const downloadSelected = (url, req, xhr) => {
|
||||
return (dispatch) => {
|
||||
var anchor = document.createElement('a')
|
||||
document.body.appendChild(anchor);
|
||||
xhr.open('POST', url, true)
|
||||
xhr.responseType = 'blob'
|
||||
|
||||
xhr.onload = function(e) {
|
||||
if (this.status == 200) {
|
||||
dispatch(checkedObjectsReset())
|
||||
var blob = new Blob([this.response], {
|
||||
type: 'octet/stream'
|
||||
})
|
||||
var blobUrl = window.URL.createObjectURL(blob);
|
||||
var separator = req.prefix.length > 1 ? '-' : ''
|
||||
|
||||
anchor.href = blobUrl
|
||||
anchor.download = req.bucketName+separator+req.prefix.slice(0, -1)+'.zip';
|
||||
|
||||
|
||||
|
||||
|
||||
anchor.click()
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
anchor.remove()
|
||||
}
|
||||
};
|
||||
xhr.send(JSON.stringify(req));
|
||||
}
|
||||
}
|
||||
|
||||
export const uploadFile = (file, xhr) => {
|
||||
return (dispatch, getState) => {
|
||||
const {currentBucket, currentPath} = getState()
|
||||
const objectName = `${currentPath}${file.name}`
|
||||
const uploadUrl = `${window.location.origin}${minioBrowserPrefix}/upload/${currentBucket}/${objectName}`
|
||||
const uploadUrl = `${window.location.origin}/minio/upload/${currentBucket}/${objectName}`
|
||||
// The slug is a unique identifer for the file upload.
|
||||
const slug = `${currentBucket}-${currentPath}-${file.name}`
|
||||
|
||||
@@ -530,7 +431,7 @@ export const uploadFile = (file, xhr) => {
|
||||
}))
|
||||
|
||||
xhr.onload = function(event) {
|
||||
if (xhr.status == 401 || xhr.status == 403) {
|
||||
if (xhr.status == 401 || xhr.status == 403 || xhr.status == 500) {
|
||||
setShowAbortModal(false)
|
||||
dispatch(stopUpload({
|
||||
slug
|
||||
@@ -540,16 +441,6 @@ 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({
|
||||
@@ -672,24 +563,3 @@ export const setPolicies = (policies) => {
|
||||
policies
|
||||
}
|
||||
}
|
||||
|
||||
export const checkedObjectsAdd = (objectName) => {
|
||||
return {
|
||||
type: CHECKED_OBJECTS_ADD,
|
||||
objectName
|
||||
}
|
||||
}
|
||||
|
||||
export const checkedObjectsRemove = (objectName) => {
|
||||
return {
|
||||
type: CHECKED_OBJECTS_REMOVE,
|
||||
objectName
|
||||
}
|
||||
}
|
||||
|
||||
export const checkedObjectsReset = (objectName) => {
|
||||
return {
|
||||
type: CHECKED_OBJECTS_RESET,
|
||||
objectName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,6 +27,7 @@ import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'
|
||||
import Tooltip from 'react-bootstrap/lib/Tooltip'
|
||||
import Dropdown from 'react-bootstrap/lib/Dropdown'
|
||||
import MenuItem from 'react-bootstrap/lib/MenuItem'
|
||||
|
||||
import InputGroup from '../components/InputGroup'
|
||||
import Dropzone from '../components/Dropzone'
|
||||
import ObjectsList from '../components/ObjectsList'
|
||||
@@ -68,7 +69,7 @@ export default class Browse extends React.Component {
|
||||
memory: res.MinioMemory,
|
||||
platform: res.MinioPlatform,
|
||||
runtime: res.MinioRuntime,
|
||||
info: res.MinioGlobalInfo
|
||||
envVars: res.MinioEnvVars
|
||||
})
|
||||
dispatch(actions.setServerInfo(serverInfo))
|
||||
})
|
||||
@@ -150,21 +151,7 @@ export default class Browse extends React.Component {
|
||||
if (prefix === currentPath) return
|
||||
browserHistory.push(utils.pathJoin(currentBucket, encPrefix))
|
||||
} else {
|
||||
if (!web.LoggedIn()) {
|
||||
let url = `${window.location.origin}/minio/download/${currentBucket}/${encPrefix}?token=''`
|
||||
window.location = url
|
||||
} else {
|
||||
// Download the selected file.
|
||||
web.CreateURLToken()
|
||||
.then(res => {
|
||||
let url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encPrefix}?token=${res.token}`
|
||||
window.location = url
|
||||
})
|
||||
.catch(err => dispatch(actions.showAlert({
|
||||
type: 'danger',
|
||||
message: err.message
|
||||
})))
|
||||
}
|
||||
window.location = `${window.location.origin}/minio/download/${currentBucket}/${encPrefix}?token=${storage.getItem('token')}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,19 +197,9 @@ export default class Browse extends React.Component {
|
||||
dispatch(actions.hideAbout())
|
||||
}
|
||||
|
||||
toggleBucketDropdown(e) {
|
||||
const {dispatch, showBucketDropdown} = this.props
|
||||
if (showBucketDropdown) {
|
||||
dispatch(actions.hideBucketDropdown())
|
||||
} else {
|
||||
dispatch(actions.showBucketDropdown())
|
||||
}
|
||||
}
|
||||
|
||||
showBucketPolicy(e) {
|
||||
e.preventDefault()
|
||||
const {dispatch} = this.props
|
||||
this.toggleBucketDropdown(e)
|
||||
dispatch(actions.showBucketPolicy())
|
||||
}
|
||||
|
||||
@@ -232,17 +209,10 @@ export default class Browse extends React.Component {
|
||||
dispatch(actions.hideBucketPolicy())
|
||||
}
|
||||
|
||||
deleteBucket(e, bucket) {
|
||||
e.preventDefault()
|
||||
const {dispatch} = this.props
|
||||
this.toggleBucketDropdown(e)
|
||||
dispatch(actions.deleteBucket(bucket))
|
||||
browserHistory.push(`${minioBrowserPrefix}/`)
|
||||
}
|
||||
|
||||
uploadFile(e) {
|
||||
e.preventDefault()
|
||||
const {dispatch, buckets, currentBucket} = this.props
|
||||
const {dispatch, buckets} = this.props
|
||||
|
||||
if (buckets.length === 0) {
|
||||
dispatch(actions.showAlert({
|
||||
type: 'danger',
|
||||
@@ -250,13 +220,6 @@ export default class Browse extends React.Component {
|
||||
}))
|
||||
return
|
||||
}
|
||||
if (currentBucket === '') {
|
||||
dispatch(actions.showAlert({
|
||||
type: 'danger',
|
||||
message: "Please choose a bucket before trying to upload files."
|
||||
}))
|
||||
return
|
||||
}
|
||||
let file = e.target.files[0]
|
||||
e.target.value = null
|
||||
this.xhr = new XMLHttpRequest()
|
||||
@@ -264,29 +227,14 @@ export default class Browse extends React.Component {
|
||||
}
|
||||
|
||||
removeObject() {
|
||||
const {web, dispatch, currentPath, currentBucket, deleteConfirmation, checkedObjects} = this.props
|
||||
let objects = []
|
||||
if (checkedObjects.length > 0) {
|
||||
objects = checkedObjects.map(obj => `${currentPath}${obj}`)
|
||||
} else {
|
||||
objects = [deleteConfirmation.object]
|
||||
}
|
||||
|
||||
const {web, dispatch, currentPath, currentBucket, deleteConfirmation} = this.props
|
||||
web.RemoveObject({
|
||||
bucketname: currentBucket,
|
||||
objects: objects
|
||||
bucketName: currentBucket,
|
||||
objectName: deleteConfirmation.object
|
||||
})
|
||||
.then(() => {
|
||||
this.hideDeleteConfirmation()
|
||||
if (checkedObjects.length > 0) {
|
||||
for (let i = 0; i < checkedObjects.length; i++) {
|
||||
dispatch(actions.removeObject(checkedObjects[i].replace(currentPath, '')))
|
||||
}
|
||||
dispatch(actions.checkedObjectsReset())
|
||||
} else {
|
||||
let delObject = deleteConfirmation.object.replace(currentPath, '')
|
||||
dispatch(actions.removeObject(delObject))
|
||||
}
|
||||
dispatch(actions.removeObject(deleteConfirmation.object))
|
||||
})
|
||||
.catch(e => dispatch(actions.showAlert({
|
||||
type: 'danger',
|
||||
@@ -314,8 +262,7 @@ export default class Browse extends React.Component {
|
||||
shareObject(e, object) {
|
||||
e.preventDefault()
|
||||
const {dispatch} = this.props
|
||||
// let expiry = 5 * 24 * 60 * 60 // 5 days expiry by default
|
||||
dispatch(actions.shareObject(object, 5, 0, 0))
|
||||
dispatch(actions.shareObject(object))
|
||||
}
|
||||
|
||||
hideShareObjectModal() {
|
||||
@@ -407,75 +354,18 @@ export default class Browse extends React.Component {
|
||||
this.refs.copyTextInput.select()
|
||||
}
|
||||
|
||||
handleExpireValue(targetInput, inc, object) {
|
||||
let value = this.refs[targetInput].value
|
||||
let maxValue = (targetInput == 'expireHours') ? 23 : (targetInput == 'expireMins') ? 59 : (targetInput == 'expireDays') ? 7 : 0
|
||||
value = isNaN(value) ? 0 : value
|
||||
handleExpireValue(targetInput, inc) {
|
||||
inc === -1 ? this.refs[targetInput].stepDown(1) : this.refs[targetInput].stepUp(1)
|
||||
|
||||
// Use custom step count to support browser Edge
|
||||
if((inc === -1)) {
|
||||
if(value != 0) {
|
||||
value--
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(value != maxValue) {
|
||||
value++
|
||||
}
|
||||
}
|
||||
this.refs[targetInput].value = value
|
||||
|
||||
// Reset hours and mins when days reaches it's max value
|
||||
if (this.refs.expireDays.value == 7) {
|
||||
this.refs.expireHours.value = 0
|
||||
this.refs.expireMins.value = 0
|
||||
}
|
||||
if (this.refs.expireDays.value + this.refs.expireHours.value + this.refs.expireMins.value == 0) {
|
||||
this.refs.expireDays.value = 7
|
||||
}
|
||||
|
||||
const {dispatch} = this.props
|
||||
dispatch(actions.shareObject(object, this.refs.expireDays.value, this.refs.expireHours.value, this.refs.expireMins.value))
|
||||
}
|
||||
|
||||
checkObject(e, objectName) {
|
||||
const {dispatch} = this.props
|
||||
e.target.checked ? dispatch(actions.checkedObjectsAdd(objectName)) : dispatch(actions.checkedObjectsRemove(objectName))
|
||||
}
|
||||
|
||||
downloadSelected() {
|
||||
const {dispatch, web} = this.props
|
||||
let req = {
|
||||
bucketName: this.props.currentBucket,
|
||||
objects: this.props.checkedObjects,
|
||||
prefix: this.props.currentPath
|
||||
}
|
||||
if (!web.LoggedIn()) {
|
||||
let requestUrl = location.origin + "/minio/zip?token=''"
|
||||
this.xhr = new XMLHttpRequest()
|
||||
dispatch(actions.downloadSelected(requestUrl, req, this.xhr))
|
||||
} else {
|
||||
web.CreateURLToken()
|
||||
.then(res => {
|
||||
let requestUrl = location.origin + minioBrowserPrefix + "/zip?token=" + res.token
|
||||
this.xhr = new XMLHttpRequest()
|
||||
dispatch(actions.downloadSelected(requestUrl, req, this.xhr))
|
||||
})
|
||||
.catch(err => dispatch(actions.showAlert({
|
||||
type: 'danger',
|
||||
message: err.message
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
clearSelected() {
|
||||
const {dispatch} = this.props
|
||||
dispatch(actions.checkedObjectsReset())
|
||||
}
|
||||
|
||||
render() {
|
||||
const {total, free} = this.props.storageInfo
|
||||
const {showMakeBucketModal, alert, sortNameOrder, sortSizeOrder, sortDateOrder, showAbout, showBucketPolicy, checkedObjects} = this.props
|
||||
const {showMakeBucketModal, alert, sortNameOrder, sortSizeOrder, sortDateOrder, showAbout, showBucketPolicy} = this.props
|
||||
const {version, memory, platform, runtime} = this.props.serverInfo
|
||||
const {sidebarStatus} = this.props
|
||||
const {showSettings} = this.props
|
||||
@@ -526,28 +416,25 @@ 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={minioBrowserPrefix+'/login'}>Login</a>
|
||||
loginButton = <a className='btn btn-danger' href='/minio/login'>Login</a>
|
||||
}
|
||||
|
||||
if (web.LoggedIn()) {
|
||||
if (!(used === 0 && free === 0)) {
|
||||
storageUsageDetails = <div className="feh-usage">
|
||||
<div className="fehu-chart">
|
||||
<div style={ { width: usedPercent } }></div>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<span>Used: </span>
|
||||
{ humanize.filesize(total - free) }
|
||||
</li>
|
||||
<li className="pull-right">
|
||||
<span>Free: </span>
|
||||
{ humanize.filesize(total - used) }
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
storageUsageDetails = <div className="feh-usage">
|
||||
<div className="fehu-chart">
|
||||
<div style={ { width: usedPercent } }></div>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
Used:
|
||||
{ humanize.filesize(total - free) }
|
||||
</li>
|
||||
<li className="pull-right">
|
||||
Free:
|
||||
{ humanize.filesize(total - used) }
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
@@ -601,18 +488,8 @@ export default class Browse extends React.Component {
|
||||
<SideBar searchBuckets={ this.searchBuckets.bind(this) }
|
||||
selectBucket={ this.selectBucket.bind(this) }
|
||||
clickOutside={ this.hideSidebar.bind(this) }
|
||||
showPolicy={ this.showBucketPolicy.bind(this) }
|
||||
deleteBucket={ this.deleteBucket.bind(this) }
|
||||
toggleBucketDropdown={ this.toggleBucketDropdown.bind(this) } />
|
||||
showPolicy={ this.showBucketPolicy.bind(this) } />
|
||||
<div className="fe-body">
|
||||
<div className={ 'list-actions' + (classNames({
|
||||
' list-actions-toggled': checkedObjects.length > 0
|
||||
})) }>
|
||||
<span className="la-label"><i className="fa fa-check-circle" /> { checkedObjects.length } Objects selected</span>
|
||||
<span className="la-actions pull-right"><button onClick={ this.downloadSelected.bind(this) }> Download all as zip </button></span>
|
||||
<span className="la-actions pull-right"><button onClick={ this.showDeleteConfirmation.bind(this) }> Delete selected </button></span>
|
||||
<i className="la-close fa fa-times" onClick={ this.clearSelected.bind(this) }></i>
|
||||
</div>
|
||||
<Dropzone>
|
||||
{ alertBox }
|
||||
<header className="fe-header-mobile hidden-lg hidden-md">
|
||||
@@ -638,8 +515,7 @@ export default class Browse extends React.Component {
|
||||
</header>
|
||||
<div className="feb-container">
|
||||
<header className="fesl-row" data-type="folder">
|
||||
<div className="fesl-item fesl-item-icon"></div>
|
||||
<div className="fesl-item fesl-item-name" onClick={ this.sortObjectsByName.bind(this) } data-sort="name">
|
||||
<div className="fesl-item fi-name" onClick={ this.sortObjectsByName.bind(this) } data-sort="name">
|
||||
Name
|
||||
<i className={ classNames({
|
||||
'fesli-sort': true,
|
||||
@@ -648,7 +524,7 @@ export default class Browse extends React.Component {
|
||||
'fa-sort-alpha-asc': !sortNameOrder
|
||||
}) } />
|
||||
</div>
|
||||
<div className="fesl-item fesl-item-size" onClick={ this.sortObjectsBySize.bind(this) } data-sort="size">
|
||||
<div className="fesl-item fi-size" onClick={ this.sortObjectsBySize.bind(this) } data-sort="size">
|
||||
Size
|
||||
<i className={ classNames({
|
||||
'fesli-sort': true,
|
||||
@@ -657,7 +533,7 @@ export default class Browse extends React.Component {
|
||||
'fa-sort-amount-asc': !sortSizeOrder
|
||||
}) } />
|
||||
</div>
|
||||
<div className="fesl-item fesl-item-modified" onClick={ this.sortObjectsByDate.bind(this) } data-sort="last-modified">
|
||||
<div className="fesl-item fi-modified" onClick={ this.sortObjectsByDate.bind(this) } data-sort="last-modified">
|
||||
Last Modified
|
||||
<i className={ classNames({
|
||||
'fesli-sort': true,
|
||||
@@ -666,7 +542,7 @@ export default class Browse extends React.Component {
|
||||
'fa-sort-numeric-asc': !sortDateOrder
|
||||
}) } />
|
||||
</div>
|
||||
<div className="fesl-item fesl-item-actions"></div>
|
||||
<div className="fesl-item fi-actions"></div>
|
||||
</header>
|
||||
</div>
|
||||
<div className="feb-container">
|
||||
@@ -677,11 +553,9 @@ export default class Browse extends React.Component {
|
||||
<ObjectsList dataType={ this.dataType.bind(this) }
|
||||
selectPrefix={ this.selectPrefix.bind(this) }
|
||||
showDeleteConfirmation={ this.showDeleteConfirmation.bind(this) }
|
||||
shareObject={ this.shareObject.bind(this) }
|
||||
checkObject={ this.checkObject.bind(this) }
|
||||
checkedObjectsArray={ checkedObjects } />
|
||||
shareObject={ this.shareObject.bind(this) } />
|
||||
</InfiniteScroll>
|
||||
<div className="text-center" style={ { display: (istruncated && currentBucket) ? 'block' : 'none' } }>
|
||||
<div className="text-center" style={ { display: istruncated ? 'block' : 'none' } }>
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -795,11 +669,11 @@ export default class Browse extends React.Component {
|
||||
</div>
|
||||
<div className="input-group" style={ { display: web.LoggedIn() ? 'block' : 'none' } }>
|
||||
<label>
|
||||
Expires in (Max 7 days)
|
||||
Expires in
|
||||
</label>
|
||||
<div className="set-expire">
|
||||
<div className="set-expire-item">
|
||||
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireDays', 1, shareObject.object) } />
|
||||
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireDays', 1) }></i>
|
||||
<div className="set-expire-title">
|
||||
Days
|
||||
</div>
|
||||
@@ -808,14 +682,12 @@ export default class Browse extends React.Component {
|
||||
type="number"
|
||||
min={ 0 }
|
||||
max={ 7 }
|
||||
defaultValue={ 5 }
|
||||
readOnly="readOnly"
|
||||
/>
|
||||
defaultValue={ 0 } />
|
||||
</div>
|
||||
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireDays', -1, shareObject.object) } />
|
||||
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireDays', -1) }></i>
|
||||
</div>
|
||||
<div className="set-expire-item">
|
||||
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireHours', 1, shareObject.object) } />
|
||||
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireHours', 1) }></i>
|
||||
<div className="set-expire-title">
|
||||
Hours
|
||||
</div>
|
||||
@@ -823,34 +695,30 @@ export default class Browse extends React.Component {
|
||||
<input ref="expireHours"
|
||||
type="number"
|
||||
min={ 0 }
|
||||
max={ 23 }
|
||||
defaultValue={ 0 }
|
||||
readOnly="readOnly"
|
||||
/>
|
||||
max={ 24 }
|
||||
defaultValue={ 0 } />
|
||||
</div>
|
||||
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireHours', -1, shareObject.object) } />
|
||||
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireHours', -1) }></i>
|
||||
</div>
|
||||
<div className="set-expire-item">
|
||||
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireMins', 1, shareObject.object) } />
|
||||
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireMins', 1) }></i>
|
||||
<div className="set-expire-title">
|
||||
Minutes
|
||||
</div>
|
||||
<div className="set-expire-value">
|
||||
<input ref="expireMins"
|
||||
type="number"
|
||||
min={ 0 }
|
||||
max={ 59 }
|
||||
defaultValue={ 0 }
|
||||
readOnly="readOnly"
|
||||
/>
|
||||
min={ 1 }
|
||||
max={ 60 }
|
||||
defaultValue={ 45 } />
|
||||
</div>
|
||||
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireMins', -1, shareObject.object) } />
|
||||
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireMins', -1) }></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<div className="modal-footer">
|
||||
<CopyToClipboard text={ window.location.protocol + '//' + shareObject.url } onCopy={ this.showMessage.bind(this) }>
|
||||
<CopyToClipboard text={ shareObject.url } onCopy={ this.showMessage.bind(this) }>
|
||||
<button className="btn btn-success">
|
||||
Copy Link
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016, 2017 Minio, Inc.
|
||||
* Minio Browser (C) 2016, 2017 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,7 +27,7 @@ let BrowserDropdown = ({fullScreenFunc, aboutFunc, settingsFunc, logoutFunc}) =>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
<li>
|
||||
<a target="_blank" href="https://github.com/minio/minio">Github <i className="fa fa-github"></i></a>
|
||||
<a target="_blank" href="https://github.com/minio/miniobrowser">Github <i className="fa fa-github"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="" onClick={ fullScreenFunc }>Fullscreen <i className="fa fa-expand"></i></a>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -39,12 +39,11 @@ export default class Dropzone extends React.Component {
|
||||
// won't handle child elements correctly.
|
||||
const style = {
|
||||
height: '100%',
|
||||
borderWidth: '0',
|
||||
borderWidth: '2px',
|
||||
borderStyle: 'dashed',
|
||||
borderColor: '#fff'
|
||||
}
|
||||
const activeStyle = {
|
||||
borderWidth: '2px',
|
||||
borderColor: '#777'
|
||||
}
|
||||
const rejectStyle = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -89,6 +89,10 @@ export default class Login extends React.Component {
|
||||
{ alertBox }
|
||||
<div className="l-wrap">
|
||||
<form onSubmit={ this.handleSubmit.bind(this) }>
|
||||
<input name="fixBrowser"
|
||||
autoComplete="username"
|
||||
type="text"
|
||||
style={ { display: 'none' } } />
|
||||
<InputGroup className="ig-dark"
|
||||
label="Access Key"
|
||||
id="accessKey"
|
||||
@@ -98,6 +102,7 @@ export default class Login extends React.Component {
|
||||
required="required"
|
||||
autoComplete="username">
|
||||
</InputGroup>
|
||||
<input type="text" autoComplete="new-password" style={ { display: 'none' } } />
|
||||
<InputGroup className="ig-dark"
|
||||
label="Secret Key"
|
||||
id="secretKey"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,7 +20,8 @@ import humanize from 'humanize'
|
||||
import connect from 'react-redux/lib/components/connect'
|
||||
import Dropdown from 'react-bootstrap/lib/Dropdown'
|
||||
|
||||
let ObjectsList = ({objects, currentPath, selectPrefix, dataType, showDeleteConfirmation, shareObject, loadPath, checkObject, checkedObjectsArray}) => {
|
||||
|
||||
let ObjectsList = ({objects, currentPath, selectPrefix, dataType, showDeleteConfirmation, shareObject, loadPath}) => {
|
||||
const list = objects.map((object, i) => {
|
||||
let size = object.name.endsWith('/') ? '-' : humanize.filesize(object.size)
|
||||
let lastModified = object.name.endsWith('/') ? '-' : Moment(object.lastModified).format('lll')
|
||||
@@ -29,51 +30,29 @@ let ObjectsList = ({objects, currentPath, selectPrefix, dataType, showDeleteConf
|
||||
let deleteButton = ''
|
||||
if (web.LoggedIn())
|
||||
deleteButton = <a href="" className="fiad-action" onClick={ (e) => showDeleteConfirmation(e, `${currentPath}${object.name}`) }><i className="fa fa-trash"></i></a>
|
||||
|
||||
if (!checkedObjectsArray.length > 0) {
|
||||
if (!object.name.endsWith('/')) {
|
||||
actionButtons = <Dropdown id={ "fia-dropdown-" + object.name.replace('.', '-') }>
|
||||
<Dropdown.Toggle noCaret className="fia-toggle"></Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<a href="" className="fiad-action" onClick={ (e) => shareObject(e, `${currentPath}${object.name}`) }><i className="fa fa-copy"></i></a>
|
||||
{ deleteButton }
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
}
|
||||
if (!object.name.endsWith('/')) {
|
||||
actionButtons = <Dropdown id="fia-dropdown">
|
||||
<Dropdown.Toggle noCaret className="fia-toggle"></Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<a href="" className="fiad-action" onClick={ (e) => shareObject(e, `${currentPath}${object.name}`) }><i className="fa fa-copy"></i></a>
|
||||
{ deleteButton }
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
}
|
||||
|
||||
let activeClass = ''
|
||||
let isChecked = ''
|
||||
|
||||
if (checkedObjectsArray.indexOf(object.name) > -1) {
|
||||
activeClass = ' fesl-row-selected'
|
||||
isChecked = true
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={ i } className={ "fesl-row " + loadingClass + activeClass } data-type={ dataType(object.name, object.contentType) }>
|
||||
<div className="fesl-item fesl-item-icon">
|
||||
<div className="fi-select">
|
||||
<input type="checkbox"
|
||||
name={ object.name }
|
||||
checked={ isChecked }
|
||||
onChange={ (e) => checkObject(e, object.name) } />
|
||||
<i className="fis-icon"></i>
|
||||
<i className="fis-helper"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fesl-item fesl-item-name">
|
||||
<div key={ i } className={ "fesl-row " + loadingClass } data-type={ dataType(object.name, object.contentType) }>
|
||||
<div className="fesl-item fi-name">
|
||||
<a href="" onClick={ (e) => selectPrefix(e, `${currentPath}${object.name}`) }>
|
||||
{ object.name }
|
||||
</a>
|
||||
</div>
|
||||
<div className="fesl-item fesl-item-size">
|
||||
<div className="fesl-item fi-size">
|
||||
{ size }
|
||||
</div>
|
||||
<div className="fesl-item fesl-item-modified">
|
||||
<div className="fesl-item fi-modified">
|
||||
{ lastModified }
|
||||
</div>
|
||||
<div className="fesl-item fesl-item-actions">
|
||||
<div className="fesl-item fi-actions">
|
||||
{ actionButtons }
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,4 +72,4 @@ export default connect(state => {
|
||||
currentPath: state.currentPath,
|
||||
loadPath: state.loadPath
|
||||
}
|
||||
})(ObjectsList)
|
||||
})(ObjectsList)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -77,4 +77,4 @@ class Policy extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => state)(Policy)
|
||||
export default connect(state => state)(Policy)
|
||||
|
||||
@@ -7,7 +7,6 @@ import * as actions from '../actions'
|
||||
class PolicyInput extends Component {
|
||||
componentDidMount() {
|
||||
const {web, dispatch} = this.props
|
||||
this.prefix.focus()
|
||||
web.ListAllBucketPolicies({
|
||||
bucketName: this.props.currentBucket
|
||||
}).then(res => {
|
||||
@@ -28,23 +27,8 @@ class PolicyInput extends Component {
|
||||
|
||||
handlePolicySubmit(e) {
|
||||
e.preventDefault()
|
||||
const {web, dispatch, currentBucket} = this.props
|
||||
const {web, dispatch} = this.props
|
||||
|
||||
let prefix = currentBucket + '/' + this.prefix.value
|
||||
let policy = this.policy.value
|
||||
|
||||
if (!prefix.endsWith('*')) prefix = prefix + '*'
|
||||
|
||||
let prefixAlreadyExists = this.props.policies.some(elem => prefix === elem.prefix)
|
||||
|
||||
if (prefixAlreadyExists) {
|
||||
dispatch(actions.showAlert({
|
||||
type: 'danger',
|
||||
message: "Policy for this prefix already exists."
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
web.SetBucketPolicy({
|
||||
bucketName: this.props.currentBucket,
|
||||
prefix: this.prefix.value,
|
||||
@@ -52,7 +36,8 @@ class PolicyInput extends Component {
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(actions.setPolicies([{
|
||||
policy, prefix
|
||||
policy: this.policy.value,
|
||||
prefix: this.prefix.value + '*',
|
||||
}, ...this.props.policies]))
|
||||
this.prefix.value = ''
|
||||
})
|
||||
@@ -95,4 +80,4 @@ class PolicyInput extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => state)(PolicyInput)
|
||||
export default connect(state => state)(PolicyInput)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -34,12 +34,23 @@ class SettingsModal extends React.Component {
|
||||
|
||||
let accessKeyEnv = ''
|
||||
let secretKeyEnv = ''
|
||||
// Check environment variables first.
|
||||
if (serverInfo.info.isEnvCreds) {
|
||||
// Check environment variables first. They may or may not have been
|
||||
// loaded already; they load in Browse#componentDidMount.
|
||||
if (serverInfo.envVars) {
|
||||
serverInfo.envVars.forEach(envVar => {
|
||||
let keyVal = envVar.split('=')
|
||||
if (keyVal[0] == 'MINIO_ACCESS_KEY') {
|
||||
accessKeyEnv = keyVal[1]
|
||||
} else if (keyVal[0] == 'MINIO_SECRET_KEY') {
|
||||
secretKeyEnv = keyVal[1]
|
||||
}
|
||||
})
|
||||
}
|
||||
if (accessKeyEnv != '' || secretKeyEnv != '') {
|
||||
dispatch(actions.setSettings({
|
||||
accessKey: 'xxxxxxxxx',
|
||||
secretKey: 'xxxxxxxxx',
|
||||
keysReadOnly: true
|
||||
accessKey: accessKeyEnv,
|
||||
secretKey: secretKeyEnv,
|
||||
keysReadOnly: true
|
||||
}))
|
||||
} else {
|
||||
web.GetAuth()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,9 +21,8 @@ import Scrollbars from 'react-custom-scrollbars/lib/Scrollbars'
|
||||
import connect from 'react-redux/lib/components/connect'
|
||||
|
||||
import logo from '../../img/logo.svg'
|
||||
import Dropdown from 'react-bootstrap/lib/Dropdown'
|
||||
|
||||
let SideBar = ({visibleBuckets, loadBucket, currentBucket, selectBucket, searchBuckets, sidebarStatus, clickOutside, showPolicy, deleteBucket, toggleBucketDropdown, showBucketDropdown}) => {
|
||||
let SideBar = ({visibleBuckets, loadBucket, currentBucket, selectBucket, searchBuckets, sidebarStatus, clickOutside, showPolicy}) => {
|
||||
|
||||
const list = visibleBuckets.map((bucket, i) => {
|
||||
return <li className={ classNames({
|
||||
@@ -34,19 +33,7 @@ let SideBar = ({visibleBuckets, loadBucket, currentBucket, selectBucket, searchB
|
||||
}) }>
|
||||
{ bucket }
|
||||
</a>
|
||||
<Dropdown open={bucket === currentBucket && showBucketDropdown} onToggle={toggleBucketDropdown} className="bucket-dropdown" id="bucket-dropdown">
|
||||
<Dropdown.Toggle noCaret>
|
||||
<i className="zmdi zmdi-more-vert" />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
<li>
|
||||
<a onClick={ showPolicy }>Edit policy</a>
|
||||
</li>
|
||||
<li>
|
||||
<a onClick={ (e) => deleteBucket(e, bucket) }>Delete</a>
|
||||
</li>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<i className="fesli-trigger" onClick={ showPolicy }></i>
|
||||
</li>
|
||||
})
|
||||
|
||||
@@ -93,7 +80,6 @@ export default connect(state => {
|
||||
visibleBuckets: state.visibleBuckets,
|
||||
loadBucket: state.loadBucket,
|
||||
currentBucket: state.currentBucket,
|
||||
showBucketDropdown: state.showBucketDropdown,
|
||||
sidebarStatus: state.sidebarStatus
|
||||
}
|
||||
})(SideBar)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,9 +17,7 @@
|
||||
// File for all the browser constants.
|
||||
|
||||
// minioBrowserPrefix absolute path.
|
||||
var p = window.location.pathname
|
||||
export const minioBrowserPrefix = p.slice(0, p.indexOf("/", 1))
|
||||
|
||||
export const minioBrowserPrefix = '/minio'
|
||||
export const READ_ONLY = 'readonly'
|
||||
export const WRITE_ONLY = 'writeonly'
|
||||
export const READ_WRITE = 'readwrite'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
+12
-37
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,7 +25,6 @@ export default (state = {
|
||||
storageInfo: {},
|
||||
serverInfo: {},
|
||||
currentBucket: '',
|
||||
showBucketDropdown: false,
|
||||
currentPath: '',
|
||||
showMakeBucketModal: false,
|
||||
uploads: {},
|
||||
@@ -55,10 +54,9 @@ export default (state = {
|
||||
shareObject: {
|
||||
show: false,
|
||||
url: '',
|
||||
object: ''
|
||||
expiry: 604800
|
||||
},
|
||||
prefixWritable: false,
|
||||
checkedObjects: []
|
||||
prefixWritable: false
|
||||
}, action) => {
|
||||
let newState = Object.assign({}, state)
|
||||
switch (action.type) {
|
||||
@@ -72,32 +70,22 @@ export default (state = {
|
||||
newState.buckets = [action.bucket, ...newState.buckets]
|
||||
newState.visibleBuckets = [action.bucket, ...newState.visibleBuckets]
|
||||
break
|
||||
case actions.REMOVE_BUCKET:
|
||||
newState.buckets = newState.buckets.filter(bucket => bucket != action.bucket)
|
||||
newState.visibleBuckets = newState.visibleBuckets.filter(bucket => bucket != action.bucket)
|
||||
newState.currentBucket = ""
|
||||
break
|
||||
case actions.SHOW_BUCKET_DROPDOWN:
|
||||
newState.showBucketDropdown = action.showBucketDropdown
|
||||
break
|
||||
case actions.SET_VISIBLE_BUCKETS:
|
||||
newState.visibleBuckets = action.visibleBuckets
|
||||
break
|
||||
case actions.SET_CURRENT_BUCKET:
|
||||
newState.currentBucket = action.currentBucket
|
||||
break
|
||||
case actions.APPEND_OBJECTS:
|
||||
newState.objects = [...newState.objects, ...action.objects]
|
||||
newState.marker = action.marker
|
||||
newState.istruncated = action.istruncated
|
||||
break
|
||||
case actions.SET_OBJECTS:
|
||||
newState.objects = [...action.objects]
|
||||
break
|
||||
case actions.RESET_OBJECTS:
|
||||
newState.objects = []
|
||||
newState.marker = ""
|
||||
newState.istruncated = false
|
||||
if (!action.objects.length) {
|
||||
newState.objects = []
|
||||
newState.marker = ""
|
||||
newState.istruncated = action.istruncated
|
||||
} else {
|
||||
newState.objects = [...newState.objects, ...action.objects]
|
||||
newState.marker = action.marker
|
||||
newState.istruncated = action.istruncated
|
||||
}
|
||||
break
|
||||
case actions.SET_CURRENT_PATH:
|
||||
newState.currentPath = action.currentPath
|
||||
@@ -197,19 +185,6 @@ export default (state = {
|
||||
if (idx == -1) break
|
||||
newState.objects = [...newState.objects.slice(0, idx), ...newState.objects.slice(idx + 1)]
|
||||
break
|
||||
|
||||
case actions.CHECKED_OBJECTS_ADD:
|
||||
newState.checkedObjects = [...newState.checkedObjects, action.objectName]
|
||||
break
|
||||
case actions.CHECKED_OBJECTS_REMOVE:
|
||||
let index = newState.checkedObjects.indexOf(action.objectName)
|
||||
if (index == -1) break
|
||||
newState.checkedObjects = [...newState.checkedObjects.slice(0, index), ...newState.checkedObjects.slice(index + 1)]
|
||||
break
|
||||
case actions.CHECKED_OBJECTS_RESET:
|
||||
newState.checkedObjects = []
|
||||
break
|
||||
}
|
||||
|
||||
return newState
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -87,9 +87,6 @@ export default class Web {
|
||||
MakeBucket(args) {
|
||||
return this.makeCall('MakeBucket', args)
|
||||
}
|
||||
DeleteBucket(args) {
|
||||
return this.makeCall('DeleteBucket', args)
|
||||
}
|
||||
ListObjects(args) {
|
||||
return this.makeCall('ListObjects', args)
|
||||
}
|
||||
@@ -115,9 +112,6 @@ export default class Web {
|
||||
return res
|
||||
})
|
||||
}
|
||||
CreateURLToken() {
|
||||
return this.makeCall('CreateURLToken')
|
||||
}
|
||||
GetBucketPolicy(args) {
|
||||
return this.makeCall('GetBucketPolicy', args)
|
||||
}
|
||||
|
||||
@@ -35,10 +35,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-white {
|
||||
.btn-variant(#fff, darken(@text-color, 20%));
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
.btn-variant(#eee, #545454);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
.fe-body {
|
||||
@media(min-width: @screen-md-min) {
|
||||
padding: 0 0 80px @fe-sidebar-width;
|
||||
padding: 0 0 40px @fe-sidebar-width;
|
||||
}
|
||||
|
||||
@media(max-width: @screen-sm-max) {
|
||||
@@ -79,7 +79,7 @@
|
||||
text-align: center;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
|
||||
@@ -81,7 +81,7 @@ select.form-control {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: 0;
|
||||
background: transparent !important;
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -114,8 +114,8 @@ select.form-control {
|
||||
|
||||
.ig-dark {
|
||||
.ig-text {
|
||||
color: @white !important;
|
||||
border-color: rgba(255,255,255,0.1) !important;
|
||||
color: @white;
|
||||
border-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.ig-helpers {
|
||||
@@ -183,17 +183,6 @@ select.form-control {
|
||||
.set-expire {
|
||||
border: 1px solid @input-border;
|
||||
margin: 35px 0 30px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.set-expire-item {
|
||||
@@ -202,7 +191,6 @@ select.form-control {
|
||||
display: table-cell;
|
||||
width: 1%;
|
||||
text-align: center;
|
||||
.user-select(none);
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid @input-border;
|
||||
@@ -221,7 +209,6 @@ select.form-control {
|
||||
left: -8px;
|
||||
|
||||
input {
|
||||
.user-select(none);
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
Header
|
||||
----------------------------*/
|
||||
.fe-header {
|
||||
@media(min-width: (@screen-sm-min - 100)) {
|
||||
padding: 45px 55px 20px;
|
||||
|
||||
@media(min-width: @screen-md-min) {
|
||||
position: relative;
|
||||
padding: 40px 40px 20px 45px;
|
||||
}
|
||||
|
||||
@media(max-width: (@screen-xs-max - 100)) {
|
||||
padding: 20px;
|
||||
padding: 25px 25px 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@@ -238,3 +239,4 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+102
-236
@@ -2,19 +2,17 @@
|
||||
Row
|
||||
----------------------------*/
|
||||
.fesl-row {
|
||||
padding-right: 40px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
position: relative;
|
||||
|
||||
@media (min-width: (@screen-sm-min - 100px)) {
|
||||
padding: 5px 20px 5px 40px;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media(max-width: (@screen-xs-max - 100px)) {
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
.clearfix();
|
||||
}
|
||||
|
||||
@@ -22,7 +20,7 @@ header.fesl-row {
|
||||
@media (min-width:(@screen-sm-min - 100px)) {
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid lighten(@text-muted-color, 20%);
|
||||
padding-left: 30px;
|
||||
padding-left: 40px;
|
||||
|
||||
.fesl-item,
|
||||
.fesli-sort {
|
||||
@@ -44,7 +42,7 @@ header.fesl-row {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:hover:not(.fesl-item-actions) {
|
||||
&:hover:not(.fi-actions) {
|
||||
background: lighten(@text-muted-color, 22%);
|
||||
color: @dark-gray;
|
||||
|
||||
@@ -60,42 +58,54 @@ header.fesl-row {
|
||||
}
|
||||
}
|
||||
|
||||
.list-type(@background, @icon) {
|
||||
.fis-icon {
|
||||
background-color: @background;
|
||||
|
||||
&:before {
|
||||
content: @icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.fesl-row {
|
||||
padding-left: 85px;
|
||||
border-bottom: 1px solid transparent;
|
||||
cursor: default;
|
||||
.transition(background-color);
|
||||
.transition-duration(500ms);
|
||||
|
||||
@media (max-width: (@screen-xs-max - 100px)) {
|
||||
padding: 5px 20px;
|
||||
padding-left: 70px;
|
||||
padding-right: 45px;
|
||||
}
|
||||
|
||||
&:not(.fesl-row-selected) {
|
||||
&:nth-child(even) {
|
||||
background-color: @list-row-even-bg;
|
||||
}
|
||||
&:nth-child(even) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fis-icon {
|
||||
&:before {
|
||||
.opacity(0)
|
||||
}
|
||||
}
|
||||
background-color: #fbf7dc;
|
||||
}
|
||||
|
||||
.fis-helper {
|
||||
&:before {
|
||||
.opacity(1);
|
||||
&[data-type]:before {
|
||||
font-family: @font-family-icon;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
text-align: center;
|
||||
line-height: 35px;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
font-size: 16px;
|
||||
left: 50px;
|
||||
top: 9px;
|
||||
color: @white;
|
||||
|
||||
@media (max-width: (@screen-xs-max - 100px)) {
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-type="folder"] {
|
||||
@media (max-width: (@screen-xs-max - 100px)) {
|
||||
.fesl-item {
|
||||
&.fi-name {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
&.fi-size,
|
||||
&.fi-modified {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,18 +113,54 @@ div.fesl-row {
|
||||
/*--------------------------
|
||||
Icons
|
||||
----------------------------*/
|
||||
&[data-type=folder] { .list-type(#a1d6dd, '\f114'); }
|
||||
&[data-type=pdf] {.list-type(#fa7775, '\f1c1'); }
|
||||
&[data-type=zip] { .list-type(#427089, '\f1c6'); }
|
||||
&[data-type=audio] { .list-type(#009688, '\f1c7'); }
|
||||
&[data-type=code] { .list-type(#997867, "\f1c9"); }
|
||||
&[data-type=excel] { .list-type(#64c866, '\f1c3'); }
|
||||
&[data-type=image] { .list-type(#f06292, '\f1c5'); }
|
||||
&[data-type=video] { .list-type(#f8c363, '\f1c8'); }
|
||||
&[data-type=other] { .list-type(#afafaf, '\f016'); }
|
||||
&[data-type=text] { .list-type(#8a8a8a, '\f0f6'); }
|
||||
&[data-type=doc] { .list-type(#2196f5, '\f1c2'); }
|
||||
&[data-type=presentation] { .list-type(#896ea6, '\f1c4'); }
|
||||
&[data-type=folder]:before {
|
||||
content: '\f114';
|
||||
background-color: #a1d6dd;
|
||||
}
|
||||
&[data-type=pdf]:before {
|
||||
content: "\f1c1";
|
||||
background-color: #fa7775;
|
||||
}
|
||||
&[data-type=zip]:before {
|
||||
content: "\f1c6";
|
||||
background-color: #427089;
|
||||
}
|
||||
&[data-type=audio]:before {
|
||||
content: "\f1c7";
|
||||
background-color: #009688
|
||||
}
|
||||
&[data-type=code]:before {
|
||||
content: "\f1c9";
|
||||
background-color: #997867;
|
||||
}
|
||||
&[data-type=excel]:before {
|
||||
content: "\f1c3";
|
||||
background-color: #64c866;
|
||||
}
|
||||
&[data-type=image]:before {
|
||||
content: "\f1c5";
|
||||
background-color: #f06292;
|
||||
}
|
||||
&[data-type=video]:before {
|
||||
content: "\f1c8";
|
||||
background-color: #f8c363;
|
||||
}
|
||||
&[data-type=other]:before {
|
||||
content: "\f016";
|
||||
background-color: #afafaf;
|
||||
}
|
||||
&[data-type=text]:before {
|
||||
content: "\f0f6";
|
||||
background-color: #8a8a8a;
|
||||
}
|
||||
&[data-type=doc]:before {
|
||||
content: "\f1c2";
|
||||
background-color: #2196f5;
|
||||
}
|
||||
&[data-type=presentation]:before {
|
||||
content: "\f1c4";
|
||||
background-color: #896ea6;
|
||||
}
|
||||
|
||||
&.fesl-loading{
|
||||
&:before {
|
||||
@@ -134,113 +180,6 @@ div.fesl-row {
|
||||
}
|
||||
}
|
||||
|
||||
.fesl-row-selected {
|
||||
background-color: @list-row-selected-bg;
|
||||
|
||||
&, .fesl-item a {
|
||||
color: darken(@text-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.fi-select {
|
||||
float: left;
|
||||
position: relative;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
margin: 3px 0;
|
||||
|
||||
@media(max-width: (@screen-xs-max - 100px)) {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
z-index: 8;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:checked {
|
||||
& ~ .fis-icon {
|
||||
background-color: #32393F;
|
||||
|
||||
&:before {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& ~ .fis-helper {
|
||||
&:before {
|
||||
.scale(0);
|
||||
}
|
||||
|
||||
&:after {
|
||||
.scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fis-icon {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
border-radius: 50%;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
.transition(background-color);
|
||||
.transition-duration(250ms);
|
||||
|
||||
&:before {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
font-family: @font-family-icon;
|
||||
line-height: 35px;
|
||||
font-size: 16px;
|
||||
color: @white;
|
||||
.transition(all);
|
||||
.transition-duration(300ms);
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.fis-helper {
|
||||
&:before,
|
||||
&:after {
|
||||
position: absolute;
|
||||
.transition(all);
|
||||
.transition-duration(250ms);
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: 2px solid @white;
|
||||
z-index: 7;
|
||||
border-radius: 2px;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
font-family: @font-family-icon;
|
||||
content: '\f00c';
|
||||
top: 8px;
|
||||
left: 9px;
|
||||
color: @white;
|
||||
font-size: 14px;
|
||||
.scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*--------------------------
|
||||
Files and Folders
|
||||
@@ -253,26 +192,26 @@ div.fesl-row {
|
||||
}
|
||||
|
||||
@media(min-width: (@screen-sm-min - 100px)) {
|
||||
&:not(.fesl-item-actions):not(.fesl-item-icon) {
|
||||
&:not(.fi-actions) {
|
||||
text-overflow: ellipsis;
|
||||
padding: 10px 15px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.fesl-item-name {
|
||||
&.fi-name {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
&.fesl-item-size {
|
||||
&.fi-size {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
&.fesl-item-modified {
|
||||
&.fi-modified {
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
&.fesl-item-actions {
|
||||
&.fi-actions {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
@@ -280,29 +219,29 @@ div.fesl-row {
|
||||
@media(max-width: (@screen-xs-max - 100px)) {
|
||||
padding: 0;
|
||||
|
||||
&.fesl-item-name {
|
||||
&.fi-name {
|
||||
width: 100%;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
&.fesl-item-size,
|
||||
&.fesl-item-modified {
|
||||
&.fi-size,
|
||||
&.fi-modified {
|
||||
font-size: 12px;
|
||||
color: #B5B5B5;
|
||||
float: left;
|
||||
}
|
||||
|
||||
&.fesl-item-modified {
|
||||
&.fi-modified {
|
||||
max-width: 72px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.fesl-item-size {
|
||||
&.fi-size {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&.fesl-item-actions {
|
||||
&.fi-actions {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 10px;
|
||||
@@ -327,7 +266,7 @@ div.fesl-row {
|
||||
}
|
||||
}
|
||||
|
||||
.fesl-item-actions {
|
||||
.fi-actions {
|
||||
.dropdown-menu {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
@@ -385,79 +324,6 @@ div.fesl-row {
|
||||
}
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
position: fixed;
|
||||
.translate3d(0, -100%, 0);
|
||||
.opacity(0);
|
||||
.transition(all);
|
||||
.transition-duration(200ms);
|
||||
padding: 20px 70px 20px 25px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: @brand-primary;
|
||||
z-index: 20;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
||||
text-align: center;
|
||||
|
||||
&.list-actions-toggled {
|
||||
.translate3d(0, 0, 0);
|
||||
.opacity(1);
|
||||
}
|
||||
}
|
||||
|
||||
.la-close {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 0;
|
||||
color: #fff;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 30px !important;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
font-weight: normal;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.la-label {
|
||||
color: @white;
|
||||
float: left;
|
||||
padding: 4px 0;
|
||||
|
||||
.fa {
|
||||
font-size: 22px;
|
||||
vertical-align: top;
|
||||
margin-right: 10px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.la-actions {
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: 2px solid rgba(255,255,255,0.9);
|
||||
color: @white;
|
||||
border-radius: 2px;
|
||||
padding: 5px 10px;
|
||||
font-size: 13px;
|
||||
.transition(all);
|
||||
.transition-duration(300ms);
|
||||
margin-left: 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: @white;
|
||||
color: @brand-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes fiad-action-anim {
|
||||
from {
|
||||
|
||||
@@ -99,18 +99,4 @@
|
||||
content: '7 days';
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.modal-aheader {
|
||||
height: 100px;
|
||||
|
||||
&:before {
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
@@ -49,4 +49,4 @@
|
||||
z-index: 1;
|
||||
-webkit-animation: zoomIn 250ms, spin 700ms 250ms infinite linear;
|
||||
animation: zoomIn 250ms, spin 700ms 250ms infinite linear;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding: 25px;
|
||||
padding: 35px;
|
||||
|
||||
@media(min-width: @screen-md-min) {
|
||||
.translate3d(0, 0, 0);
|
||||
@@ -63,15 +63,15 @@
|
||||
height: ~"calc(100vh - 260px)";
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
margin: 0 -25px;
|
||||
margin: 0 -35px;
|
||||
|
||||
& li {
|
||||
position: relative;
|
||||
|
||||
& > a {
|
||||
display: block;
|
||||
padding: 10px 45px 12px 55px;
|
||||
word-wrap: break-word;
|
||||
padding: 10px 40px 12px 65px;
|
||||
.text-overflow();
|
||||
|
||||
&:before {
|
||||
font-family: FontAwesome;
|
||||
@@ -79,7 +79,7 @@
|
||||
font-size: 17px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 25px;
|
||||
left: 35px;
|
||||
.opacity(0.8);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #282e32;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
|
||||
& > a {
|
||||
color: @white;
|
||||
@@ -111,7 +111,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.bucket-dropdown .dropdown-toggle {
|
||||
.fesli-trigger {
|
||||
.opacity(0.6);
|
||||
|
||||
&:hover {
|
||||
@@ -132,40 +132,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Dropdown */
|
||||
.bucket-dropdown {
|
||||
.fesli-trigger {
|
||||
.opacity(0);
|
||||
.transition(all);
|
||||
.transition-duration(200ms);
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
width: 35px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
color: @white;
|
||||
|
||||
.dropdown-toggle {
|
||||
.opacity(0);
|
||||
.transition(all);
|
||||
.transition-duration(200ms);
|
||||
font-size: 20px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.dropdown-menu-right {
|
||||
padding: 15px 0;
|
||||
margin-top: -1px;
|
||||
& li {
|
||||
a:before {
|
||||
content: none;
|
||||
-webkit-box-shadow: @dropdown-shadow;
|
||||
box-shadow: @dropdown-shadow;
|
||||
}
|
||||
&:not(.active):hover {
|
||||
& > a {
|
||||
color: @dropdown-link-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
background: url(../../img/more-h-light.svg) no-repeat left;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
/*--------------------------
|
||||
File Explorer
|
||||
----------------------------*/
|
||||
@fe-sidebar-width : 320px;
|
||||
@fe-sidebar-width : 300px;
|
||||
@text-muted-color : #BDBDBD;
|
||||
@text-strong-color : #333;
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
/*-------------------------
|
||||
Colors
|
||||
--------------------------*/
|
||||
@brand-primary: #2298f7;
|
||||
@brand-primary: #2196F3;
|
||||
@brand-success: #4CAF50;
|
||||
@brand-info: #00BCD4;
|
||||
@brand-warning: #FF9800;
|
||||
@@ -91,11 +91,4 @@
|
||||
/*-------------------------
|
||||
Form
|
||||
--------------------------*/
|
||||
@input-border: #eee;
|
||||
|
||||
|
||||
/*-------------------------
|
||||
List
|
||||
--------------------------*/
|
||||
@list-row-selected-bg: #fbf2bf;
|
||||
@list-row-even-bg: #fafafa;
|
||||
@input-border: #eee;
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -70,9 +70,9 @@ async.waterfall([
|
||||
commitId = stdout.replace('\n', '')
|
||||
if (commitId.length !== 40) throw new Error('commitId invalid : ' + commitId)
|
||||
assetsFileName = 'ui-assets.go';
|
||||
var cmd = 'go-bindata-assetfs -pkg browser -nocompress=true production/...'
|
||||
var cmd = 'go-bindata-assetfs -pkg miniobrowser -nocompress=true production/...'
|
||||
if (!isProduction) {
|
||||
cmd = 'go-bindata-assetfs -pkg browser -nocompress=true dev/...'
|
||||
cmd = 'go-bindata-assetfs -pkg miniobrowser -nocompress=true dev/...'
|
||||
}
|
||||
console.log('Running', cmd)
|
||||
exec(cmd, cb)
|
||||
|
||||
+19
-19
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "browser",
|
||||
"name": "minio-browser",
|
||||
"version": "0.0.1",
|
||||
"description": "Minio Browser",
|
||||
"scripts": {
|
||||
@@ -11,14 +11,14 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/minio/minio"
|
||||
"url": "https://github.com/minio/miniobrowser"
|
||||
},
|
||||
"author": "Minio Inc",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/minio/minio/issues"
|
||||
"url": "https://github.com/minio/miniobrowser/issues"
|
||||
},
|
||||
"homepage": "https://github.com/minio/minio",
|
||||
"homepage": "https://github.com/minio/miniobrowser",
|
||||
"devDependencies": {
|
||||
"async": "^1.5.2",
|
||||
"babel-cli": "^6.14.0",
|
||||
@@ -26,7 +26,6 @@
|
||||
"babel-loader": "^6.2.5",
|
||||
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
||||
"babel-polyfill": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.14.0",
|
||||
"babel-preset-react": "^6.11.1",
|
||||
"babel-register": "^6.14.0",
|
||||
@@ -35,6 +34,8 @@
|
||||
"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",
|
||||
@@ -46,39 +47,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",
|
||||
"material-design-iconic-font": "^2.2.0",
|
||||
"mime-db": "^1.25.0",
|
||||
"mime-types": "^2.1.13",
|
||||
"moment": "^2.15.1",
|
||||
"react": "^0.14.8",
|
||||
"react-addons-test-utils": "^0.14.8",
|
||||
"react-bootstrap": "^0.28.5",
|
||||
"react-copy-to-clipboard": "^4.2.3",
|
||||
"react-custom-scrollbars": "^2.2.2",
|
||||
"react-dom": "^0.14.6",
|
||||
"react-dropzone": "^3.5.3",
|
||||
"react-infinite-scroller": "^1.0.6",
|
||||
"react-onclickout": "2.0.4",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^2.8.1",
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^1.0.3",
|
||||
"superagent": "^1.8.4",
|
||||
"superagent-es6-promise": "^1.0.0",
|
||||
"webpack": "^1.12.11"
|
||||
"react-onclickout": "2.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
+61
-75
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||
* Minio Browser (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -71,10 +71,6 @@ var exports = {
|
||||
target: 'http://localhost:9000',
|
||||
secure: false
|
||||
},
|
||||
'/minio/zip': {
|
||||
target: 'http://localhost:9000',
|
||||
secure: false
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
|
||||
+1030
-1355
File diff suppressed because it is too large
Load Diff
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
|
||||
_init() {
|
||||
# Save release LDFLAGS
|
||||
LDFLAGS=$(go run buildscripts/gen-ldflags.go)
|
||||
|
||||
# Extract release tag
|
||||
release_tag=$(echo $LDFLAGS | awk {'print $4'} | cut -f2 -d=)
|
||||
|
||||
# Verify release tag.
|
||||
if [ -z "$release_tag" ]; then
|
||||
echo "Release tag cannot be empty. Please check return value of \`go run buildscripts/gen-ldflags.go\`"
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# Extract release string.
|
||||
release_str=$(echo $MINIO_RELEASE | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Verify release string.
|
||||
if [ -z "$release_str" ]; then
|
||||
echo "Release string cannot be empty. Please set \`MINIO_RELEASE\` env variable."
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# List of supported architectures
|
||||
SUPPORTED_OSARCH='linux/386 linux/amd64 linux/arm linux/arm64 windows/386 windows/amd64 darwin/amd64 freebsd/amd64'
|
||||
|
||||
## System binaries
|
||||
CP=`which cp`
|
||||
SHASUM=`which shasum`
|
||||
SED=`which sed`
|
||||
}
|
||||
|
||||
go_build() {
|
||||
local osarch=$1
|
||||
os=$(echo $osarch | cut -f1 -d'/')
|
||||
arch=$(echo $osarch | cut -f2 -d'/')
|
||||
package=$(go list -f '{{.ImportPath}}')
|
||||
echo -n "-->"
|
||||
printf "%15s:%s\n" "${osarch}" "${package}"
|
||||
|
||||
# Release binary name
|
||||
release_bin="$release_str/$os-$arch/$(basename $package).$release_tag"
|
||||
# Release binary downloadable name
|
||||
release_real_bin="$release_str/$os-$arch/$(basename $package)"
|
||||
# Release shasum name
|
||||
release_shasum="$release_str/$os-$arch/$(basename $package).shasum"
|
||||
|
||||
# Go build to build the binary.
|
||||
if [ "${arch}" == "arm" ]; then
|
||||
# Release binary downloadable name
|
||||
release_real_bin_6="$release_str/$os-${arch}6vl/$(basename $package)"
|
||||
|
||||
release_bin_6="$release_str/$os-${arch}6vl/$(basename $package).$release_tag"
|
||||
## Support building for ARM6vl
|
||||
GOARM=6 GOOS=$os GOARCH=$arch go build --ldflags "${LDFLAGS}" -o $release_bin_6
|
||||
|
||||
## Copy
|
||||
$CP -p $release_bin_6 $release_real_bin_6
|
||||
|
||||
# Release shasum name
|
||||
release_shasum_6="$release_str/$os-${arch}6vl/$(basename $package).shasum"
|
||||
|
||||
# Calculate shasum
|
||||
shasum_str=$(${SHASUM} ${release_bin_6})
|
||||
echo ${shasum_str} | $SED "s/$release_str\/$os-${arch}6vl\///g" > $release_shasum_6
|
||||
|
||||
# Release binary downloadable name
|
||||
release_real_bin_7="$release_str/$os-$arch/$(basename $package)"
|
||||
|
||||
release_bin_7="$release_str/$os-$arch/$(basename $package).$release_tag"
|
||||
## Support building for ARM7vl
|
||||
GOARM=7 GOOS=$os GOARCH=$arch go build --ldflags "${LDFLAGS}" -o $release_bin_7
|
||||
|
||||
## Copy
|
||||
$CP -p $release_bin_7 $release_real_bin_7
|
||||
|
||||
# Release shasum name
|
||||
release_shasum_7="$release_str/$os-$arch/$(basename $package).shasum"
|
||||
|
||||
# Calculate shasum
|
||||
shasum_str=$(${SHASUM} ${release_bin_7})
|
||||
echo ${shasum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_shasum_7
|
||||
else
|
||||
GOOS=$os GOARCH=$arch go build --ldflags "${LDFLAGS}" -o $release_bin
|
||||
|
||||
# Create copy
|
||||
if [ $os == "windows" ]; then
|
||||
$CP -p $release_bin ${release_real_bin}.exe
|
||||
else
|
||||
$CP -p $release_bin $release_real_bin
|
||||
fi
|
||||
|
||||
# Calculate shasum
|
||||
shasum_str=$(${SHASUM} ${release_bin})
|
||||
echo ${shasum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_shasum
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
# Build releases.
|
||||
echo "Executing $release_str builds for OS: ${SUPPORTED_OSARCH}"
|
||||
echo "Choose an OS Arch from the below"
|
||||
for osarch in ${SUPPORTED_OSARCH}; do
|
||||
echo ${osarch}
|
||||
done
|
||||
|
||||
read -p "If you want to build for all, Just press Enter: " chosen_osarch
|
||||
if [ "$chosen_osarch" = "" ] || [ "$chosen_osarch" = "all" ]; then
|
||||
for each_osarch in ${SUPPORTED_OSARCH}; do
|
||||
go_build ${each_osarch}
|
||||
done
|
||||
else
|
||||
for each_osarch in $(echo $chosen_osarch | sed 's/,/ /g'); do
|
||||
go_build ${each_osarch}
|
||||
done
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
# Run main.
|
||||
_init && main
|
||||
+24
-15
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Minio Cloud Storage, (C) 2014-2018 Minio, Inc.
|
||||
# Minio Cloud Storage, (C) 2014-2016 Minio, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -21,15 +21,10 @@ _init() {
|
||||
|
||||
## Minimum required versions for build dependencies
|
||||
GIT_VERSION="1.0"
|
||||
GO_VERSION="1.9.4"
|
||||
GO_VERSION="1.7.1"
|
||||
OSX_VERSION="10.8"
|
||||
KNAME=$(uname -s)
|
||||
ARCH=$(uname -m)
|
||||
case "${KNAME}" in
|
||||
SunOS )
|
||||
ARCH=$(isainfo -k)
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
## FIXME:
|
||||
@@ -93,44 +88,56 @@ assert_is_supported_arch() {
|
||||
return
|
||||
;;
|
||||
*)
|
||||
echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, aarch64, arm*]"
|
||||
echo "ERROR"
|
||||
echo "Arch '${ARCH}' not supported."
|
||||
echo "Supported Arch: [x86_64, amd64, aarch64, arm*]"
|
||||
exit 1
|
||||
esac
|
||||
}
|
||||
|
||||
assert_is_supported_os() {
|
||||
case "${KNAME}" in
|
||||
Linux | FreeBSD | OpenBSD | NetBSD | DragonFly | SunOS )
|
||||
Linux | FreeBSD | OpenBSD | NetBSD | DragonFly )
|
||||
return
|
||||
;;
|
||||
Darwin )
|
||||
osx_host_version=$(env sw_vers -productVersion)
|
||||
if ! check_minimum_version "${OSX_VERSION}" "${osx_host_version}"; then
|
||||
echo "OSX version '${osx_host_version}' is not supported. Minimum supported version: ${OSX_VERSION}"
|
||||
echo "ERROR"
|
||||
echo "OSX version '${osx_host_version}' not supported."
|
||||
echo "Minimum supported version: ${OSX_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
return
|
||||
;;
|
||||
*)
|
||||
echo "OS '${KNAME}' is not supported. Supported OS: [Linux, FreeBSD, OpenBSD, NetBSD, Darwin, DragonFly]"
|
||||
echo "ERROR"
|
||||
echo "OS '${KNAME}' is not supported."
|
||||
echo "Supported OS: [Linux, FreeBSD, OpenBSD, NetBSD, Darwin, DragonFly]"
|
||||
exit 1
|
||||
esac
|
||||
}
|
||||
|
||||
assert_check_golang_env() {
|
||||
if ! which go >/dev/null 2>&1; then
|
||||
echo "Cannot find go binary in your PATH configuration, please refer to Go installation document at https://docs.minio.io/docs/how-to-install-golang"
|
||||
echo "ERROR"
|
||||
echo "Cannot find go binary in your PATH configuration, please refer to Go installation document at"
|
||||
echo "https://docs.minio.io/docs/how-to-install-golang"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
installed_go_version=$(go version | sed 's/^.* go\([0-9.]*\).*$/\1/')
|
||||
if ! check_minimum_version "${GO_VERSION}" "${installed_go_version}"; then
|
||||
echo "Go runtime version '${installed_go_version}' is unsupported. Minimum supported version: ${GO_VERSION} to compile."
|
||||
echo "ERROR"
|
||||
echo "Go version '${installed_go_version}' not supported."
|
||||
echo "Minimum supported version: ${GO_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${GOPATH}" ]; then
|
||||
echo "GOPATH environment variable missing, please refer to Go installation document at https://docs.minio.io/docs/how-to-install-golang"
|
||||
echo "ERROR"
|
||||
echo "GOPATH environment variable missing, please refer to Go installation document"
|
||||
echo "https://docs.minio.io/docs/how-to-install-golang"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -140,7 +147,9 @@ assert_check_deps() {
|
||||
# support unusual Git versions such as: 2.7.4 (Apple Git-66)
|
||||
installed_git_version=$(git version | perl -ne '$_ =~ m/git version (.*?)( |$)/; print "$1\n";')
|
||||
if ! check_minimum_version "${GIT_VERSION}" "${installed_git_version}"; then
|
||||
echo "Git version '${installed_git_version}' is not supported. Minimum supported version: ${GIT_VERSION}"
|
||||
echo "ERROR"
|
||||
echo "Git version '${installed_git_version}' not supported."
|
||||
echo "Minimum supported version: ${GIT_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#
|
||||
|
||||
main() {
|
||||
echo "Checking project is in GOPATH:"
|
||||
|
||||
IFS=':' read -r -a paths <<< "$GOPATH"
|
||||
for path in "${paths[@]}"; do
|
||||
minio_path="$path/src/github.com/minio/minio"
|
||||
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
)
|
||||
|
||||
func genLDFlags(version string) string {
|
||||
ldflagsStr := "-s -w"
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.Version=" + version
|
||||
var ldflagsStr string
|
||||
ldflagsStr = "-X github.com/minio/minio/cmd.Version=" + version
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.ReleaseTag=" + releaseTag(version)
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.CommitID=" + commitID()
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.ShortCommitID=" + commitID()[:12]
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
set -e
|
||||
echo "" > coverage.txt
|
||||
|
||||
for d in $(go list ./... | grep -v browser); do
|
||||
go test -coverprofile=profile.out -covermode=atomic "$d"
|
||||
for d in $(go list ./... | grep -v vendor); do
|
||||
go test -coverprofile=profile.out -covermode=atomic $d
|
||||
if [ -f profile.out ]; then
|
||||
cat profile.out >> coverage.txt
|
||||
rm profile.out
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Minio Cloud Storage, (C) 2017, 2018 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 10
|
||||
|
||||
echo "$minio_pid"
|
||||
}
|
||||
|
||||
function start_minio_erasure()
|
||||
{
|
||||
"${MINIO[@]}" server "${WORK_DIR}/erasure-disk1" "${WORK_DIR}/erasure-disk2" "${WORK_DIR}/erasure-disk3" "${WORK_DIR}/erasure-disk4" >"$WORK_DIR/erasure-minio.log" 2>&1 &
|
||||
minio_pid=$!
|
||||
sleep 15
|
||||
|
||||
echo "$minio_pid"
|
||||
}
|
||||
|
||||
function start_minio_erasure_sets()
|
||||
{
|
||||
"${MINIO[@]}" server "${WORK_DIR}/erasure-disk-sets{1...32}" >"$WORK_DIR/erasure-minio-sets.log" 2>&1 &
|
||||
minio_pid=$!
|
||||
sleep 15
|
||||
|
||||
echo "$minio_pid"
|
||||
}
|
||||
|
||||
function start_minio_dist_erasure_sets()
|
||||
{
|
||||
declare -a minio_pids
|
||||
"${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9000.log" 2>&1 &
|
||||
minio_pids[0]=$!
|
||||
"${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9001.log" 2>&1 &
|
||||
minio_pids[1]=$!
|
||||
"${MINIO[@]}" server --address=:9002 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9002.log" 2>&1 &
|
||||
minio_pids[2]=$!
|
||||
"${MINIO[@]}" server --address=:9003 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9003.log" 2>&1 &
|
||||
minio_pids[3]=$!
|
||||
"${MINIO[@]}" server --address=:9004 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9004.log" 2>&1 &
|
||||
minio_pids[4]=$!
|
||||
"${MINIO[@]}" server --address=:9005 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9005.log" 2>&1 &
|
||||
minio_pids[5]=$!
|
||||
"${MINIO[@]}" server --address=:9006 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9006.log" 2>&1 &
|
||||
minio_pids[6]=$!
|
||||
"${MINIO[@]}" server --address=:9007 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9007.log" 2>&1 &
|
||||
minio_pids[7]=$!
|
||||
"${MINIO[@]}" server --address=:9008 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9008.log" 2>&1 &
|
||||
minio_pids[8]=$!
|
||||
"${MINIO[@]}" server --address=:9009 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9009.log" 2>&1 &
|
||||
minio_pids[9]=$!
|
||||
|
||||
sleep 30
|
||||
echo "${minio_pids[@]}"
|
||||
}
|
||||
|
||||
function start_minio_dist_erasure()
|
||||
{
|
||||
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_erasure_sets() {
|
||||
minio_pid="$(start_minio_erasure_sets)"
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
kill "$minio_pid"
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
cat "$WORK_DIR/erasure-minio-sets.log"
|
||||
fi
|
||||
rm -f "$WORK_DIR/erasure-minio-sets.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_dist_erasure_sets()
|
||||
{
|
||||
minio_pids=( $(start_minio_dist_erasure_sets) )
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
kill "$pid"
|
||||
done
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
for i in $(seq 0 9); do
|
||||
echo "server$i log:"
|
||||
cat "$WORK_DIR/dist-minio-900$i.log"
|
||||
done
|
||||
fi
|
||||
|
||||
for i in $(seq 0 9); do
|
||||
rm -f "$WORK_DIR/dist-minio-900$i.log"
|
||||
done
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_erasure()
|
||||
{
|
||||
minio_pid="$(start_minio_erasure)"
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
kill "$minio_pid"
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
cat "$WORK_DIR/erasure-minio.log"
|
||||
fi
|
||||
rm -f "$WORK_DIR/erasure-minio.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_dist_erasure()
|
||||
{
|
||||
minio_pids=( $(start_minio_dist_erasure) )
|
||||
|
||||
(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 Erasure setup"
|
||||
if ! run_test_erasure; then
|
||||
echo "FAILED"
|
||||
rm -fr "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Distributed Erasure setup"
|
||||
if ! run_test_dist_erasure; then
|
||||
echo "FAILED"
|
||||
rm -fr "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Erasure setup as sets"
|
||||
if ! run_test_erasure_sets; then
|
||||
echo "FAILED"
|
||||
rm -fr "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Distributed Erasure setup as sets"
|
||||
if ! run_test_dist_erasure_sets; 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"
|
||||
+434
-575
File diff suppressed because it is too large
Load Diff
+479
-692
File diff suppressed because it is too large
Load Diff
@@ -1,654 +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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
)
|
||||
|
||||
// healStatusSummary - overall short summary of a healing sequence
|
||||
type healStatusSummary string
|
||||
|
||||
// healStatusSummary constants
|
||||
const (
|
||||
healNotStartedStatus healStatusSummary = "not started"
|
||||
healRunningStatus = "running"
|
||||
healStoppedStatus = "stopped"
|
||||
healFinishedStatus = "finished"
|
||||
)
|
||||
|
||||
const (
|
||||
// a heal sequence with this many un-consumed heal result
|
||||
// items blocks until heal-status consumption resumes or is
|
||||
// aborted due to timeout.
|
||||
maxUnconsumedHealResultItems = 1000
|
||||
|
||||
// if no heal-results are consumed (via the heal-status API)
|
||||
// for this timeout duration, the heal sequence is aborted.
|
||||
healUnconsumedTimeout = 24 * time.Hour
|
||||
|
||||
// time-duration to keep heal sequence state after it
|
||||
// completes.
|
||||
keepHealSeqStateDuration = time.Minute * 10
|
||||
)
|
||||
|
||||
var (
|
||||
errHealIdleTimeout = fmt.Errorf("healing results were not consumed for too long")
|
||||
errHealPushStopNDiscard = fmt.Errorf("heal push stopped due to heal stop signal")
|
||||
errHealStopSignalled = fmt.Errorf("heal stop signalled")
|
||||
|
||||
errFnHealFromAPIErr = func(err error) error {
|
||||
errCode := toAPIErrorCode(err)
|
||||
apiErr := getAPIError(errCode)
|
||||
return fmt.Errorf("Heal internal error: %s: %s",
|
||||
apiErr.Code, apiErr.Description)
|
||||
}
|
||||
)
|
||||
|
||||
// healSequenceStatus - accumulated status of the heal sequence
|
||||
type healSequenceStatus struct {
|
||||
// lock to update this structure as it is concurrently
|
||||
// accessed
|
||||
updateLock *sync.RWMutex
|
||||
|
||||
// summary and detail for failures
|
||||
Summary healStatusSummary `json:"Summary"`
|
||||
FailureDetail string `json:"Detail,omitempty"`
|
||||
StartTime time.Time `json:"StartTime"`
|
||||
|
||||
// disk information
|
||||
NumDisks int `json:"NumDisks"`
|
||||
|
||||
// settings for the heal sequence
|
||||
HealSettings madmin.HealOpts `json:"Settings"`
|
||||
|
||||
// slice of available heal result records
|
||||
Items []madmin.HealResultItem `json:"Items"`
|
||||
}
|
||||
|
||||
// structure to hold state of all heal sequences in server memory
|
||||
type allHealState struct {
|
||||
sync.Mutex
|
||||
|
||||
// map of heal path to heal sequence
|
||||
healSeqMap map[string]*healSequence
|
||||
}
|
||||
|
||||
var (
|
||||
// global server heal state
|
||||
globalAllHealState allHealState
|
||||
)
|
||||
|
||||
// initAllHealState - initialize healing apparatus
|
||||
func initAllHealState(isErasureMode bool) {
|
||||
if !isErasureMode {
|
||||
return
|
||||
}
|
||||
|
||||
globalAllHealState = allHealState{
|
||||
healSeqMap: make(map[string]*healSequence),
|
||||
}
|
||||
}
|
||||
|
||||
// getHealSequence - Retrieve a heal sequence by path. The second
|
||||
// argument returns if a heal sequence actually exists.
|
||||
func (ahs *allHealState) getHealSequence(path string) (h *healSequence, exists bool) {
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
h, exists = ahs.healSeqMap[path]
|
||||
return h, exists
|
||||
}
|
||||
|
||||
// LaunchNewHealSequence - launches a background routine that performs
|
||||
// healing according to the healSequence argument. For each heal
|
||||
// sequence, state is stored in the `globalAllHealState`, which is a
|
||||
// map of the heal path to `healSequence` which holds state about the
|
||||
// heal sequence.
|
||||
//
|
||||
// Heal results are persisted in server memory for
|
||||
// `keepHealSeqStateDuration`. This function also launches a
|
||||
// background routine to clean up heal results after the
|
||||
// aforementioned duration.
|
||||
func (ahs *allHealState) LaunchNewHealSequence(h *healSequence) (
|
||||
respBytes []byte, errCode APIErrorCode, errMsg string) {
|
||||
|
||||
existsAndLive := false
|
||||
he, exists := ahs.getHealSequence(h.path)
|
||||
if exists {
|
||||
if !he.hasEnded() || len(he.currentStatus.Items) > 0 {
|
||||
existsAndLive = true
|
||||
}
|
||||
}
|
||||
if existsAndLive {
|
||||
// A heal sequence exists on the given path.
|
||||
if h.forceStarted {
|
||||
// stop the running heal sequence - wait for
|
||||
// it to finish.
|
||||
he.stop()
|
||||
for !he.hasEnded() {
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
} else {
|
||||
errMsg = "Heal is already running on the given path " +
|
||||
"(use force-start option to stop and start afresh). " +
|
||||
fmt.Sprintf("The heal was started by IP %s at %s",
|
||||
h.clientAddress, h.startTime)
|
||||
|
||||
return nil, ErrHealAlreadyRunning, errMsg
|
||||
}
|
||||
}
|
||||
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
|
||||
// Check if new heal sequence to be started overlaps with any
|
||||
// existing, running sequence
|
||||
for k, hSeq := range ahs.healSeqMap {
|
||||
if !hSeq.hasEnded() && (strings.HasPrefix(k, h.path) ||
|
||||
strings.HasPrefix(h.path, k)) {
|
||||
|
||||
errMsg = "The provided heal sequence path overlaps with an existing " +
|
||||
fmt.Sprintf("heal path: %s", k)
|
||||
return nil, ErrHealOverlappingPaths, errMsg
|
||||
}
|
||||
}
|
||||
|
||||
// Add heal state and start sequence
|
||||
ahs.healSeqMap[h.path] = h
|
||||
|
||||
// Launch top-level background heal go-routine
|
||||
go h.healSequenceStart()
|
||||
|
||||
// Launch clean-up routine to remove this heal sequence (after
|
||||
// it ends) from the global state after timeout has elapsed.
|
||||
go func() {
|
||||
var keepStateTimeout <-chan time.Time
|
||||
ticker := time.NewTicker(time.Minute)
|
||||
defer ticker.Stop()
|
||||
everyMinute := ticker.C
|
||||
for {
|
||||
select {
|
||||
// Check every minute if heal sequence has ended.
|
||||
case <-everyMinute:
|
||||
if h.hasEnded() {
|
||||
keepStateTimeout = time.After(keepHealSeqStateDuration)
|
||||
everyMinute = nil
|
||||
}
|
||||
|
||||
// This case does not fire until the heal
|
||||
// sequence completes.
|
||||
case <-keepStateTimeout:
|
||||
// Heal sequence has ended, keep
|
||||
// results state duration has elapsed,
|
||||
// so purge state.
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
delete(ahs.healSeqMap, h.path)
|
||||
return
|
||||
|
||||
case <-globalServiceDoneCh:
|
||||
// server could be restarting - need
|
||||
// to exit immediately
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
b, err := json.Marshal(madmin.HealStartSuccess{
|
||||
h.clientToken,
|
||||
h.clientAddress,
|
||||
h.startTime,
|
||||
})
|
||||
if err != nil {
|
||||
errorIf(err, "Failed to marshal heal result into json.")
|
||||
return nil, ErrInternalError, ""
|
||||
}
|
||||
return b, ErrNone, ""
|
||||
}
|
||||
|
||||
// PopHealStatusJSON - Called by heal-status API. It fetches the heal
|
||||
// status results from global state and returns its JSON
|
||||
// representation. The clientToken helps ensure there aren't
|
||||
// conflicting clients fetching status.
|
||||
func (ahs *allHealState) PopHealStatusJSON(path string,
|
||||
clientToken string) ([]byte, APIErrorCode) {
|
||||
|
||||
// fetch heal state for given path
|
||||
h, exists := ahs.getHealSequence(path)
|
||||
if !exists {
|
||||
// If there is no such heal sequence, return error.
|
||||
return nil, ErrHealNoSuchProcess
|
||||
}
|
||||
|
||||
// Check if client-token is valid
|
||||
if clientToken != h.clientToken {
|
||||
return nil, ErrHealInvalidClientToken
|
||||
}
|
||||
|
||||
// Take lock to access and update the heal-sequence
|
||||
h.currentStatus.updateLock.Lock()
|
||||
defer h.currentStatus.updateLock.Unlock()
|
||||
|
||||
numItems := len(h.currentStatus.Items)
|
||||
|
||||
// calculate index of most recently available heal result
|
||||
// record.
|
||||
lastResultIndex := h.lastSentResultIndex
|
||||
if numItems > 0 {
|
||||
lastResultIndex = h.currentStatus.Items[numItems-1].ResultIndex
|
||||
}
|
||||
|
||||
// After sending status to client, and before relinquishing
|
||||
// the updateLock, reset Item to nil and record the result
|
||||
// index sent to the client.
|
||||
defer func(i int64) {
|
||||
h.lastSentResultIndex = i
|
||||
h.currentStatus.Items = nil
|
||||
}(lastResultIndex)
|
||||
|
||||
jbytes, err := json.Marshal(h.currentStatus)
|
||||
if err != nil {
|
||||
errorIf(err, "Failed to marshal heal result into json.")
|
||||
return nil, ErrInternalError
|
||||
}
|
||||
|
||||
return jbytes, ErrNone
|
||||
}
|
||||
|
||||
// healSequence - state for each heal sequence initiated on the
|
||||
// server.
|
||||
type healSequence struct {
|
||||
// bucket, and prefix on which heal seq. was initiated
|
||||
bucket, objPrefix string
|
||||
|
||||
// path is just bucket + "/" + objPrefix
|
||||
path string
|
||||
|
||||
// time at which heal sequence was started
|
||||
startTime time.Time
|
||||
|
||||
// Heal client info
|
||||
clientToken, clientAddress string
|
||||
|
||||
// was this heal sequence force started?
|
||||
forceStarted bool
|
||||
|
||||
// heal settings applied to this heal sequence
|
||||
settings madmin.HealOpts
|
||||
|
||||
// current accumulated status of the heal sequence
|
||||
currentStatus healSequenceStatus
|
||||
|
||||
// channel signalled by background routine when traversal has
|
||||
// completed
|
||||
traverseAndHealDoneCh chan error
|
||||
|
||||
// channel to signal heal sequence to stop (e.g. from the
|
||||
// heal-stop API)
|
||||
stopSignalCh chan struct{}
|
||||
|
||||
// the last result index sent to client
|
||||
lastSentResultIndex int64
|
||||
}
|
||||
|
||||
// NewHealSequence - creates healSettings, assumes bucket and
|
||||
// objPrefix are already validated.
|
||||
func newHealSequence(bucket, objPrefix, clientAddr string,
|
||||
numDisks int, hs madmin.HealOpts, forceStart bool) *healSequence {
|
||||
|
||||
return &healSequence{
|
||||
bucket: bucket,
|
||||
objPrefix: objPrefix,
|
||||
path: bucket + "/" + objPrefix,
|
||||
startTime: UTCNow(),
|
||||
clientToken: mustGetUUID(),
|
||||
clientAddress: clientAddr,
|
||||
forceStarted: forceStart,
|
||||
settings: hs,
|
||||
currentStatus: healSequenceStatus{
|
||||
Summary: healNotStartedStatus,
|
||||
HealSettings: hs,
|
||||
NumDisks: numDisks,
|
||||
updateLock: &sync.RWMutex{},
|
||||
},
|
||||
traverseAndHealDoneCh: make(chan error),
|
||||
stopSignalCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// isQuitting - determines if the heal sequence is quitting (due to an
|
||||
// external signal)
|
||||
func (h *healSequence) isQuitting() bool {
|
||||
select {
|
||||
case <-h.stopSignalCh:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// check if the heal sequence has ended
|
||||
func (h *healSequence) hasEnded() bool {
|
||||
h.currentStatus.updateLock.RLock()
|
||||
summary := h.currentStatus.Summary
|
||||
h.currentStatus.updateLock.RUnlock()
|
||||
return summary == healStoppedStatus || summary == healFinishedStatus
|
||||
}
|
||||
|
||||
// stops the heal sequence - safe to call multiple times.
|
||||
func (h *healSequence) stop() {
|
||||
select {
|
||||
case <-h.stopSignalCh:
|
||||
default:
|
||||
close(h.stopSignalCh)
|
||||
}
|
||||
}
|
||||
|
||||
// pushHealResultItem - pushes a heal result item for consumption in
|
||||
// the heal-status API. It blocks if there are
|
||||
// maxUnconsumedHealResultItems. When it blocks, the heal sequence
|
||||
// routine is effectively paused - this happens when the server has
|
||||
// accumulated the maximum number of heal records per heal
|
||||
// sequence. When the client consumes further records, the heal
|
||||
// sequence automatically resumes. The return value indicates if the
|
||||
// operation succeeded.
|
||||
func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
||||
|
||||
// start a timer to keep an upper time limit to find an empty
|
||||
// slot to add the given heal result - if no slot is found it
|
||||
// means that the server is holding the maximum amount of
|
||||
// heal-results in memory and the client has not consumed it
|
||||
// for too long.
|
||||
unconsumedTimer := time.NewTimer(healUnconsumedTimeout)
|
||||
defer func() {
|
||||
// stop the timeout timer so it is garbage collected.
|
||||
if !unconsumedTimer.Stop() {
|
||||
<-unconsumedTimer.C
|
||||
}
|
||||
}()
|
||||
|
||||
var itemsLen int
|
||||
for {
|
||||
h.currentStatus.updateLock.Lock()
|
||||
itemsLen = len(h.currentStatus.Items)
|
||||
if itemsLen == maxUnconsumedHealResultItems {
|
||||
// unlock and wait to check again if we can push
|
||||
h.currentStatus.updateLock.Unlock()
|
||||
|
||||
// wait for a second, or quit if an external
|
||||
// stop signal is received or the
|
||||
// unconsumedTimer fires.
|
||||
select {
|
||||
// Check after a second
|
||||
case <-time.After(time.Second):
|
||||
continue
|
||||
|
||||
case <-h.stopSignalCh:
|
||||
// discard result and return.
|
||||
return errHealPushStopNDiscard
|
||||
|
||||
// Timeout if no results consumed for too
|
||||
// long.
|
||||
case <-unconsumedTimer.C:
|
||||
return errHealIdleTimeout
|
||||
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Set the correct result index for the new result item
|
||||
if itemsLen > 0 {
|
||||
r.ResultIndex = 1 + h.currentStatus.Items[itemsLen-1].ResultIndex
|
||||
} else {
|
||||
r.ResultIndex = 1 + h.lastSentResultIndex
|
||||
}
|
||||
|
||||
// append to results
|
||||
h.currentStatus.Items = append(h.currentStatus.Items, r)
|
||||
|
||||
// release lock
|
||||
h.currentStatus.updateLock.Unlock()
|
||||
|
||||
// This is a "safe" point for the heal sequence to quit if
|
||||
// signalled externally.
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// healSequenceStart - this is the top-level background heal
|
||||
// routine. It launches another go-routine that actually traverses
|
||||
// on-disk data, checks and heals according to the selected
|
||||
// settings. This go-routine itself, (1) monitors the traversal
|
||||
// routine for completion, and (2) listens for external stop
|
||||
// signals. When either event happens, it sets the finish status for
|
||||
// the heal-sequence.
|
||||
func (h *healSequence) healSequenceStart() {
|
||||
// Set status as running
|
||||
h.currentStatus.updateLock.Lock()
|
||||
h.currentStatus.Summary = healRunningStatus
|
||||
h.currentStatus.StartTime = UTCNow()
|
||||
h.currentStatus.updateLock.Unlock()
|
||||
|
||||
go h.traverseAndHeal()
|
||||
|
||||
select {
|
||||
case err, ok := <-h.traverseAndHealDoneCh:
|
||||
h.currentStatus.updateLock.Lock()
|
||||
defer h.currentStatus.updateLock.Unlock()
|
||||
// Heal traversal is complete.
|
||||
if ok {
|
||||
// heal traversal had an error.
|
||||
h.currentStatus.Summary = healStoppedStatus
|
||||
h.currentStatus.FailureDetail = err.Error()
|
||||
} else {
|
||||
// heal traversal succeeded.
|
||||
h.currentStatus.Summary = healFinishedStatus
|
||||
}
|
||||
|
||||
case <-h.stopSignalCh:
|
||||
h.currentStatus.updateLock.Lock()
|
||||
h.currentStatus.Summary = healStoppedStatus
|
||||
h.currentStatus.FailureDetail = errHealStopSignalled.Error()
|
||||
h.currentStatus.updateLock.Unlock()
|
||||
|
||||
// drain traverse channel so the traversal
|
||||
// go-routine does not leak.
|
||||
go func() {
|
||||
// Eventually the traversal go-routine closes
|
||||
// the channel and returns, so this go-routine
|
||||
// itself will not leak.
|
||||
<-h.traverseAndHealDoneCh
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// traverseAndHeal - traverses on-disk data and performs healing
|
||||
// according to settings. At each "safe" point it also checks if an
|
||||
// external quit signal has been received and quits if so. Since the
|
||||
// healing traversal may be mutating on-disk data when an external
|
||||
// quit signal is received, this routine cannot quit immediately and
|
||||
// has to wait until a safe point is reached, such as between scanning
|
||||
// two objects.
|
||||
func (h *healSequence) traverseAndHeal() {
|
||||
var err error
|
||||
checkErr := func(f func() error) {
|
||||
switch {
|
||||
case err != nil:
|
||||
return
|
||||
case h.isQuitting():
|
||||
err = errHealStopSignalled
|
||||
return
|
||||
}
|
||||
err = f()
|
||||
}
|
||||
|
||||
// Start with format healing
|
||||
checkErr(h.healDiskFormat)
|
||||
|
||||
// Heal buckets and objects
|
||||
checkErr(h.healBuckets)
|
||||
|
||||
if err != nil {
|
||||
h.traverseAndHealDoneCh <- err
|
||||
}
|
||||
|
||||
close(h.traverseAndHealDoneCh)
|
||||
}
|
||||
|
||||
// healDiskFormat - heals format.json, return value indicates if a
|
||||
// failure error occurred.
|
||||
func (h *healSequence) healDiskFormat() error {
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
res, err := objectAPI.HealFormat(h.settings.DryRun)
|
||||
if err != nil {
|
||||
return errFnHealFromAPIErr(err)
|
||||
}
|
||||
|
||||
peersReInitFormat(globalAdminPeers, h.settings.DryRun)
|
||||
|
||||
// Push format heal result
|
||||
return h.pushHealResultItem(res)
|
||||
}
|
||||
|
||||
// healBuckets - check for all buckets heal or just particular bucket.
|
||||
func (h *healSequence) healBuckets() error {
|
||||
// 1. If a bucket was specified, heal only the bucket.
|
||||
if h.bucket != "" {
|
||||
return h.healBucket(h.bucket)
|
||||
}
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
buckets, err := objectAPI.ListBucketsHeal()
|
||||
if err != nil {
|
||||
return errFnHealFromAPIErr(err)
|
||||
}
|
||||
|
||||
for _, bucket := range buckets {
|
||||
if err = h.healBucket(bucket.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// healBucket - traverses and heals given bucket
|
||||
func (h *healSequence) healBucket(bucket string) error {
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
}
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
if err := bucketLock.GetLock(globalHealingTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results, err := objectAPI.HealBucket(bucket, h.settings.DryRun)
|
||||
// push any available results before checking for error
|
||||
for _, result := range results {
|
||||
if perr := h.pushHealResultItem(result); perr != nil {
|
||||
bucketLock.Unlock()
|
||||
return perr
|
||||
}
|
||||
}
|
||||
bucketLock.Unlock()
|
||||
// handle heal-bucket error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !h.settings.Recursive {
|
||||
if h.objPrefix != "" {
|
||||
// Check if an object named as the objPrefix exists,
|
||||
// and if so heal it.
|
||||
_, err = objectAPI.GetObjectInfo(bucket, h.objPrefix)
|
||||
if err == nil {
|
||||
err = h.healObject(bucket, h.objPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
marker := ""
|
||||
isTruncated := true
|
||||
for isTruncated {
|
||||
objectInfos, err := objectAPI.ListObjectsHeal(bucket,
|
||||
h.objPrefix, marker, "", 1000)
|
||||
if err != nil {
|
||||
return errFnHealFromAPIErr(err)
|
||||
}
|
||||
|
||||
for _, o := range objectInfos.Objects {
|
||||
if err := h.healObject(o.Bucket, o.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
isTruncated = objectInfos.IsTruncated
|
||||
marker = objectInfos.NextMarker
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// healObject - heal the given object and record result
|
||||
func (h *healSequence) healObject(bucket, object string) error {
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
}
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
hri, err := objectAPI.HealObject(bucket, object, h.settings.DryRun)
|
||||
if err != nil {
|
||||
hri.Detail = err.Error()
|
||||
}
|
||||
return h.pushHealResultItem(hri)
|
||||
}
|
||||
+20
-33
@@ -16,15 +16,7 @@
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
router "github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const (
|
||||
adminAPIPathPrefix = "/minio/admin"
|
||||
)
|
||||
import router "github.com/gorilla/mux"
|
||||
|
||||
// adminAPIHandlers provides HTTP handlers for Minio admin API.
|
||||
type adminAPIHandlers struct {
|
||||
@@ -35,44 +27,39 @@ func registerAdminRouter(mux *router.Router) {
|
||||
|
||||
adminAPI := adminAPIHandlers{}
|
||||
// Admin router
|
||||
adminRouter := mux.NewRoute().PathPrefix(adminAPIPathPrefix).Subrouter()
|
||||
|
||||
// Version handler
|
||||
adminRouter.Methods(http.MethodGet).Path("/version").HandlerFunc(adminAPI.VersionHandler)
|
||||
|
||||
adminV1Router := adminRouter.PathPrefix("/v1").Subrouter()
|
||||
adminRouter := mux.NewRoute().PathPrefix("/").Subrouter()
|
||||
|
||||
/// Service operations
|
||||
|
||||
// Service status
|
||||
adminV1Router.Methods(http.MethodGet).Path("/service").HandlerFunc(adminAPI.ServiceStatusHandler)
|
||||
adminRouter.Methods("GET").Queries("service", "").Headers(minioAdminOpHeader, "status").HandlerFunc(adminAPI.ServiceStatusHandler)
|
||||
|
||||
// Service restart and stop - TODO
|
||||
adminV1Router.Methods(http.MethodPost).Path("/service").HandlerFunc(adminAPI.ServiceStopNRestartHandler)
|
||||
// Service restart
|
||||
adminRouter.Methods("POST").Queries("service", "").Headers(minioAdminOpHeader, "restart").HandlerFunc(adminAPI.ServiceRestartHandler)
|
||||
// Service update credentials
|
||||
adminRouter.Methods("POST").Queries("service", "").Headers(minioAdminOpHeader, "set-credentials").HandlerFunc(adminAPI.ServiceCredentialsHandler)
|
||||
|
||||
// Info operations
|
||||
adminV1Router.Methods(http.MethodGet).Path("/info").HandlerFunc(adminAPI.ServerInfoHandler)
|
||||
adminRouter.Methods("GET").Queries("info", "").HandlerFunc(adminAPI.ServerInfoHandler)
|
||||
|
||||
/// Lock operations
|
||||
|
||||
// List Locks
|
||||
adminV1Router.Methods(http.MethodGet).Path("/locks").HandlerFunc(adminAPI.ListLocksHandler)
|
||||
adminRouter.Methods("GET").Queries("lock", "").Headers(minioAdminOpHeader, "list").HandlerFunc(adminAPI.ListLocksHandler)
|
||||
// Clear locks
|
||||
adminV1Router.Methods(http.MethodDelete).Path("/locks").HandlerFunc(adminAPI.ClearLocksHandler)
|
||||
adminRouter.Methods("POST").Queries("lock", "").Headers(minioAdminOpHeader, "clear").HandlerFunc(adminAPI.ClearLocksHandler)
|
||||
|
||||
/// Heal operations
|
||||
|
||||
// Heal processing endpoint.
|
||||
adminV1Router.Methods(http.MethodPost).Path("/heal/").HandlerFunc(adminAPI.HealHandler)
|
||||
adminV1Router.Methods(http.MethodPost).Path("/heal/{bucket}").HandlerFunc(adminAPI.HealHandler)
|
||||
adminV1Router.Methods(http.MethodPost).Path("/heal/{bucket}/{prefix:.*}").HandlerFunc(adminAPI.HealHandler)
|
||||
// List Objects needing heal.
|
||||
adminRouter.Methods("GET").Queries("heal", "").Headers(minioAdminOpHeader, "list-objects").HandlerFunc(adminAPI.ListObjectsHealHandler)
|
||||
// List Buckets needing heal.
|
||||
adminRouter.Methods("GET").Queries("heal", "").Headers(minioAdminOpHeader, "list-buckets").HandlerFunc(adminAPI.ListBucketsHealHandler)
|
||||
|
||||
/// Config operations
|
||||
|
||||
// Update credentials
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(adminAPI.UpdateCredentialsHandler)
|
||||
// Get config
|
||||
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(adminAPI.GetConfigHandler)
|
||||
// Set config
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(adminAPI.SetConfigHandler)
|
||||
// Heal Buckets.
|
||||
adminRouter.Methods("POST").Queries("heal", "").Headers(minioAdminOpHeader, "bucket").HandlerFunc(adminAPI.HealBucketHandler)
|
||||
// Heal Objects.
|
||||
adminRouter.Methods("POST").Queries("heal", "").Headers(minioAdminOpHeader, "object").HandlerFunc(adminAPI.HealObjectHandler)
|
||||
// Heal Format.
|
||||
adminRouter.Methods("POST").Queries("heal", "").Headers(minioAdminOpHeader, "format").HandlerFunc(adminAPI.HealFormatHandler)
|
||||
}
|
||||
|
||||
+103
-408
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2014, 2015, 2016, 2017 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2014-2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,29 +17,11 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// Admin service names
|
||||
signalServiceRPC = "Admin.SignalService"
|
||||
reInitFormatRPC = "Admin.ReInitFormat"
|
||||
listLocksRPC = "Admin.ListLocks"
|
||||
serverInfoDataRPC = "Admin.ServerInfoData"
|
||||
getConfigRPC = "Admin.GetConfig"
|
||||
writeTmpConfigRPC = "Admin.WriteTmpConfig"
|
||||
commitConfigRPC = "Admin.CommitConfig"
|
||||
)
|
||||
|
||||
// localAdminClient - represents admin operation to be executed locally.
|
||||
@@ -55,266 +37,144 @@ type remoteAdminClient struct {
|
||||
// adminCmdRunner - abstracts local and remote execution of admin
|
||||
// commands like service stop and service restart.
|
||||
type adminCmdRunner interface {
|
||||
SignalService(s serviceSignal) error
|
||||
ReInitFormat(dryRun bool) error
|
||||
Restart() error
|
||||
ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error)
|
||||
ServerInfoData() (ServerInfoData, error)
|
||||
GetConfig() ([]byte, error)
|
||||
WriteTmpConfig(tmpFileName string, configBytes []byte) error
|
||||
CommitConfig(tmpFileName string) error
|
||||
ReInitDisks() error
|
||||
Uptime() (time.Duration, error)
|
||||
}
|
||||
|
||||
var errUnsupportedSignal = fmt.Errorf("unsupported signal: only restart and stop signals are supported")
|
||||
|
||||
// SignalService - sends a restart or stop signal to the local server
|
||||
func (lc localAdminClient) SignalService(s serviceSignal) error {
|
||||
switch s {
|
||||
case serviceRestart, serviceStop:
|
||||
globalServiceSignalCh <- s
|
||||
default:
|
||||
return errUnsupportedSignal
|
||||
}
|
||||
// Restart - Sends a message over channel to the go-routine
|
||||
// responsible for restarting the process.
|
||||
func (lc localAdminClient) Restart() error {
|
||||
globalServiceSignalCh <- serviceRestart
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReInitFormat - re-initialize disk format.
|
||||
func (lc localAdminClient) ReInitFormat(dryRun bool) error {
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
_, err := objectAPI.HealFormat(dryRun)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListLocks - Fetches lock information from local lock instrumentation.
|
||||
func (lc localAdminClient) ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
|
||||
return listLocksInfo(bucket, prefix, duration), nil
|
||||
}
|
||||
|
||||
func (rc remoteAdminClient) SignalService(s serviceSignal) (err error) {
|
||||
switch s {
|
||||
case serviceRestart, serviceStop:
|
||||
reply := AuthRPCReply{}
|
||||
err = rc.Call(signalServiceRPC, &SignalServiceArgs{Sig: s},
|
||||
&reply)
|
||||
default:
|
||||
err = errUnsupportedSignal
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ReInitFormat - re-initialize disk format, remotely.
|
||||
func (rc remoteAdminClient) ReInitFormat(dryRun bool) error {
|
||||
// Restart - Sends restart command to remote server via RPC.
|
||||
func (rc remoteAdminClient) Restart() error {
|
||||
args := AuthRPCArgs{}
|
||||
reply := AuthRPCReply{}
|
||||
return rc.Call(reInitFormatRPC, &ReInitFormatArgs{
|
||||
DryRun: dryRun,
|
||||
}, &reply)
|
||||
return rc.Call("Admin.Restart", &args, &reply)
|
||||
}
|
||||
|
||||
// ListLocks - Sends list locks command to remote server via RPC.
|
||||
func (rc remoteAdminClient) ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
|
||||
listArgs := ListLocksQuery{
|
||||
Bucket: bucket,
|
||||
Prefix: prefix,
|
||||
Duration: duration,
|
||||
bucket: bucket,
|
||||
prefix: prefix,
|
||||
duration: duration,
|
||||
}
|
||||
var reply ListLocksReply
|
||||
if err := rc.Call(listLocksRPC, &listArgs, &reply); err != nil {
|
||||
if err := rc.Call("Admin.ListLocks", &listArgs, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reply.VolLocks, nil
|
||||
return reply.volLocks, nil
|
||||
}
|
||||
|
||||
// ServerInfoData - Returns the server info of this server.
|
||||
func (lc localAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
|
||||
// ReInitDisks - There is nothing to do here, heal format REST API
|
||||
// handler has already formatted and reinitialized the local disks.
|
||||
func (lc localAdminClient) ReInitDisks() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReInitDisks - Signals peers via RPC to reinitialize their disks and
|
||||
// object layer.
|
||||
func (rc remoteAdminClient) ReInitDisks() error {
|
||||
args := AuthRPCArgs{}
|
||||
reply := AuthRPCReply{}
|
||||
return rc.Call("Admin.ReInitDisks", &args, &reply)
|
||||
}
|
||||
|
||||
// Uptime - Returns the uptime of this server. Timestamp is taken
|
||||
// after object layer is initialized.
|
||||
func (lc localAdminClient) Uptime() (time.Duration, error) {
|
||||
if globalBootTime.IsZero() {
|
||||
return sid, errServerNotInitialized
|
||||
return time.Duration(0), errServerNotInitialized
|
||||
}
|
||||
|
||||
// Build storage info
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
return sid, errServerNotInitialized
|
||||
}
|
||||
storage := objLayer.StorageInfo()
|
||||
|
||||
var arns []string
|
||||
for queueArn := range globalEventNotifier.GetAllExternalTargets() {
|
||||
arns = append(arns, queueArn)
|
||||
}
|
||||
|
||||
return ServerInfoData{
|
||||
StorageInfo: storage,
|
||||
ConnStats: globalConnStats.toServerConnStats(),
|
||||
HTTPStats: globalHTTPStats.toServerHTTPStats(),
|
||||
Properties: ServerProperties{
|
||||
Uptime: UTCNow().Sub(globalBootTime),
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
SQSARN: arns,
|
||||
Region: globalServerConfig.GetRegion(),
|
||||
},
|
||||
}, nil
|
||||
return time.Now().UTC().Sub(globalBootTime), nil
|
||||
}
|
||||
|
||||
// ServerInfo - returns the server info of the server to which the RPC call is made.
|
||||
func (rc remoteAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
|
||||
// Uptime - returns the uptime of the server to which the RPC call is made.
|
||||
func (rc remoteAdminClient) Uptime() (time.Duration, error) {
|
||||
args := AuthRPCArgs{}
|
||||
reply := ServerInfoDataReply{}
|
||||
err := rc.Call(serverInfoDataRPC, &args, &reply)
|
||||
reply := UptimeReply{}
|
||||
err := rc.Call("Admin.Uptime", &args, &reply)
|
||||
if err != nil {
|
||||
return sid, err
|
||||
return time.Duration(0), err
|
||||
}
|
||||
|
||||
return reply.ServerInfoData, nil
|
||||
return reply.Uptime, nil
|
||||
}
|
||||
|
||||
// GetConfig - returns config.json of the local server.
|
||||
func (lc localAdminClient) GetConfig() ([]byte, error) {
|
||||
if globalServerConfig == nil {
|
||||
return nil, fmt.Errorf("config not present")
|
||||
}
|
||||
|
||||
return json.Marshal(globalServerConfig)
|
||||
}
|
||||
|
||||
// GetConfig - returns config.json of the remote server.
|
||||
func (rc remoteAdminClient) GetConfig() ([]byte, error) {
|
||||
args := AuthRPCArgs{}
|
||||
reply := ConfigReply{}
|
||||
if err := rc.Call(getConfigRPC, &args, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reply.Config, nil
|
||||
}
|
||||
|
||||
// WriteTmpConfig - writes config file content to a temporary file on
|
||||
// the local server.
|
||||
func (lc localAdminClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
|
||||
return writeTmpConfigCommon(tmpFileName, configBytes)
|
||||
}
|
||||
|
||||
// WriteTmpConfig - writes config file content to a temporary file on
|
||||
// a remote node.
|
||||
func (rc remoteAdminClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
|
||||
wArgs := WriteConfigArgs{
|
||||
TmpFileName: tmpFileName,
|
||||
Buf: configBytes,
|
||||
}
|
||||
|
||||
err := rc.Call(writeTmpConfigRPC, &wArgs, &WriteConfigReply{})
|
||||
if err != nil {
|
||||
errorIf(err, "Failed to write temporary config file.")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommitConfig - Move the new config in tmpFileName onto config.json
|
||||
// on a local node.
|
||||
func (lc localAdminClient) CommitConfig(tmpFileName string) error {
|
||||
configFile := getConfigFile()
|
||||
tmpConfigFile := filepath.Join(getConfigDir(), tmpFileName)
|
||||
|
||||
err := os.Rename(tmpConfigFile, configFile)
|
||||
errorIf(err, fmt.Sprintf("Failed to rename %s to %s", tmpConfigFile, configFile))
|
||||
return err
|
||||
}
|
||||
|
||||
// CommitConfig - Move the new config in tmpFileName onto config.json
|
||||
// on a remote node.
|
||||
func (rc remoteAdminClient) CommitConfig(tmpFileName string) error {
|
||||
cArgs := CommitConfigArgs{
|
||||
FileName: tmpFileName,
|
||||
}
|
||||
cReply := CommitConfigReply{}
|
||||
err := rc.Call(commitConfigRPC, &cArgs, &cReply)
|
||||
if err != nil {
|
||||
errorIf(err, "Failed to rename config file.")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// adminPeer - represents an entity that implements admin API RPCs.
|
||||
// adminPeer - represents an entity that implements Restart methods.
|
||||
type adminPeer struct {
|
||||
addr string
|
||||
cmdRunner adminCmdRunner
|
||||
isLocal bool
|
||||
}
|
||||
|
||||
// type alias for a collection of adminPeer.
|
||||
type adminPeers []adminPeer
|
||||
|
||||
// makeAdminPeers - helper function to construct a collection of adminPeer.
|
||||
func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
|
||||
thisPeer := globalMinioAddr
|
||||
if globalMinioHost == "" {
|
||||
thisPeer = net.JoinHostPort("localhost", globalMinioPort)
|
||||
}
|
||||
adminPeerList = append(adminPeerList, adminPeer{
|
||||
thisPeer,
|
||||
localAdminClient{},
|
||||
true,
|
||||
})
|
||||
func makeAdminPeers(eps []*url.URL) adminPeers {
|
||||
var servicePeers []adminPeer
|
||||
|
||||
hostSet := set.CreateStringSet(globalMinioAddr)
|
||||
cred := globalServerConfig.GetCredential()
|
||||
serviceEndpoint := path.Join(minioReservedBucketPath, adminPath)
|
||||
for _, host := range GetRemotePeers(endpoints) {
|
||||
if hostSet.Contains(host) {
|
||||
// map to store peers that are already added to ret
|
||||
seenAddr := make(map[string]bool)
|
||||
|
||||
// add local (self) as peer in the array
|
||||
servicePeers = append(servicePeers, adminPeer{
|
||||
globalMinioAddr,
|
||||
localAdminClient{},
|
||||
})
|
||||
seenAddr[globalMinioAddr] = true
|
||||
|
||||
serverCred := serverConfig.GetCredential()
|
||||
// iterate over endpoints to find new remote peers and add
|
||||
// them to ret.
|
||||
for _, ep := range eps {
|
||||
if ep.Host == "" {
|
||||
continue
|
||||
}
|
||||
hostSet.Add(host)
|
||||
adminPeerList = append(adminPeerList, adminPeer{
|
||||
addr: host,
|
||||
cmdRunner: &remoteAdminClient{newAuthRPCClient(authConfig{
|
||||
accessKey: cred.AccessKey,
|
||||
secretKey: cred.SecretKey,
|
||||
serverAddr: host,
|
||||
serviceEndpoint: serviceEndpoint,
|
||||
|
||||
// Check if the remote host has been added already
|
||||
if !seenAddr[ep.Host] {
|
||||
cfg := authConfig{
|
||||
accessKey: serverCred.AccessKey,
|
||||
secretKey: serverCred.SecretKey,
|
||||
serverAddr: ep.Host,
|
||||
secureConn: globalIsSSL,
|
||||
serviceEndpoint: path.Join(reservedBucket, adminPath),
|
||||
serviceName: "Admin",
|
||||
})},
|
||||
})
|
||||
}
|
||||
|
||||
return adminPeerList
|
||||
}
|
||||
|
||||
// peersReInitFormat - reinitialize remote object layers to new format.
|
||||
func peersReInitFormat(peers adminPeers, dryRun bool) error {
|
||||
errs := make([]error, len(peers))
|
||||
|
||||
// Send ReInitFormat RPC call to all nodes.
|
||||
// for local adminPeer this is a no-op.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
if !peer.isLocal {
|
||||
errs[idx] = peer.cmdRunner.ReInitFormat(dryRun)
|
||||
}
|
||||
}(i, peer)
|
||||
|
||||
servicePeers = append(servicePeers, adminPeer{
|
||||
addr: ep.Host,
|
||||
cmdRunner: &remoteAdminClient{newAuthRPCClient(cfg)},
|
||||
})
|
||||
seenAddr[ep.Host] = true
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
return nil
|
||||
|
||||
return servicePeers
|
||||
}
|
||||
|
||||
// Initialize global adminPeer collection.
|
||||
func initGlobalAdminPeers(endpoints EndpointList) {
|
||||
globalAdminPeers = makeAdminPeers(endpoints)
|
||||
func initGlobalAdminPeers(eps []*url.URL) {
|
||||
globalAdminPeers = makeAdminPeers(eps)
|
||||
}
|
||||
|
||||
// invokeServiceCmd - Invoke Restart/Stop command.
|
||||
// invokeServiceCmd - Invoke Restart command.
|
||||
func invokeServiceCmd(cp adminPeer, cmd serviceSignal) (err error) {
|
||||
switch cmd {
|
||||
case serviceRestart, serviceStop:
|
||||
err = cp.cmdRunner.SignalService(cmd)
|
||||
case serviceRestart:
|
||||
err = cp.cmdRunner.Restart()
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -388,6 +248,24 @@ func listPeerLocksInfo(peers adminPeers, bucket, prefix string, duration time.Du
|
||||
return groupedLockInfos, nil
|
||||
}
|
||||
|
||||
// reInitPeerDisks - reinitialize disks and object layer on peer servers to use the new format.
|
||||
func reInitPeerDisks(peers adminPeers) error {
|
||||
errs := make([]error, len(peers))
|
||||
|
||||
// Send ReInitDisks RPC call to all nodes.
|
||||
// for local adminPeer this is a no-op.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
errs[idx] = peer.cmdRunner.ReInitDisks()
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// uptimeSlice - used to sort uptimes in chronological order.
|
||||
type uptimeSlice []struct {
|
||||
err error
|
||||
@@ -413,7 +291,7 @@ func getPeerUptimes(peers adminPeers) (time.Duration, error) {
|
||||
// the setup is the uptime of the single minio server
|
||||
// instance.
|
||||
if !globalIsDistXL {
|
||||
return UTCNow().Sub(globalBootTime), nil
|
||||
return time.Now().UTC().Sub(globalBootTime), nil
|
||||
}
|
||||
|
||||
uptimes := make(uptimeSlice, len(peers))
|
||||
@@ -424,8 +302,7 @@ func getPeerUptimes(peers adminPeers) (time.Duration, error) {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
serverInfoData, rpcErr := peer.cmdRunner.ServerInfoData()
|
||||
uptimes[idx].uptime, uptimes[idx].err = serverInfoData.Properties.Uptime, rpcErr
|
||||
uptimes[idx].uptime, uptimes[idx].err = peer.cmdRunner.Uptime()
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -459,185 +336,3 @@ func getPeerUptimes(peers adminPeers) (time.Duration, error) {
|
||||
|
||||
return latestUptime, nil
|
||||
}
|
||||
|
||||
// getPeerConfig - Fetches config.json from all nodes in the setup and
|
||||
// returns the one that occurs in a majority of them.
|
||||
func getPeerConfig(peers adminPeers) ([]byte, error) {
|
||||
if !globalIsDistXL {
|
||||
return peers[0].cmdRunner.GetConfig()
|
||||
}
|
||||
|
||||
errs := make([]error, len(peers))
|
||||
configs := make([][]byte, len(peers))
|
||||
|
||||
// Get config from all servers.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
configs[idx], errs[idx] = peer.cmdRunner.GetConfig()
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Find the maximally occurring config among peers in a
|
||||
// distributed setup.
|
||||
|
||||
serverConfigs := make([]serverConfig, len(peers))
|
||||
for i, configBytes := range configs {
|
||||
if errs[i] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Unmarshal the received config files.
|
||||
err := json.Unmarshal(configBytes, &serverConfigs[i])
|
||||
if err != nil {
|
||||
errorIf(err, "Failed to unmarshal serverConfig from ", peers[i].addr)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
configJSON, err := getValidServerConfig(serverConfigs, errs)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to find a valid server config")
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
|
||||
// Return the config.json that was present quorum or more
|
||||
// number of disks.
|
||||
return json.Marshal(configJSON)
|
||||
}
|
||||
|
||||
// getValidServerConfig - finds the server config that is present in
|
||||
// quorum or more number of servers.
|
||||
func getValidServerConfig(serverConfigs []serverConfig, errs []error) (scv serverConfig, e error) {
|
||||
// majority-based quorum
|
||||
quorum := len(serverConfigs)/2 + 1
|
||||
|
||||
// Count the number of disks a config.json was found in.
|
||||
configCounter := make([]int, len(serverConfigs))
|
||||
|
||||
// We group equal serverConfigs by the lowest index of the
|
||||
// same value; e.g, let us take the following serverConfigs
|
||||
// in a 4-node setup,
|
||||
// serverConfigs == [c1, c2, c1, c1]
|
||||
// configCounter == [3, 1, 0, 0]
|
||||
// c1, c2 are the only distinct values that appear. c1 is
|
||||
// identified by 0, the lowest index it appears in and c2 is
|
||||
// identified by 1. So, we need to find the number of times
|
||||
// each of these distinct values occur.
|
||||
|
||||
// Invariants:
|
||||
|
||||
// 1. At the beginning of the i-th iteration, the number of
|
||||
// unique configurations seen so far is equal to the number of
|
||||
// non-zero counter values in config[:i].
|
||||
|
||||
// 2. At the beginning of the i-th iteration, the sum of
|
||||
// elements of configCounter[:i] is equal to the number of
|
||||
// non-error configurations seen so far.
|
||||
|
||||
// For each of the serverConfig ...
|
||||
for i := range serverConfigs {
|
||||
// Skip nodes where getConfig failed.
|
||||
if errs[i] != nil {
|
||||
continue
|
||||
}
|
||||
// Check if it is equal to any of the configurations
|
||||
// seen so far. If j == i is reached then we have an
|
||||
// unseen configuration.
|
||||
for j := 0; j <= i; j++ {
|
||||
if j < i && configCounter[j] == 0 {
|
||||
// serverConfigs[j] is known to be
|
||||
// equal to a value that was already
|
||||
// seen. See example above for
|
||||
// clarity.
|
||||
continue
|
||||
} else if j < i && serverConfigs[i].ConfigDiff(&serverConfigs[j]) == "" {
|
||||
// serverConfigs[i] is equal to
|
||||
// serverConfigs[j], update
|
||||
// serverConfigs[j]'s counter since it
|
||||
// is the lower index.
|
||||
configCounter[j]++
|
||||
break
|
||||
} else if j == i {
|
||||
// serverConfigs[i] is equal to no
|
||||
// other value seen before. It is
|
||||
// unique so far.
|
||||
configCounter[i] = 1
|
||||
break
|
||||
} // else invariants specified above are violated.
|
||||
}
|
||||
}
|
||||
|
||||
// We find the maximally occurring server config and check if
|
||||
// there is quorum.
|
||||
var configJSON serverConfig
|
||||
maxOccurrence := 0
|
||||
for i, count := range configCounter {
|
||||
if maxOccurrence < count {
|
||||
maxOccurrence = count
|
||||
configJSON = serverConfigs[i]
|
||||
}
|
||||
}
|
||||
|
||||
// If quorum nodes don't agree.
|
||||
if maxOccurrence < quorum {
|
||||
return scv, errXLWriteQuorum
|
||||
}
|
||||
|
||||
return configJSON, nil
|
||||
}
|
||||
|
||||
// Write config contents into a temporary file on all nodes.
|
||||
func writeTmpConfigPeers(peers adminPeers, tmpFileName string, configBytes []byte) []error {
|
||||
// For a single-node minio server setup.
|
||||
if !globalIsDistXL {
|
||||
err := peers[0].cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
errs := make([]error, len(peers))
|
||||
|
||||
// Write config into temporary file on all nodes.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
errs[idx] = peer.cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Return bytes written and errors (if any) during writing
|
||||
// temporary config file.
|
||||
return errs
|
||||
}
|
||||
|
||||
// Move config contents from the given temporary file onto config.json
|
||||
// on all nodes.
|
||||
func commitConfigPeers(peers adminPeers, tmpFileName string) []error {
|
||||
// For a single-node minio server setup.
|
||||
if !globalIsDistXL {
|
||||
return []error{peers[0].cmdRunner.CommitConfig(tmpFileName)}
|
||||
}
|
||||
|
||||
errs := make([]error, len(peers))
|
||||
|
||||
// Rename temporary config file into configDir/config.json on
|
||||
// all nodes.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
errs[idx] = peer.cmdRunner.CommitConfig(tmpFileName)
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Return errors (if any) received during rename.
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -1,260 +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 (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
config1 = []byte(`{
|
||||
"version": "13",
|
||||
"credential": {
|
||||
"accessKey": "minio",
|
||||
"secretKey": "minio123"
|
||||
},
|
||||
"region": "us-east-1",
|
||||
"logger": {
|
||||
"console": {
|
||||
"enable": true,
|
||||
"level": "debug"
|
||||
},
|
||||
"file": {
|
||||
"enable": false,
|
||||
"fileName": "",
|
||||
"level": ""
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"amqp": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"exchange": "",
|
||||
"routingKey": "",
|
||||
"exchangeType": "",
|
||||
"mandatory": false,
|
||||
"immediate": false,
|
||||
"durable": false,
|
||||
"internal": false,
|
||||
"noWait": false,
|
||||
"autoDeleted": false
|
||||
}
|
||||
},
|
||||
"nats": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"subject": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"token": "",
|
||||
"secure": false,
|
||||
"pingInterval": 0,
|
||||
"streaming": {
|
||||
"enable": false,
|
||||
"clusterID": "",
|
||||
"clientID": "",
|
||||
"async": false,
|
||||
"maxPubAcksInflight": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"elasticsearch": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"index": ""
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"password": "",
|
||||
"key": ""
|
||||
}
|
||||
},
|
||||
"postgresql": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"connectionString": "",
|
||||
"table": "",
|
||||
"host": "",
|
||||
"port": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"database": ""
|
||||
}
|
||||
},
|
||||
"kafka": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"brokers": null,
|
||||
"topic": ""
|
||||
}
|
||||
},
|
||||
"webhook": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"endpoint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
// diff from config1 - amqp.Enable is True
|
||||
config2 = []byte(`{
|
||||
"version": "13",
|
||||
"credential": {
|
||||
"accessKey": "minio",
|
||||
"secretKey": "minio123"
|
||||
},
|
||||
"region": "us-east-1",
|
||||
"logger": {
|
||||
"console": {
|
||||
"enable": true,
|
||||
"level": "debug"
|
||||
},
|
||||
"file": {
|
||||
"enable": false,
|
||||
"fileName": "",
|
||||
"level": ""
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"amqp": {
|
||||
"1": {
|
||||
"enable": true,
|
||||
"url": "",
|
||||
"exchange": "",
|
||||
"routingKey": "",
|
||||
"exchangeType": "",
|
||||
"mandatory": false,
|
||||
"immediate": false,
|
||||
"durable": false,
|
||||
"internal": false,
|
||||
"noWait": false,
|
||||
"autoDeleted": false
|
||||
}
|
||||
},
|
||||
"nats": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"subject": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"token": "",
|
||||
"secure": false,
|
||||
"pingInterval": 0,
|
||||
"streaming": {
|
||||
"enable": false,
|
||||
"clusterID": "",
|
||||
"clientID": "",
|
||||
"async": false,
|
||||
"maxPubAcksInflight": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"elasticsearch": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"index": ""
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"password": "",
|
||||
"key": ""
|
||||
}
|
||||
},
|
||||
"postgresql": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"connectionString": "",
|
||||
"table": "",
|
||||
"host": "",
|
||||
"port": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"database": ""
|
||||
}
|
||||
},
|
||||
"kafka": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"brokers": null,
|
||||
"topic": ""
|
||||
}
|
||||
},
|
||||
"webhook": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"endpoint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
)
|
||||
|
||||
// TestGetValidServerConfig - test for getValidServerConfig.
|
||||
func TestGetValidServerConfig(t *testing.T) {
|
||||
var c1, c2 serverConfig
|
||||
err := json.Unmarshal(config1, &c1)
|
||||
if err != nil {
|
||||
t.Fatalf("json unmarshal of %s failed: %v", string(config1), err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(config2, &c2)
|
||||
if err != nil {
|
||||
t.Fatalf("json unmarshal of %s failed: %v", string(config2), err)
|
||||
}
|
||||
|
||||
// Valid config.
|
||||
noErrs := []error{nil, nil, nil, nil}
|
||||
serverConfigs := []serverConfig{c1, c2, c1, c1}
|
||||
validConfig, err := getValidServerConfig(serverConfigs, noErrs)
|
||||
if err != nil {
|
||||
t.Errorf("Expected a valid config but received %v instead", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(validConfig, c1) {
|
||||
t.Errorf("Expected valid config to be %v but received %v", config1, validConfig)
|
||||
}
|
||||
|
||||
// Invalid config - no quorum.
|
||||
serverConfigs = []serverConfig{c1, c2, c2, c1}
|
||||
_, err = getValidServerConfig(serverConfigs, noErrs)
|
||||
if err != errXLWriteQuorum {
|
||||
t.Errorf("Expected to fail due to lack of quorum but received %v", err)
|
||||
}
|
||||
|
||||
// All errors
|
||||
allErrs := []error{errDiskNotFound, errDiskNotFound, errDiskNotFound, errDiskNotFound}
|
||||
serverConfigs = []serverConfig{{}, {}, {}, {}}
|
||||
_, err = getValidServerConfig(serverConfigs, allErrs)
|
||||
if err != errXLWriteQuorum {
|
||||
t.Errorf("Expected to fail due to lack of quorum but received %v", err)
|
||||
}
|
||||
}
|
||||
+65
-146
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,94 +17,102 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"errors"
|
||||
"net/rpc"
|
||||
"time"
|
||||
|
||||
router "github.com/gorilla/mux"
|
||||
"github.com/minio/minio/pkg/errors"
|
||||
)
|
||||
|
||||
const adminPath = "/admin"
|
||||
|
||||
var errUnsupportedBackend = errors.New("not supported for non erasure-code backend")
|
||||
|
||||
// adminCmd - exports RPC methods for service status, stop and
|
||||
// restart commands.
|
||||
type adminCmd struct {
|
||||
AuthRPCServer
|
||||
}
|
||||
|
||||
// SignalServiceArgs - provides the signal argument to SignalService RPC
|
||||
type SignalServiceArgs struct {
|
||||
AuthRPCArgs
|
||||
Sig serviceSignal
|
||||
}
|
||||
|
||||
// ListLocksQuery - wraps ListLocks API's query values to send over RPC.
|
||||
type ListLocksQuery struct {
|
||||
AuthRPCArgs
|
||||
Bucket string
|
||||
Prefix string
|
||||
Duration time.Duration
|
||||
bucket string
|
||||
prefix string
|
||||
duration time.Duration
|
||||
}
|
||||
|
||||
// ListLocksReply - wraps ListLocks response over RPC.
|
||||
type ListLocksReply struct {
|
||||
AuthRPCReply
|
||||
VolLocks []VolumeLockInfo
|
||||
volLocks []VolumeLockInfo
|
||||
}
|
||||
|
||||
// ServerInfoDataReply - wraps the server info response over RPC.
|
||||
type ServerInfoDataReply struct {
|
||||
// UptimeReply - wraps the uptime response over RPC.
|
||||
type UptimeReply struct {
|
||||
AuthRPCReply
|
||||
ServerInfoData ServerInfoData
|
||||
Uptime time.Duration
|
||||
}
|
||||
|
||||
// ConfigReply - wraps the server config response over RPC.
|
||||
type ConfigReply struct {
|
||||
AuthRPCReply
|
||||
Config []byte // json-marshalled bytes of serverConfigV13
|
||||
}
|
||||
|
||||
// SignalService - Send a restart or stop signal to the service
|
||||
func (s *adminCmd) SignalService(args *SignalServiceArgs, reply *AuthRPCReply) error {
|
||||
// Restart - Restart this instance of minio server.
|
||||
func (s *adminCmd) Restart(args *AuthRPCArgs, reply *AuthRPCReply) error {
|
||||
if err := args.IsAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
globalServiceSignalCh <- args.Sig
|
||||
globalServiceSignalCh <- serviceRestart
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReInitFormatArgs - provides dry-run information to re-initialize format.json
|
||||
type ReInitFormatArgs struct {
|
||||
AuthRPCArgs
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// ReInitFormat - re-init 'format.json'
|
||||
func (s *adminCmd) ReInitFormat(args *ReInitFormatArgs, reply *AuthRPCReply) error {
|
||||
if err := args.IsAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := newObjectLayerFn().HealFormat(args.DryRun)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListLocks - lists locks held by requests handled by this server instance.
|
||||
func (s *adminCmd) ListLocks(query *ListLocksQuery, reply *ListLocksReply) error {
|
||||
if err := query.IsAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
volLocks := listLocksInfo(query.Bucket, query.Prefix, query.Duration)
|
||||
*reply = ListLocksReply{VolLocks: volLocks}
|
||||
volLocks := listLocksInfo(query.bucket, query.prefix, query.duration)
|
||||
*reply = ListLocksReply{volLocks: volLocks}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerInfo - returns the server info when object layer was initialized on this server.
|
||||
func (s *adminCmd) ServerInfoData(args *AuthRPCArgs, reply *ServerInfoDataReply) error {
|
||||
// ReInitDisk - reinitialize storage disks and object layer to use the
|
||||
// new format.
|
||||
func (s *adminCmd) ReInitDisks(args *AuthRPCArgs, reply *AuthRPCReply) error {
|
||||
if err := args.IsAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !globalIsXL {
|
||||
return errUnsupportedBackend
|
||||
}
|
||||
|
||||
// Get the current object layer instance.
|
||||
objLayer := newObjectLayerFn()
|
||||
|
||||
// Initialize new disks to include the newly formatted disks.
|
||||
bootstrapDisks, err := initStorageDisks(globalEndpoints)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize new object layer with newly formatted disks.
|
||||
newObjectAPI, err := newXLObjects(bootstrapDisks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Replace object layer with newly formatted storage.
|
||||
globalObjLayerMutex.Lock()
|
||||
globalObjectAPI = newObjectAPI
|
||||
globalObjLayerMutex.Unlock()
|
||||
|
||||
// Shutdown storage belonging to old object layer instance.
|
||||
objLayer.Shutdown()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Uptime - returns the time when object layer was initialized on this server.
|
||||
func (s *adminCmd) Uptime(args *AuthRPCArgs, reply *UptimeReply) error {
|
||||
if err := args.IsAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -113,116 +121,27 @@ func (s *adminCmd) ServerInfoData(args *AuthRPCArgs, reply *ServerInfoDataReply)
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
// Build storage info
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
storageInfo := objLayer.StorageInfo()
|
||||
|
||||
var arns []string
|
||||
for queueArn := range globalEventNotifier.GetAllExternalTargets() {
|
||||
arns = append(arns, queueArn)
|
||||
}
|
||||
|
||||
reply.ServerInfoData = ServerInfoData{
|
||||
Properties: ServerProperties{
|
||||
Uptime: UTCNow().Sub(globalBootTime),
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
Region: globalServerConfig.GetRegion(),
|
||||
SQSARN: arns,
|
||||
},
|
||||
StorageInfo: storageInfo,
|
||||
ConnStats: globalConnStats.toServerConnStats(),
|
||||
HTTPStats: globalHTTPStats.toServerHTTPStats(),
|
||||
// N B The uptime is computed assuming that the system time is
|
||||
// monotonic. This is not the case in time pkg in Go, see
|
||||
// https://github.com/golang/go/issues/12914. This is expected
|
||||
// to be fixed by go1.9.
|
||||
*reply = UptimeReply{
|
||||
Uptime: time.Now().UTC().Sub(globalBootTime),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig - returns the config.json of this server.
|
||||
func (s *adminCmd) GetConfig(args *AuthRPCArgs, reply *ConfigReply) error {
|
||||
if err := args.IsAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if globalServerConfig == nil {
|
||||
return fmt.Errorf("config not present")
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(globalServerConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reply.Config = jsonBytes
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteConfigArgs - wraps the bytes to be written and temporary file name.
|
||||
type WriteConfigArgs struct {
|
||||
AuthRPCArgs
|
||||
TmpFileName string
|
||||
Buf []byte
|
||||
}
|
||||
|
||||
// WriteConfigReply - wraps the result of a writing config into a temporary file.
|
||||
// the remote node.
|
||||
type WriteConfigReply struct {
|
||||
AuthRPCReply
|
||||
}
|
||||
|
||||
func writeTmpConfigCommon(tmpFileName string, configBytes []byte) error {
|
||||
tmpConfigFile := filepath.Join(getConfigDir(), tmpFileName)
|
||||
err := ioutil.WriteFile(tmpConfigFile, configBytes, 0666)
|
||||
errorIf(err, fmt.Sprintf("Failed to write to temporary config file %s", tmpConfigFile))
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteTmpConfig - writes the supplied config contents onto the
|
||||
// supplied temporary file.
|
||||
func (s *adminCmd) WriteTmpConfig(wArgs *WriteConfigArgs, wReply *WriteConfigReply) error {
|
||||
if err := wArgs.IsAuthenticated(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeTmpConfigCommon(wArgs.TmpFileName, wArgs.Buf)
|
||||
}
|
||||
|
||||
// CommitConfigArgs - wraps the config file name that needs to be
|
||||
// committed into config.json on this node.
|
||||
type CommitConfigArgs struct {
|
||||
AuthRPCArgs
|
||||
FileName string
|
||||
}
|
||||
|
||||
// CommitConfigReply - represents response to commit of config file on
|
||||
// this node.
|
||||
type CommitConfigReply struct {
|
||||
AuthRPCReply
|
||||
}
|
||||
|
||||
// CommitConfig - Renames the temporary file into config.json on this node.
|
||||
func (s *adminCmd) CommitConfig(cArgs *CommitConfigArgs, cReply *CommitConfigReply) error {
|
||||
configFile := getConfigFile()
|
||||
tmpConfigFile := filepath.Join(getConfigDir(), cArgs.FileName)
|
||||
|
||||
err := os.Rename(tmpConfigFile, configFile)
|
||||
errorIf(err, fmt.Sprintf("Failed to rename %s to %s", tmpConfigFile, configFile))
|
||||
return err
|
||||
}
|
||||
|
||||
// registerAdminRPCRouter - registers RPC methods for service status,
|
||||
// stop and restart commands.
|
||||
func registerAdminRPCRouter(mux *router.Router) error {
|
||||
adminRPCHandler := &adminCmd{}
|
||||
adminRPCServer := newRPCServer()
|
||||
adminRPCServer := rpc.NewServer()
|
||||
err := adminRPCServer.RegisterName("Admin", adminRPCHandler)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
return traceError(err)
|
||||
}
|
||||
adminRouter := mux.NewRoute().PathPrefix(minioReservedBucketPath).Subrouter()
|
||||
adminRouter := mux.NewRoute().PathPrefix(reservedBucket).Subrouter()
|
||||
adminRouter.Path(adminPath).Handler(adminRPCServer)
|
||||
return nil
|
||||
}
|
||||
|
||||
+53
-169
@@ -17,35 +17,32 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func testAdminCmd(cmd cmdType, t *testing.T) {
|
||||
// reset globals. this is to make sure that the tests are not
|
||||
// affected by modified globals.
|
||||
// reset globals.
|
||||
// this is to make sure that the tests are not affected by modified globals.
|
||||
resetTestGlobals()
|
||||
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test config - %v", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
creds := globalServerConfig.GetCredential()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
|
||||
adminServer := adminCmd{}
|
||||
creds := serverConfig.GetCredential()
|
||||
args := LoginRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
RequestTime: time.Now().UTC(),
|
||||
}
|
||||
err = adminServer.Login(&args, &LoginRPCReply{})
|
||||
reply := LoginRPCReply{}
|
||||
err = adminServer.Login(&args, &reply)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login to admin server - %v", err)
|
||||
}
|
||||
@@ -55,23 +52,12 @@ func testAdminCmd(cmd cmdType, t *testing.T) {
|
||||
<-globalServiceSignalCh
|
||||
}()
|
||||
|
||||
sa := SignalServiceArgs{
|
||||
AuthRPCArgs: AuthRPCArgs{AuthToken: token, Version: globalRPCAPIVersion},
|
||||
Sig: cmd.toServiceSignal(),
|
||||
}
|
||||
|
||||
ga := AuthRPCArgs{AuthToken: reply.AuthToken, RequestTime: time.Now().UTC()}
|
||||
genReply := AuthRPCReply{}
|
||||
switch cmd {
|
||||
case restartCmd, stopCmd:
|
||||
if err = adminServer.SignalService(&sa, &genReply); err != nil {
|
||||
t.Errorf("restartCmd/stopCmd: Expected: <nil>, got: %v",
|
||||
err)
|
||||
}
|
||||
default:
|
||||
err = adminServer.SignalService(&sa, &genReply)
|
||||
if err != nil && err.Error() != errUnsupportedSignal.Error() {
|
||||
t.Errorf("invalidSignal %s: unexpected error got: %v",
|
||||
cmd, err)
|
||||
case restartCmd:
|
||||
if err = adminServer.Restart(&ga, &genReply); err != nil {
|
||||
t.Errorf("restartCmd: Expected: <nil>, got: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,18 +67,8 @@ func TestAdminRestart(t *testing.T) {
|
||||
testAdminCmd(restartCmd, t)
|
||||
}
|
||||
|
||||
// TestAdminStop - test for Admin.Stop RPC service.
|
||||
func TestAdminStop(t *testing.T) {
|
||||
testAdminCmd(stopCmd, t)
|
||||
}
|
||||
|
||||
// TestAdminStatus - test for Admin.Status RPC service (error case)
|
||||
func TestAdminStatus(t *testing.T) {
|
||||
testAdminCmd(statusCmd, t)
|
||||
}
|
||||
|
||||
// TestReInitFormat - test for Admin.ReInitFormat RPC service.
|
||||
func TestReInitFormat(t *testing.T) {
|
||||
// TestReInitDisks - test for Admin.ReInitDisks RPC service.
|
||||
func TestReInitDisks(t *testing.T) {
|
||||
// Reset global variables to start afresh.
|
||||
resetTestGlobals()
|
||||
|
||||
@@ -100,7 +76,7 @@ func TestReInitFormat(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize server config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
defer removeAll(rootPath)
|
||||
|
||||
// Initializing objectLayer for HealFormatHandler.
|
||||
_, xlDirs, xlErr := initTestXLObjLayer()
|
||||
@@ -110,153 +86,61 @@ func TestReInitFormat(t *testing.T) {
|
||||
defer removeRoots(xlDirs)
|
||||
|
||||
// Set globalEndpoints for a single node XL setup.
|
||||
globalEndpoints = mustGetNewEndpointList(xlDirs...)
|
||||
for _, xlDir := range xlDirs {
|
||||
globalEndpoints = append(globalEndpoints, &url.URL{Path: xlDir})
|
||||
}
|
||||
|
||||
// Setup admin rpc server for an XL backend.
|
||||
globalIsXL = true
|
||||
adminServer := adminCmd{}
|
||||
|
||||
creds := globalServerConfig.GetCredential()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
creds := serverConfig.GetCredential()
|
||||
args := LoginRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
RequestTime: time.Now().UTC(),
|
||||
}
|
||||
err = adminServer.Login(&args, &LoginRPCReply{})
|
||||
reply := LoginRPCReply{}
|
||||
err = adminServer.Login(&args, &reply)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login to admin server - %v", err)
|
||||
}
|
||||
|
||||
authArgs := AuthRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: globalRPCAPIVersion,
|
||||
AuthToken: reply.AuthToken,
|
||||
RequestTime: time.Now().UTC(),
|
||||
}
|
||||
authReply := AuthRPCReply{}
|
||||
|
||||
err = adminServer.ReInitFormat(&ReInitFormatArgs{
|
||||
AuthRPCArgs: authArgs,
|
||||
DryRun: false,
|
||||
}, &authReply)
|
||||
err = adminServer.ReInitDisks(&authArgs, &authReply)
|
||||
if err != nil {
|
||||
t.Errorf("Expected to pass, but failed with %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetConfig - Test for GetConfig admin RPC.
|
||||
func TestGetConfig(t *testing.T) {
|
||||
// Reset global variables to start afresh.
|
||||
resetTestGlobals()
|
||||
|
||||
rootPath, err := newTestConfig("us-east-1")
|
||||
// Negative test case with admin rpc server setup for FS.
|
||||
globalIsXL = false
|
||||
fsAdminServer := adminCmd{}
|
||||
fsArgs := LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
RequestTime: time.Now().UTC(),
|
||||
}
|
||||
fsReply := LoginRPCReply{}
|
||||
err = fsAdminServer.Login(&fsArgs, &fsReply)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize server config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
adminServer := adminCmd{}
|
||||
creds := globalServerConfig.GetCredential()
|
||||
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatalf("Failed to login to fs admin server - %v", err)
|
||||
}
|
||||
|
||||
args := LoginRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
authArgs = AuthRPCArgs{
|
||||
AuthToken: fsReply.AuthToken,
|
||||
RequestTime: time.Now().UTC(),
|
||||
}
|
||||
reply := LoginRPCReply{}
|
||||
err = adminServer.Login(&args, &reply)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login to admin server - %v", err)
|
||||
}
|
||||
|
||||
authArgs := AuthRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: globalRPCAPIVersion,
|
||||
}
|
||||
|
||||
configReply := ConfigReply{}
|
||||
|
||||
err = adminServer.GetConfig(&authArgs, &configReply)
|
||||
if err != nil {
|
||||
t.Errorf("Expected GetConfig to pass but failed with %v", err)
|
||||
}
|
||||
|
||||
var config serverConfigV13
|
||||
err = json.Unmarshal(configReply.Config, &config)
|
||||
if err != nil {
|
||||
t.Errorf("Expected json unmarshal to pass but failed with %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteAndCommitConfig - test for WriteTmpConfig and CommitConfig
|
||||
// RPC handler.
|
||||
func TestWriteAndCommitConfig(t *testing.T) {
|
||||
// Reset global variables to start afresh.
|
||||
resetTestGlobals()
|
||||
|
||||
rootPath, err := newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize server config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
adminServer := adminCmd{}
|
||||
creds := globalServerConfig.GetCredential()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
args := LoginRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
reply := LoginRPCReply{}
|
||||
err = adminServer.Login(&args, &reply)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to login to admin server - %v", err)
|
||||
}
|
||||
|
||||
// Write temporary config.
|
||||
buf := []byte("hello")
|
||||
tmpFileName := mustGetUUID()
|
||||
wArgs := WriteConfigArgs{
|
||||
AuthRPCArgs: AuthRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: globalRPCAPIVersion,
|
||||
},
|
||||
TmpFileName: tmpFileName,
|
||||
Buf: buf,
|
||||
}
|
||||
|
||||
err = adminServer.WriteTmpConfig(&wArgs, &WriteConfigReply{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write temporary config %v", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected to succeed but failed %v", err)
|
||||
}
|
||||
|
||||
cArgs := CommitConfigArgs{
|
||||
AuthRPCArgs: AuthRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: globalRPCAPIVersion,
|
||||
},
|
||||
FileName: tmpFileName,
|
||||
}
|
||||
cReply := CommitConfigReply{}
|
||||
|
||||
err = adminServer.CommitConfig(&cArgs, &cReply)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to commit config file %v", err)
|
||||
authReply = AuthRPCReply{}
|
||||
// Attempt ReInitDisks service on a FS backend.
|
||||
err = fsAdminServer.ReInitDisks(&authArgs, &authReply)
|
||||
if err != errUnsupportedBackend {
|
||||
t.Errorf("Expected to fail with %v, but received %v",
|
||||
errUnsupportedBackend, err)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-289
@@ -18,12 +18,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/errors"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
// APIError structure
|
||||
@@ -41,8 +36,8 @@ type APIErrorResponse struct {
|
||||
Key string
|
||||
BucketName string
|
||||
Resource string
|
||||
RequestID string `xml:"RequestId" json:"RequestId"`
|
||||
HostID string `xml:"HostId" json:"HostId"`
|
||||
RequestID string `xml:"RequestId"`
|
||||
HostID string `xml:"HostId"`
|
||||
}
|
||||
|
||||
// APIErrorCode type of error status.
|
||||
@@ -61,8 +56,6 @@ const (
|
||||
ErrInvalidBucketName
|
||||
ErrInvalidDigest
|
||||
ErrInvalidRange
|
||||
ErrInvalidCopyPartRange
|
||||
ErrInvalidCopyPartRangeSource
|
||||
ErrInvalidMaxKeys
|
||||
ErrInvalidMaxUploads
|
||||
ErrInvalidMaxParts
|
||||
@@ -119,25 +112,8 @@ const (
|
||||
ErrInvalidQueryParams
|
||||
ErrBucketAlreadyOwnedByYou
|
||||
ErrInvalidDuration
|
||||
ErrBucketAlreadyExists
|
||||
ErrMetadataTooLarge
|
||||
ErrUnsupportedMetadata
|
||||
ErrMaximumExpires
|
||||
ErrSlowDown
|
||||
ErrInvalidPrefixMarker
|
||||
// Add new error codes here.
|
||||
|
||||
// Server-Side-Encryption (with Customer provided key) related API errors.
|
||||
ErrInsecureSSECustomerRequest
|
||||
ErrSSEMultipartEncrypted
|
||||
ErrSSEEncryptedObject
|
||||
ErrInvalidEncryptionParameters
|
||||
ErrInvalidSSECustomerAlgorithm
|
||||
ErrInvalidSSECustomerKey
|
||||
ErrMissingSSECustomerKey
|
||||
ErrMissingSSECustomerKeyMD5
|
||||
ErrSSECustomerKeyMD5Mismatch
|
||||
|
||||
// Bucket notification related errors.
|
||||
ErrEventNotification
|
||||
ErrARNNotification
|
||||
@@ -148,7 +124,6 @@ const (
|
||||
ErrFilterNameSuffix
|
||||
ErrFilterValueInvalid
|
||||
ErrOverlappingConfigs
|
||||
ErrUnsupportedNotification
|
||||
|
||||
// S3 extended errors.
|
||||
ErrContentSHA256Mismatch
|
||||
@@ -159,38 +134,16 @@ const (
|
||||
ErrReadQuorum
|
||||
ErrWriteQuorum
|
||||
ErrStorageFull
|
||||
ErrRequestBodyParse
|
||||
ErrObjectExistsAsDirectory
|
||||
ErrPolicyNesting
|
||||
ErrInvalidObjectName
|
||||
ErrInvalidResourceName
|
||||
ErrServerNotInitialized
|
||||
ErrOperationTimedOut
|
||||
ErrPartsSizeUnequal
|
||||
ErrInvalidRequest
|
||||
|
||||
// Minio storage class error codes
|
||||
ErrInvalidStorageClass
|
||||
|
||||
// Add new extended error codes here.
|
||||
// Please open a https://github.com/minio/minio/issues before adding
|
||||
// new error codes here.
|
||||
|
||||
ErrMalformedJSON
|
||||
ErrAdminInvalidAccessKey
|
||||
ErrAdminInvalidSecretKey
|
||||
ErrAdminConfigNoQuorum
|
||||
ErrAdminConfigTooLarge
|
||||
ErrAdminConfigBadJSON
|
||||
ErrAdminCredentialsMismatch
|
||||
ErrInsecureClientRequest
|
||||
ErrObjectTampered
|
||||
ErrHealNotImplemented
|
||||
ErrHealNoSuchProcess
|
||||
ErrHealInvalidClientToken
|
||||
ErrHealMissingBucket
|
||||
ErrHealAlreadyRunning
|
||||
ErrHealOverlappingPaths
|
||||
)
|
||||
|
||||
// error code to APIError structure, these fields carry respective
|
||||
@@ -211,11 +164,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Unknown metadata directive.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidStorageClass: {
|
||||
Code: "InvalidStorageClass",
|
||||
Description: "Invalid storage class.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidRequestBody: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Body shouldn't be set for this request.",
|
||||
@@ -277,7 +225,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
ErrInvalidAccessKeyID: {
|
||||
Code: "InvalidAccessKeyId",
|
||||
Code: "InvalidAccessKeyID",
|
||||
Description: "The access key ID you provided does not exist in our records.",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
@@ -363,7 +311,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
},
|
||||
ErrInvalidPart: {
|
||||
Code: "InvalidPart",
|
||||
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.",
|
||||
Description: "One or more of the specified parts could not be found.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidPartOrder: {
|
||||
@@ -401,11 +349,6 @@ 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.",
|
||||
@@ -528,17 +471,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Request is not valid yet",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
ErrSlowDown: {
|
||||
Code: "SlowDown",
|
||||
Description: "Please reduce your request",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrInvalidPrefixMarker: {
|
||||
Code: "InvalidPrefixMarker",
|
||||
Description: "Invalid marker prefix combination",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
// FIXME: Actual XML error response also contains the header which missed in list of signed header parameters.
|
||||
ErrUnsignedHeaders: {
|
||||
Code: "AccessDenied",
|
||||
@@ -607,71 +539,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Configurations overlap. Configurations on the same bucket cannot share a common event type.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrUnsupportedNotification: {
|
||||
Code: "UnsupportedNotification",
|
||||
Description: "Minio server does not support Topic or Cloud Function based notifications.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidCopyPartRange: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "The x-amz-copy-source-range value must be of the form bytes=first-last where first and last are the zero-based offsets of the first and last bytes to copy",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidCopyPartRangeSource: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Range specified is not valid for source object",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMetadataTooLarge: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Your metadata headers exceed the maximum allowed metadata size.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInsecureSSECustomerRequest: {
|
||||
Code: "InvalidRequest",
|
||||
Description: errInsecureSSERequest.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrSSEMultipartEncrypted: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "The multipart upload initiate requested encryption. Subsequent part requests must include the appropriate encryption parameters.",
|
||||
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: {
|
||||
@@ -683,14 +550,9 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
/// Minio extensions.
|
||||
ErrStorageFull: {
|
||||
Code: "XMinioStorageFull",
|
||||
Description: "Storage backend has reached its minimum free disk threshold. Please delete a few objects to proceed.",
|
||||
Description: "Storage backend has reached its minimum free disk threshold. Please delete few objects to proceed.",
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
ErrRequestBodyParse: {
|
||||
Code: "XMinioRequestBodyParse",
|
||||
Description: "The request body failed to parse.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrObjectExistsAsDirectory: {
|
||||
Code: "XMinioObjectExistsAsDirectory",
|
||||
Description: "Object name already exists as a directory.",
|
||||
@@ -713,12 +575,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
},
|
||||
ErrInvalidObjectName: {
|
||||
Code: "XMinioInvalidObjectName",
|
||||
Description: "Object name contains unsupported characters.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidResourceName: {
|
||||
Code: "XMinioInvalidResourceName",
|
||||
Description: "Resource name contains bad components such as \"..\" or \".\".",
|
||||
Description: "Object name contains unsupported characters. Unsupported characters are `^*|\\\"",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrServerNotInitialized: {
|
||||
@@ -726,11 +583,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Server not initialized, please try again.",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrMalformedJSON: {
|
||||
Code: "XMinioMalformedJSON",
|
||||
Description: "The JSON you provided was not well-formed or did not validate against our published format.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminInvalidAccessKey: {
|
||||
Code: "XMinioAdminInvalidAccessKey",
|
||||
Description: "The access key is invalid.",
|
||||
@@ -741,95 +593,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "The secret key is invalid.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminConfigNoQuorum: {
|
||||
Code: "XMinioAdminConfigNoQuorum",
|
||||
Description: "Configuration update failed because server quorum was not met",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrAdminConfigTooLarge: {
|
||||
Code: "XMinioAdminConfigTooLarge",
|
||||
Description: fmt.Sprintf("Configuration data provided exceeds the allowed maximum of %d bytes",
|
||||
maxConfigJSONSize),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminConfigBadJSON: {
|
||||
Code: "XMinioAdminConfigBadJSON",
|
||||
Description: "JSON configuration provided has objects with duplicate keys",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
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,
|
||||
},
|
||||
ErrHealNotImplemented: {
|
||||
Code: "XMinioHealNotImplemented",
|
||||
Description: "This server does not implement heal functionality.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrHealNoSuchProcess: {
|
||||
Code: "XMinioHealNoSuchProcess",
|
||||
Description: "No such heal process is running on the server",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrHealInvalidClientToken: {
|
||||
Code: "XMinioHealInvalidClientToken",
|
||||
Description: "Client token mismatch",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrHealMissingBucket: {
|
||||
Code: "XMinioHealMissingBucket",
|
||||
Description: "A heal start request with a non-empty object-prefix parameter requires a bucket to be specified.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrHealAlreadyRunning: {
|
||||
Code: "XMinioHealAlreadyRunning",
|
||||
Description: "",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrHealOverlappingPaths: {
|
||||
Code: "XMinioHealOverlappingPaths",
|
||||
Description: "",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
// Add your error structure here.
|
||||
}
|
||||
@@ -842,18 +605,20 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
err = errors.Cause(err)
|
||||
err = errorCause(err)
|
||||
// Verify if the underlying error is signature mismatch.
|
||||
switch err {
|
||||
case errSignatureMismatch:
|
||||
apiErr = ErrSignatureDoesNotMatch
|
||||
case errContentSHA256Mismatch:
|
||||
apiErr = ErrContentSHA256Mismatch
|
||||
case errDataTooLarge:
|
||||
apiErr = ErrEntityTooLarge
|
||||
case errDataTooSmall:
|
||||
apiErr = ErrEntityTooSmall
|
||||
case auth.ErrInvalidAccessKeyLength:
|
||||
case errInvalidAccessKeyLength:
|
||||
apiErr = ErrAdminInvalidAccessKey
|
||||
case auth.ErrInvalidSecretKeyLength:
|
||||
case errInvalidSecretKeyLength:
|
||||
apiErr = ErrAdminInvalidSecretKey
|
||||
}
|
||||
|
||||
@@ -862,34 +627,11 @@ 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 hash.BadDigest:
|
||||
case BadDigest:
|
||||
apiErr = ErrBadDigest
|
||||
case AllAccessDisabled:
|
||||
apiErr = ErrAllAccessDisabled
|
||||
case IncompleteBody:
|
||||
apiErr = ErrIncompleteBody
|
||||
case ObjectExistsAsDirectory:
|
||||
@@ -900,12 +642,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrInvalidBucketName
|
||||
case BucketNotFound:
|
||||
apiErr = ErrNoSuchBucket
|
||||
case BucketAlreadyOwnedByYou:
|
||||
apiErr = ErrBucketAlreadyOwnedByYou
|
||||
case BucketNotEmpty:
|
||||
apiErr = ErrBucketNotEmpty
|
||||
case BucketAlreadyExists:
|
||||
apiErr = ErrBucketAlreadyExists
|
||||
case BucketExists:
|
||||
apiErr = ErrBucketAlreadyOwnedByYou
|
||||
case ObjectNotFound:
|
||||
@@ -930,26 +668,12 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrNoSuchUpload
|
||||
case PartTooSmall:
|
||||
apiErr = ErrEntityTooSmall
|
||||
case SignatureDoesNotMatch:
|
||||
apiErr = ErrSignatureDoesNotMatch
|
||||
case hash.SHA256Mismatch:
|
||||
case SHA256Mismatch:
|
||||
apiErr = ErrContentSHA256Mismatch
|
||||
case ObjectTooLarge:
|
||||
apiErr = ErrEntityTooLarge
|
||||
case ObjectTooSmall:
|
||||
apiErr = ErrEntityTooSmall
|
||||
case NotImplemented:
|
||||
apiErr = ErrNotImplemented
|
||||
case PolicyNotFound:
|
||||
apiErr = ErrNoSuchBucketPolicy
|
||||
case PartTooBig:
|
||||
apiErr = ErrEntityTooLarge
|
||||
case UnsupportedMetadata:
|
||||
apiErr = ErrUnsupportedMetadata
|
||||
case PartsSizeUnequal:
|
||||
apiErr = ErrPartsSizeUnequal
|
||||
case BucketPolicyNotFound:
|
||||
apiErr = ErrNoSuchBucketPolicy
|
||||
default:
|
||||
apiErr = ErrInternalError
|
||||
}
|
||||
|
||||
+105
-43
@@ -19,52 +19,114 @@ 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) {
|
||||
for i, testCase := range toAPIErrorCodeTests {
|
||||
testCases := []struct {
|
||||
err error
|
||||
errCode APIErrorCode
|
||||
}{
|
||||
// Valid cases.
|
||||
{
|
||||
BadDigest{},
|
||||
ErrBadDigest,
|
||||
},
|
||||
{
|
||||
IncompleteBody{},
|
||||
ErrIncompleteBody,
|
||||
},
|
||||
{
|
||||
ObjectExistsAsDirectory{},
|
||||
ErrObjectExistsAsDirectory,
|
||||
},
|
||||
{
|
||||
BucketNameInvalid{},
|
||||
ErrInvalidBucketName,
|
||||
},
|
||||
{
|
||||
BucketExists{},
|
||||
ErrBucketAlreadyOwnedByYou,
|
||||
},
|
||||
{
|
||||
ObjectNotFound{},
|
||||
ErrNoSuchKey,
|
||||
},
|
||||
{
|
||||
ObjectNameInvalid{},
|
||||
ErrInvalidObjectName,
|
||||
},
|
||||
{
|
||||
InvalidUploadID{},
|
||||
ErrNoSuchUpload,
|
||||
},
|
||||
{
|
||||
InvalidPart{},
|
||||
ErrInvalidPart,
|
||||
},
|
||||
{
|
||||
InsufficientReadQuorum{},
|
||||
ErrReadQuorum,
|
||||
},
|
||||
{
|
||||
InsufficientWriteQuorum{},
|
||||
ErrWriteQuorum,
|
||||
},
|
||||
{
|
||||
UnsupportedDelimiter{},
|
||||
ErrNotImplemented,
|
||||
},
|
||||
{
|
||||
InvalidMarkerPrefixCombination{},
|
||||
ErrNotImplemented,
|
||||
},
|
||||
{
|
||||
InvalidUploadIDKeyCombination{},
|
||||
ErrNotImplemented,
|
||||
},
|
||||
{
|
||||
MalformedUploadID{},
|
||||
ErrNoSuchUpload,
|
||||
},
|
||||
{
|
||||
PartTooSmall{},
|
||||
ErrEntityTooSmall,
|
||||
},
|
||||
{
|
||||
BucketNotEmpty{},
|
||||
ErrBucketNotEmpty,
|
||||
},
|
||||
{
|
||||
BucketNotFound{},
|
||||
ErrNoSuchBucket,
|
||||
},
|
||||
{
|
||||
StorageFull{},
|
||||
ErrStorageFull,
|
||||
},
|
||||
{
|
||||
errSignatureMismatch,
|
||||
ErrSignatureDoesNotMatch,
|
||||
},
|
||||
{
|
||||
errContentSHA256Mismatch,
|
||||
ErrContentSHA256Mismatch,
|
||||
}, // End of all valid cases.
|
||||
|
||||
// Case where err is nil.
|
||||
{
|
||||
nil,
|
||||
ErrNone,
|
||||
},
|
||||
|
||||
// Case where err type is unknown.
|
||||
{
|
||||
errors.New("Custom error"),
|
||||
ErrInternalError,
|
||||
},
|
||||
}
|
||||
|
||||
// Validate all the errors with their API error codes.
|
||||
for i, testCase := range testCases {
|
||||
errCode := toAPIErrorCode(testCase.err)
|
||||
if errCode != testCase.errCode {
|
||||
t.Errorf("Test %d: Expected error code %d, got %d", i+1, testCase.errCode, errCode)
|
||||
|
||||
+4
-31
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2015,2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,7 +18,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -35,13 +34,8 @@ func mustGetRequestID(t time.Time) string {
|
||||
// Write http common headers
|
||||
func setCommonHeaders(w http.ResponseWriter) {
|
||||
// Set unique request ID for each reply.
|
||||
w.Header().Set(responseRequestIDKey, mustGetRequestID(UTCNow()))
|
||||
w.Header().Set(responseRequestIDKey, mustGetRequestID(time.Now().UTC()))
|
||||
w.Header().Set("Server", globalServerUserAgent)
|
||||
// Set `x-amz-bucket-region` only if region is set on the server
|
||||
// by default minio uses an empty region.
|
||||
if region := globalServerConfig.GetRegion(); region != "" {
|
||||
w.Header().Set("X-Amz-Bucket-Region", region)
|
||||
}
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
}
|
||||
|
||||
@@ -54,14 +48,6 @@ func encodeResponse(response interface{}) []byte {
|
||||
return bytesBuffer.Bytes()
|
||||
}
|
||||
|
||||
// Encodes the response headers into JSON format.
|
||||
func encodeResponseJSON(response interface{}) []byte {
|
||||
var bytesBuffer bytes.Buffer
|
||||
e := json.NewEncoder(&bytesBuffer)
|
||||
e.Encode(response)
|
||||
return bytesBuffer.Bytes()
|
||||
}
|
||||
|
||||
// Write object header
|
||||
func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *httpRange) {
|
||||
// set common headers
|
||||
@@ -75,25 +61,12 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *h
|
||||
w.Header().Set("Last-Modified", lastModified)
|
||||
|
||||
// Set Etag if available.
|
||||
if objInfo.ETag != "" {
|
||||
w.Header().Set("ETag", "\""+objInfo.ETag+"\"")
|
||||
}
|
||||
|
||||
if objInfo.ContentType != "" {
|
||||
w.Header().Set("Content-Type", objInfo.ContentType)
|
||||
}
|
||||
|
||||
if objInfo.ContentEncoding != "" {
|
||||
w.Header().Set("Content-Encoding", objInfo.ContentEncoding)
|
||||
if objInfo.MD5Sum != "" {
|
||||
w.Header().Set("ETag", "\""+objInfo.MD5Sum+"\"")
|
||||
}
|
||||
|
||||
// Set all other user defined metadata.
|
||||
for k, v := range objInfo.UserDefined {
|
||||
if hasPrefix(k, ReservedMetadataPrefix) {
|
||||
// Do not need to send any internal metadata
|
||||
// values to client.
|
||||
continue
|
||||
}
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,11 +18,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewRequestID(t *testing.T) {
|
||||
// Ensure that it returns an alphanumeric result of length 16.
|
||||
var id = mustGetRequestID(UTCNow())
|
||||
var id = mustGetRequestID(time.Now().UTC())
|
||||
|
||||
if len(id) != 16 {
|
||||
t.Fail()
|
||||
|
||||
+31
-77
@@ -22,8 +22,6 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -183,8 +181,9 @@ type CommonPrefix struct {
|
||||
|
||||
// Bucket container for bucket metadata
|
||||
type Bucket struct {
|
||||
Name string
|
||||
CreationDate string // time string of format "2006-01-02T15:04:05.000Z"
|
||||
Name string
|
||||
CreationDate string // time string of format "2006-01-02T15:04:05.000Z"
|
||||
HealBucketInfo *HealBucketInfo `xml:"HealBucketInfo,omitempty"`
|
||||
}
|
||||
|
||||
// Object container for object metadata
|
||||
@@ -198,7 +197,8 @@ type Object struct {
|
||||
Owner Owner
|
||||
|
||||
// The class of storage used to store the object.
|
||||
StorageClass string
|
||||
StorageClass string
|
||||
HealObjectInfo *HealObjectInfo `xml:"HealObjectInfo,omitempty"`
|
||||
}
|
||||
|
||||
// CopyObjectResponse container returns ETag and LastModified of the successfully copied object
|
||||
@@ -274,31 +274,9 @@ func getLocation(r *http.Request) string {
|
||||
return path.Clean(r.URL.Path) // Clean any trailing slashes.
|
||||
}
|
||||
|
||||
// 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(r *http.Request, domain, bucket, object string) string {
|
||||
proto := handlers.GetSourceScheme(r)
|
||||
if proto == "" {
|
||||
proto = getURLScheme(globalIsSSL)
|
||||
}
|
||||
u := &url.URL{
|
||||
Host: r.Host,
|
||||
Path: path.Join(slashSeparator, bucket, object),
|
||||
Scheme: proto,
|
||||
}
|
||||
// If domain is set then we need to use bucket DNS style.
|
||||
if domain != "" {
|
||||
u.Host = bucket + "." + domain
|
||||
u.Path = path.Join(slashSeparator, object)
|
||||
}
|
||||
return u.String()
|
||||
// getObjectLocation gets the relative URL for an object
|
||||
func getObjectLocation(bucketName string, key string) string {
|
||||
return "/" + bucketName + "/" + key
|
||||
}
|
||||
|
||||
// generates ListBucketsResponse from array of BucketInfo which can be
|
||||
@@ -309,10 +287,13 @@ 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.UTC().Format(timeFormatAMZLong)
|
||||
listbucket.CreationDate = bucket.Created.Format(timeFormatAMZLong)
|
||||
listbucket.HealBucketInfo = bucket.HealBucketInfo
|
||||
listbuckets = append(listbuckets, listbucket)
|
||||
}
|
||||
|
||||
@@ -330,6 +311,8 @@ 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 == "" {
|
||||
@@ -337,12 +320,14 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max
|
||||
}
|
||||
content.Key = object.Name
|
||||
content.LastModified = object.ModTime.UTC().Format(timeFormatAMZLong)
|
||||
if object.ETag != "" {
|
||||
content.ETag = "\"" + object.ETag + "\""
|
||||
if object.MD5Sum != "" {
|
||||
content.ETag = "\"" + object.MD5Sum + "\""
|
||||
}
|
||||
content.Size = object.Size
|
||||
content.StorageClass = globalMinioDefaultStorageClass
|
||||
content.Owner = owner
|
||||
// object.HealObjectInfo is non-empty only when resp is constructed in ListObjectsHeal.
|
||||
content.HealObjectInfo = object.HealObjectInfo
|
||||
contents = append(contents, content)
|
||||
}
|
||||
// TODO - support EncodingType in xml decoding
|
||||
@@ -366,25 +351,26 @@ 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, nextToken, startAfter, delimiter string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string) ListObjectsV2Response {
|
||||
func generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter string, fetchOwner bool, maxKeys int, resp ListObjectsInfo) ListObjectsV2Response {
|
||||
var contents []Object
|
||||
var commonPrefixes []CommonPrefix
|
||||
var prefixes []CommonPrefix
|
||||
var owner = Owner{}
|
||||
var data = ListObjectsV2Response{}
|
||||
|
||||
if fetchOwner {
|
||||
owner.ID = globalMinioDefaultOwnerID
|
||||
owner.DisplayName = globalMinioDefaultOwnerID
|
||||
}
|
||||
|
||||
for _, object := range objects {
|
||||
for _, object := range resp.Objects {
|
||||
var content = Object{}
|
||||
if object.Name == "" {
|
||||
continue
|
||||
}
|
||||
content.Key = object.Name
|
||||
content.LastModified = object.ModTime.UTC().Format(timeFormatAMZLong)
|
||||
if object.ETag != "" {
|
||||
content.ETag = "\"" + object.ETag + "\""
|
||||
if object.MD5Sum != "" {
|
||||
content.ETag = "\"" + object.MD5Sum + "\""
|
||||
}
|
||||
content.Size = object.Size
|
||||
content.StorageClass = globalMinioDefaultStorageClass
|
||||
@@ -400,14 +386,14 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
|
||||
data.Prefix = prefix
|
||||
data.MaxKeys = maxKeys
|
||||
data.ContinuationToken = token
|
||||
data.NextContinuationToken = nextToken
|
||||
data.IsTruncated = isTruncated
|
||||
for _, prefix := range prefixes {
|
||||
data.NextContinuationToken = resp.NextMarker
|
||||
data.IsTruncated = resp.IsTruncated
|
||||
for _, prefix := range resp.Prefixes {
|
||||
var prefixItem = CommonPrefix{}
|
||||
prefixItem.Prefix = prefix
|
||||
commonPrefixes = append(commonPrefixes, prefixItem)
|
||||
prefixes = append(prefixes, prefixItem)
|
||||
}
|
||||
data.CommonPrefixes = commonPrefixes
|
||||
data.CommonPrefixes = prefixes
|
||||
data.KeyCount = len(data.Contents) + len(data.CommonPrefixes)
|
||||
return data
|
||||
}
|
||||
@@ -456,7 +442,9 @@ 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
|
||||
@@ -568,12 +556,6 @@ func writeSuccessResponseHeadersOnly(w http.ResponseWriter) {
|
||||
|
||||
// writeErrorRespone writes error headers
|
||||
func writeErrorResponse(w http.ResponseWriter, errorCode APIErrorCode, reqURL *url.URL) {
|
||||
switch errorCode {
|
||||
case ErrSlowDown, ErrServerNotInitialized, ErrReadQuorum, ErrWriteQuorum:
|
||||
// Set retry-after header to indicate user-agents to retry request after 120secs.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
||||
w.Header().Set("Retry-After", "120")
|
||||
}
|
||||
apiError := getAPIError(errorCode)
|
||||
// Generate error response.
|
||||
errorResponse := getAPIErrorResponse(apiError, reqURL.Path)
|
||||
@@ -585,31 +567,3 @@ func writeErrorResponseHeadersOnly(w http.ResponseWriter, errorCode APIErrorCode
|
||||
apiError := getAPIError(errorCode)
|
||||
writeResponse(w, apiError.HTTPStatusCode, nil, mimeNone)
|
||||
}
|
||||
|
||||
// writeErrorResponseJSON - writes error response in JSON format;
|
||||
// useful for admin APIs.
|
||||
func writeErrorResponseJSON(w http.ResponseWriter, errorCode APIErrorCode, reqURL *url.URL) {
|
||||
apiError := getAPIError(errorCode)
|
||||
// Generate error response.
|
||||
errorResponse := getAPIErrorResponse(apiError, reqURL.Path)
|
||||
encodedErrorResponse := encodeResponseJSON(errorResponse)
|
||||
writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeJSON)
|
||||
}
|
||||
|
||||
// writeCustomErrorResponseJSON - similar to writeErrorResponseJSON,
|
||||
// but accepts the error message directly (this allows messages to be
|
||||
// dynamically generated.)
|
||||
func writeCustomErrorResponseJSON(w http.ResponseWriter, errorCode APIErrorCode,
|
||||
errBody string, reqURL *url.URL) {
|
||||
|
||||
apiError := getAPIError(errorCode)
|
||||
errorResponse := APIErrorResponse{
|
||||
Code: apiError.Code,
|
||||
Message: errBody,
|
||||
Resource: reqURL.Path,
|
||||
RequestID: "3L137",
|
||||
HostID: "3L137",
|
||||
}
|
||||
encodedErrorResponse := encodeResponseJSON(errorResponse)
|
||||
writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeJSON)
|
||||
}
|
||||
|
||||
@@ -1,121 +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 (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests object location.
|
||||
func TestObjectLocation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
request *http.Request
|
||||
bucket, object string
|
||||
domain string
|
||||
expectedLocation string
|
||||
}{
|
||||
// Server binding to localhost IP with https.
|
||||
{
|
||||
request: &http.Request{
|
||||
Host: "127.0.0.1:9000",
|
||||
Header: map[string][]string{
|
||||
"X-Forwarded-Scheme": {httpScheme},
|
||||
},
|
||||
},
|
||||
bucket: "testbucket1",
|
||||
object: "test/1.txt",
|
||||
expectedLocation: "http://127.0.0.1:9000/testbucket1/test/1.txt",
|
||||
},
|
||||
{
|
||||
request: &http.Request{
|
||||
Host: "127.0.0.1:9000",
|
||||
Header: map[string][]string{
|
||||
"X-Forwarded-Scheme": {httpsScheme},
|
||||
},
|
||||
},
|
||||
bucket: "testbucket1",
|
||||
object: "test/1.txt",
|
||||
expectedLocation: "https://127.0.0.1:9000/testbucket1/test/1.txt",
|
||||
},
|
||||
// Server binding to fqdn.
|
||||
{
|
||||
request: &http.Request{
|
||||
Host: "s3.mybucket.org",
|
||||
Header: map[string][]string{
|
||||
"X-Forwarded-Scheme": {httpScheme},
|
||||
},
|
||||
},
|
||||
bucket: "mybucket",
|
||||
object: "test/1.txt",
|
||||
expectedLocation: "http://s3.mybucket.org/mybucket/test/1.txt",
|
||||
},
|
||||
// Server binding to fqdn.
|
||||
{
|
||||
request: &http.Request{
|
||||
Host: "mys3.mybucket.org",
|
||||
Header: map[string][]string{},
|
||||
},
|
||||
bucket: "mybucket",
|
||||
object: "test/1.txt",
|
||||
expectedLocation: "http://mys3.mybucket.org/mybucket/test/1.txt",
|
||||
},
|
||||
// Server with virtual domain name.
|
||||
{
|
||||
request: &http.Request{
|
||||
Host: "mys3.bucket.org",
|
||||
Header: map[string][]string{},
|
||||
},
|
||||
domain: "mys3.bucket.org",
|
||||
bucket: "mybucket",
|
||||
object: "test/1.txt",
|
||||
expectedLocation: "http://mybucket.mys3.bucket.org/test/1.txt",
|
||||
},
|
||||
{
|
||||
request: &http.Request{
|
||||
Host: "mys3.bucket.org",
|
||||
Header: map[string][]string{
|
||||
"X-Forwarded-Scheme": {httpsScheme},
|
||||
},
|
||||
},
|
||||
domain: "mys3.bucket.org",
|
||||
bucket: "mybucket",
|
||||
object: "test/1.txt",
|
||||
expectedLocation: "https://mybucket.mys3.bucket.org/test/1.txt",
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
gotLocation := getObjectLocation(testCase.request, testCase.domain, 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)
|
||||
}
|
||||
}
|
||||
+60
-66
@@ -17,7 +17,6 @@
|
||||
package cmd
|
||||
|
||||
import router "github.com/gorilla/mux"
|
||||
import "net/http"
|
||||
|
||||
// objectAPIHandler implements and provides http handlers for S3 API.
|
||||
type objectAPIHandlers struct {
|
||||
@@ -33,75 +32,70 @@ 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())
|
||||
|
||||
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))
|
||||
// Bucket router
|
||||
bucket := apiRouter.PathPrefix("/{bucket}").Subrouter()
|
||||
|
||||
/// 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))
|
||||
}
|
||||
/// 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)
|
||||
|
||||
/// Root operation
|
||||
|
||||
// ListBuckets
|
||||
apiRouter.Methods("GET").Path("/").HandlerFunc(httpTraceAll(api.ListBucketsHandler))
|
||||
|
||||
// If none of the routes match.
|
||||
apiRouter.NotFoundHandler = http.HandlerFunc(httpTraceAll(notFoundHandler))
|
||||
apiRouter.Methods("GET").HandlerFunc(api.ListBucketsHandler)
|
||||
}
|
||||
|
||||
+43
-56
@@ -18,14 +18,16 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
)
|
||||
|
||||
// Verify if the request http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD"
|
||||
func isRequestUnsignedPayload(r *http.Request) bool {
|
||||
return r.Header.Get("x-amz-content-sha256") == unsignedPayload
|
||||
}
|
||||
|
||||
// Verify if request has JWT.
|
||||
func isRequestJWT(r *http.Request) bool {
|
||||
return strings.HasPrefix(r.Header.Get("Authorization"), jwtAlgorithm)
|
||||
@@ -56,14 +58,12 @@ func isRequestPresignedSignatureV2(r *http.Request) bool {
|
||||
|
||||
// Verify if request has AWS Post policy Signature Version '4'.
|
||||
func isRequestPostPolicySignatureV4(r *http.Request) bool {
|
||||
return strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") &&
|
||||
r.Method == http.MethodPost
|
||||
return strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") && r.Method == httpPOST
|
||||
}
|
||||
|
||||
// Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation.
|
||||
func isRequestSignStreamingV4(r *http.Request) bool {
|
||||
return r.Header.Get("x-amz-content-sha256") == streamingContentSHA256 &&
|
||||
r.Method == http.MethodPut
|
||||
return r.Header.Get("x-amz-content-sha256") == streamingContentSHA256 && r.Method == httpPUT
|
||||
}
|
||||
|
||||
// Authorization type.
|
||||
@@ -104,38 +104,29 @@ func getRequestAuthType(r *http.Request) authType {
|
||||
return authTypeUnknown
|
||||
}
|
||||
|
||||
// checkAdminRequestAuthType checks whether the request is a valid signature V2 or V4 request.
|
||||
// It does not accept presigned or JWT or anonymous requests.
|
||||
func checkAdminRequestAuthType(r *http.Request, region string) APIErrorCode {
|
||||
s3Err := ErrAccessDenied
|
||||
if getRequestAuthType(r) == authTypeSigned { // we only support V4 (no presign)
|
||||
s3Err = isReqAuthenticated(r, region)
|
||||
}
|
||||
if s3Err != ErrNone {
|
||||
errorIf(errors.New(getAPIError(s3Err).Description), "%s", dumpRequest(r))
|
||||
}
|
||||
return s3Err
|
||||
}
|
||||
|
||||
func checkRequestAuthType(r *http.Request, bucket, policyAction, region string) APIErrorCode {
|
||||
reqAuthType := getRequestAuthType(r)
|
||||
|
||||
switch reqAuthType {
|
||||
case authTypePresignedV2, authTypeSignedV2:
|
||||
// Signature V2 validation.
|
||||
return isReqAuthenticatedV2(r)
|
||||
s3Error := isReqAuthenticatedV2(r)
|
||||
if s3Error != ErrNone {
|
||||
errorIf(errSignatureMismatch, dumpRequest(r))
|
||||
}
|
||||
return s3Error
|
||||
case authTypeSigned, authTypePresigned:
|
||||
return isReqAuthenticated(r, region)
|
||||
s3Error := isReqAuthenticated(r, region)
|
||||
if s3Error != ErrNone {
|
||||
errorIf(errSignatureMismatch, dumpRequest(r))
|
||||
}
|
||||
return s3Error
|
||||
}
|
||||
|
||||
if reqAuthType == authTypeAnonymous && policyAction != "" {
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
||||
resource, err := getResource(r.URL.Path, r.Host, globalDomainName)
|
||||
if err != nil {
|
||||
return ErrInternalError
|
||||
}
|
||||
return enforceBucketPolicy(bucket, policyAction, resource,
|
||||
r.Referer(), handlers.GetSourceIP(r), r.URL.Query())
|
||||
return enforceBucketPolicy(bucket, policyAction, r.URL.Path,
|
||||
r.Referer(), r.URL.Query())
|
||||
}
|
||||
|
||||
// By default return ErrAccessDenied
|
||||
@@ -150,16 +141,19 @@ func isReqAuthenticatedV2(r *http.Request) (s3Error APIErrorCode) {
|
||||
return doesPresignV2SignatureMatch(r)
|
||||
}
|
||||
|
||||
func reqSignatureV4Verify(r *http.Request, region string) (s3Error APIErrorCode) {
|
||||
sha256sum := getContentSha256Cksum(r)
|
||||
switch {
|
||||
case isRequestSignatureV4(r):
|
||||
return doesSignatureMatch(sha256sum, r, region)
|
||||
case isRequestPresignedSignatureV4(r):
|
||||
return doesPresignedSignatureMatch(sha256sum, r, region)
|
||||
default:
|
||||
return ErrAccessDenied
|
||||
func reqSignatureV4Verify(r *http.Request) (s3Error APIErrorCode) {
|
||||
sha256sum := r.Header.Get("X-Amz-Content-Sha256")
|
||||
// Skips calculating sha256 on the payload on server,
|
||||
// if client requested for it.
|
||||
if skipContentSha256Cksum(r) {
|
||||
sha256sum = unsignedPayload
|
||||
}
|
||||
if isRequestSignatureV4(r) {
|
||||
return doesSignatureMatch(sha256sum, r, serverConfig.GetRegion())
|
||||
} else if isRequestPresignedSignatureV4(r) {
|
||||
return doesPresignedSignatureMatch(sha256sum, r, serverConfig.GetRegion())
|
||||
}
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
// Verify if request has valid AWS Signature Version '4'.
|
||||
@@ -167,39 +161,32 @@ func isReqAuthenticated(r *http.Request, region string) (s3Error APIErrorCode) {
|
||||
if r == nil {
|
||||
return ErrInternalError
|
||||
}
|
||||
if errCode := reqSignatureV4Verify(r, region); errCode != ErrNone {
|
||||
return errCode
|
||||
}
|
||||
payload, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to read request body for signature verification")
|
||||
return ErrInternalError
|
||||
}
|
||||
|
||||
// Populate back the payload.
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
||||
|
||||
// Verify Content-Md5, if payload is set.
|
||||
if r.Header.Get("Content-Md5") != "" {
|
||||
if r.Header.Get("Content-Md5") != getMD5HashBase64(payload) {
|
||||
return ErrBadDigest
|
||||
}
|
||||
}
|
||||
|
||||
// Populate back the payload.
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
||||
// Skips calculating sha256 on the payload on server, if client requested for it.
|
||||
var sha256sum string
|
||||
if skipContentSha256Cksum(r) {
|
||||
return ErrNone
|
||||
sha256sum = unsignedPayload
|
||||
} else {
|
||||
sha256sum = getSHA256Hash(payload)
|
||||
}
|
||||
|
||||
// Verify that X-Amz-Content-Sha256 Header == sha256(payload)
|
||||
// If X-Amz-Content-Sha256 header is not sent then we don't calculate/verify sha256(payload)
|
||||
sum := r.Header.Get("X-Amz-Content-Sha256")
|
||||
if isRequestPresignedSignatureV4(r) {
|
||||
sum = r.URL.Query().Get("X-Amz-Content-Sha256")
|
||||
if isRequestSignatureV4(r) {
|
||||
return doesSignatureMatch(sha256sum, r, region)
|
||||
} else if isRequestPresignedSignatureV4(r) {
|
||||
return doesPresignedSignatureMatch(sha256sum, r, region)
|
||||
}
|
||||
if sum != "" && sum != getSHA256Hash(payload) {
|
||||
return ErrContentSHA256Mismatch
|
||||
}
|
||||
return ErrNone
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
// authHandler - handles all the incoming authorization headers and validates them if possible.
|
||||
|
||||
+43
-87
@@ -21,11 +21,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Test get request auth type.
|
||||
@@ -47,7 +43,6 @@ func TestGetRequestAuthType(t *testing.T) {
|
||||
Header: http.Header{
|
||||
"Authorization": []string{"AWS4-HMAC-SHA256 <cred_string>"},
|
||||
"X-Amz-Content-Sha256": []string{streamingContentSHA256},
|
||||
"Content-Encoding": []string{streamingContentEncoding},
|
||||
},
|
||||
Method: "PUT",
|
||||
},
|
||||
@@ -191,6 +186,40 @@ func TestS3SupportedAuthType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsRequestUnsignedPayload - Test validates the Unsigned payload detection logic.
|
||||
func TestIsRequestUnsignedPayload(t *testing.T) {
|
||||
testCases := []struct {
|
||||
inputAmzContentHeader string
|
||||
expectedResult bool
|
||||
}{
|
||||
// Test case - 1.
|
||||
// Test case with "X-Amz-Content-Sha256" header set to empty value.
|
||||
{"", false},
|
||||
// Test case - 2.
|
||||
// Test case with "X-Amz-Content-Sha256" header set to "UNSIGNED-PAYLOAD"
|
||||
// The payload is flagged as unsigned When "X-Amz-Content-Sha256" header is set to "UNSIGNED-PAYLOAD".
|
||||
{unsignedPayload, true},
|
||||
// Test case - 3.
|
||||
// set to a random value.
|
||||
{"abcd", false},
|
||||
}
|
||||
|
||||
// creating an input HTTP request.
|
||||
// Only the headers are relevant for this particular test.
|
||||
inputReq, err := http.NewRequest("GET", "http://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Error initializing input HTTP request: %v", err)
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
inputReq.Header.Set("X-Amz-Content-Sha256", testCase.inputAmzContentHeader)
|
||||
actualResult := isRequestUnsignedPayload(inputReq)
|
||||
if testCase.expectedResult != actualResult {
|
||||
t.Errorf("Test %d: Expected the result to `%v`, but instead got `%v`", i+1, testCase.expectedResult, actualResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRequestPresignedSignatureV2(t *testing.T) {
|
||||
testCases := []struct {
|
||||
inputQueryKey string
|
||||
@@ -271,70 +300,23 @@ func mustNewRequest(method string, urlStr string, contentLength int64, body io.R
|
||||
// is signed with AWS Signature V4, fails if not able to do so.
|
||||
func mustNewSignedRequest(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
|
||||
req := mustNewRequest(method, urlStr, contentLength, body, t)
|
||||
cred := globalServerConfig.GetCredential()
|
||||
cred := serverConfig.GetCredential()
|
||||
if err := signRequestV4(req, cred.AccessKey, cred.SecretKey); err != nil {
|
||||
t.Fatalf("Unable to inititalized new signed http request %s", err)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
// This is similar to mustNewRequest but additionally the request
|
||||
// is signed with AWS Signature V2, fails if not able to do so.
|
||||
func mustNewSignedV2Request(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
|
||||
req := mustNewRequest(method, urlStr, contentLength, body, t)
|
||||
cred := globalServerConfig.GetCredential()
|
||||
if err := signRequestV2(req, cred.AccessKey, cred.SecretKey); err != nil {
|
||||
t.Fatalf("Unable to inititalized new signed http request %s", err)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
// This is similar to mustNewRequest but additionally the request
|
||||
// is presigned with AWS Signature V2, fails if not able to do so.
|
||||
func mustNewPresignedV2Request(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
|
||||
req := mustNewRequest(method, urlStr, contentLength, body, t)
|
||||
cred := globalServerConfig.GetCredential()
|
||||
if err := preSignV2(req, cred.AccessKey, cred.SecretKey, time.Now().Add(10*time.Minute).Unix()); err != nil {
|
||||
t.Fatalf("Unable to inititalized new signed http request %s", err)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
// This is similar to mustNewRequest but additionally the request
|
||||
// is presigned with AWS Signature V4, fails if not able to do so.
|
||||
func mustNewPresignedRequest(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
|
||||
req := mustNewRequest(method, urlStr, contentLength, body, t)
|
||||
cred := globalServerConfig.GetCredential()
|
||||
if err := preSignV4(req, cred.AccessKey, cred.SecretKey, time.Now().Add(10*time.Minute).Unix()); err != nil {
|
||||
t.Fatalf("Unable to inititalized new signed http request %s", err)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func mustNewSignedBadMD5Request(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
|
||||
req := mustNewRequest(method, urlStr, contentLength, body, t)
|
||||
req.Header.Set("Content-Md5", "YWFhYWFhYWFhYWFhYWFhCg==")
|
||||
cred := globalServerConfig.GetCredential()
|
||||
if err := signRequestV4(req, cred.AccessKey, cred.SecretKey); err != nil {
|
||||
t.Fatalf("Unable to initialized new signed http request %s", err)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
// Tests is requested authenticated function, tests replies for s3 errors.
|
||||
func TestIsReqAuthenticated(t *testing.T) {
|
||||
path, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
defer removeAll(path)
|
||||
|
||||
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
||||
if err != nil {
|
||||
t.Fatalf("unable create credential, %s", err)
|
||||
}
|
||||
|
||||
globalServerConfig.SetCredential(creds)
|
||||
creds := newCredentialWithKeys("myuser", "mypassword")
|
||||
serverConfig.SetCredential(creds)
|
||||
|
||||
// List of test cases for validating http request authentication.
|
||||
testCases := []struct {
|
||||
@@ -346,44 +328,18 @@ func TestIsReqAuthenticated(t *testing.T) {
|
||||
// When request is unsigned, access denied is returned.
|
||||
{mustNewRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrAccessDenied},
|
||||
// When request is properly signed, but has bad Content-MD5 header.
|
||||
{mustNewSignedBadMD5Request("PUT", "http://127.0.0.1:9000/", 5, bytes.NewReader([]byte("hello")), t), ErrBadDigest},
|
||||
{mustNewSignedRequest("PUT", "http://127.0.0.1:9000", 5, bytes.NewReader([]byte("hello")), t), ErrBadDigest},
|
||||
// When request is properly signed, error is none.
|
||||
{mustNewSignedRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrNone},
|
||||
}
|
||||
|
||||
// Validates all testcases.
|
||||
for _, testCase := range testCases {
|
||||
if s3Error := isReqAuthenticated(testCase.req, globalServerConfig.GetRegion()); s3Error != testCase.s3Error {
|
||||
if testCase.s3Error == ErrBadDigest {
|
||||
testCase.req.Header.Set("Content-Md5", "garbage")
|
||||
}
|
||||
if s3Error := isReqAuthenticated(testCase.req, serverConfig.GetRegion()); s3Error != testCase.s3Error {
|
||||
t.Fatalf("Unexpected s3error returned wanted %d, got %d", testCase.s3Error, s3Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestCheckAdminRequestAuthType(t *testing.T) {
|
||||
path, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
||||
if err != nil {
|
||||
t.Fatalf("unable create credential, %s", err)
|
||||
}
|
||||
|
||||
globalServerConfig.SetCredential(creds)
|
||||
testCases := []struct {
|
||||
Request *http.Request
|
||||
ErrCode APIErrorCode
|
||||
}{
|
||||
{Request: mustNewRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied},
|
||||
{Request: mustNewSignedRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrNone},
|
||||
{Request: mustNewSignedV2Request("GET", "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied},
|
||||
{Request: mustNewPresignedV2Request("GET", "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied},
|
||||
{Request: mustNewPresignedRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
if s3Error := checkAdminRequestAuthType(testCase.Request, globalServerConfig.GetRegion()); s3Error != testCase.ErrCode {
|
||||
t.Errorf("Test %d: Unexpected s3error returned wanted %d, got %d", i, testCase.ErrCode, s3Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+39
-175
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,16 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -61,11 +52,10 @@ type authConfig struct {
|
||||
|
||||
// AuthRPCClient is a authenticated RPC client which does authentication before doing Call().
|
||||
type AuthRPCClient struct {
|
||||
sync.RWMutex // Mutex to lock this object.
|
||||
rpcClient *rpc.Client // RPC client to make any RPC call.
|
||||
config authConfig // Authentication configuration information.
|
||||
authToken string // Authentication token.
|
||||
version semVersion // RPC version.
|
||||
sync.Mutex // Mutex to lock this object.
|
||||
rpcClient *RPCClient // Reconnectable RPC client to make any RPC call.
|
||||
config authConfig // Authentication configuration information.
|
||||
authToken string // Authentication token.
|
||||
}
|
||||
|
||||
// newAuthRPCClient - returns a JWT based authenticated (go) rpc client, which does automatic reconnect.
|
||||
@@ -83,95 +73,62 @@ func newAuthRPCClient(config authConfig) *AuthRPCClient {
|
||||
}
|
||||
|
||||
return &AuthRPCClient{
|
||||
config: config,
|
||||
version: globalRPCAPIVersion,
|
||||
rpcClient: newRPCClient(config.serverAddr, config.serviceEndpoint, config.secureConn),
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Login a JWT based authentication is performed with rpc server.
|
||||
// Login - a jwt based authentication is performed with rpc server.
|
||||
func (authClient *AuthRPCClient) Login() (err error) {
|
||||
// Login should be attempted one at a time.
|
||||
//
|
||||
// The reason for large region lock here is
|
||||
// to avoid two simultaneous login attempts
|
||||
// racing over each other.
|
||||
//
|
||||
// #1 Login() gets the lock proceeds to login.
|
||||
// #2 Login() waits for the unlock to happen
|
||||
// after login in #1.
|
||||
// #1 Successfully completes login saves the
|
||||
// newly acquired token.
|
||||
// #2 Successfully gets the lock and proceeds,
|
||||
// but since we have acquired the token
|
||||
// already the call quickly returns.
|
||||
authClient.Lock()
|
||||
defer authClient.Unlock()
|
||||
|
||||
// Attempt to login if not logged in already.
|
||||
if authClient.authToken == "" {
|
||||
var authToken string
|
||||
authToken, err = authenticateNode(authClient.config.accessKey, authClient.config.secretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Login to authenticate your token.
|
||||
var (
|
||||
loginMethod = authClient.config.serviceName + loginMethodName
|
||||
loginArgs = LoginRPCArgs{
|
||||
AuthToken: authToken,
|
||||
Version: globalRPCAPIVersion,
|
||||
RequestTime: UTCNow(),
|
||||
}
|
||||
)
|
||||
|
||||
// Re-dial after we have disconnected or if its a fresh run.
|
||||
var rpcClient *rpc.Client
|
||||
rpcClient, err = rpcDial(authClient.config.serverAddr, authClient.config.serviceEndpoint, authClient.config.secureConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = rpcClient.Call(loginMethod, &loginArgs, &LoginRPCReply{}); err != nil {
|
||||
// gob doesn't provide any typed errors for us to reflect
|
||||
// upon, this is the only way to return proper error.
|
||||
if strings.Contains(err.Error(), "gob: wrong type") {
|
||||
return errRPCAPIVersionUnsupported
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize rpc client and auth token after a successful login.
|
||||
authClient.authToken = authToken
|
||||
authClient.rpcClient = rpcClient
|
||||
// Return if already logged in.
|
||||
if authClient.authToken != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Call login.
|
||||
args := LoginRPCArgs{
|
||||
Username: authClient.config.accessKey,
|
||||
Password: authClient.config.secretKey,
|
||||
Version: Version,
|
||||
RequestTime: time.Now().UTC(),
|
||||
}
|
||||
|
||||
reply := LoginRPCReply{}
|
||||
serviceMethod := authClient.config.serviceName + loginMethodName
|
||||
if err = authClient.rpcClient.Call(serviceMethod, &args, &reply); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Logged in successfully.
|
||||
authClient.authToken = reply.AuthToken
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// call makes a RPC call after logs into the server.
|
||||
func (authClient *AuthRPCClient) call(serviceMethod string, args interface {
|
||||
SetAuthToken(authToken string)
|
||||
SetRPCAPIVersion(version semVersion)
|
||||
SetRequestTime(requestTime time.Time)
|
||||
}, reply interface{}) (err error) {
|
||||
if err = authClient.Login(); err != nil {
|
||||
return err
|
||||
} // On successful login, execute RPC call.
|
||||
// On successful login, execute RPC call.
|
||||
if err = authClient.Login(); err == nil {
|
||||
// Set token and timestamp before the rpc call.
|
||||
args.SetAuthToken(authClient.authToken)
|
||||
args.SetRequestTime(time.Now().UTC())
|
||||
|
||||
// Set token before the rpc call.
|
||||
authClient.RLock()
|
||||
defer authClient.RUnlock()
|
||||
args.SetAuthToken(authClient.authToken)
|
||||
args.SetRPCAPIVersion(authClient.version)
|
||||
|
||||
// Do an RPC call.
|
||||
return authClient.rpcClient.Call(serviceMethod, args, reply)
|
||||
// Do RPC call.
|
||||
err = authClient.rpcClient.Call(serviceMethod, args, reply)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Call executes RPC call till success or globalAuthRPCRetryThreshold on ErrShutdown.
|
||||
func (authClient *AuthRPCClient) Call(serviceMethod string, args interface {
|
||||
SetAuthToken(authToken string)
|
||||
SetRPCAPIVersion(version semVersion)
|
||||
SetRequestTime(requestTime time.Time)
|
||||
}, reply interface{}) (err error) {
|
||||
|
||||
// Done channel is used to close any lingering retry routine, as soon
|
||||
@@ -192,11 +149,6 @@ func (authClient *AuthRPCClient) Call(serviceMethod string, args interface {
|
||||
}
|
||||
}
|
||||
}
|
||||
// gob doesn't provide any typed errors for us to reflect
|
||||
// upon, this is the only way to return proper error.
|
||||
if err != nil && strings.Contains(err.Error(), "gob: wrong type") {
|
||||
err = errRPCAPIVersionUnsupported
|
||||
}
|
||||
break
|
||||
}
|
||||
return err
|
||||
@@ -207,10 +159,6 @@ func (authClient *AuthRPCClient) Close() error {
|
||||
authClient.Lock()
|
||||
defer authClient.Unlock()
|
||||
|
||||
if authClient.rpcClient == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
authClient.authToken = ""
|
||||
return authClient.rpcClient.Close()
|
||||
}
|
||||
@@ -224,87 +172,3 @@ func (authClient *AuthRPCClient) ServerAddr() string {
|
||||
func (authClient *AuthRPCClient) ServiceEndpoint() string {
|
||||
return authClient.config.serviceEndpoint
|
||||
}
|
||||
|
||||
// default Dial timeout for RPC connections.
|
||||
const defaultDialTimeout = 3 * time.Second
|
||||
|
||||
// Connect success message required from rpc server.
|
||||
const connectSuccessMessage = "200 Connected to Go RPC"
|
||||
|
||||
// dial tries to establish a connection to serverAddr in a safe manner.
|
||||
// If there is a valid rpc.Cliemt, it returns that else creates a new one.
|
||||
func rpcDial(serverAddr, serviceEndpoint string, secureConn bool) (netRPCClient *rpc.Client, err error) {
|
||||
if serverAddr == "" || serviceEndpoint == "" {
|
||||
return nil, errInvalidArgument
|
||||
}
|
||||
d := &net.Dialer{
|
||||
Timeout: defaultDialTimeout,
|
||||
}
|
||||
var conn net.Conn
|
||||
if secureConn {
|
||||
var hostname string
|
||||
if hostname, _, err = net.SplitHostPort(serverAddr); err != nil {
|
||||
return nil, &net.OpError{
|
||||
Op: "dial-http",
|
||||
Net: serverAddr + serviceEndpoint,
|
||||
Addr: nil,
|
||||
Err: fmt.Errorf("Unable to parse server address <%s>: %s", serverAddr, err),
|
||||
}
|
||||
}
|
||||
// ServerName in tls.Config needs to be specified to support SNI certificates.
|
||||
conn, err = tls.DialWithDialer(d, "tcp", serverAddr, &tls.Config{
|
||||
ServerName: hostname,
|
||||
RootCAs: globalRootCAs,
|
||||
})
|
||||
} else {
|
||||
conn, err = d.Dial("tcp", serverAddr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Print RPC connection errors that are worthy to display in log.
|
||||
switch err.(type) {
|
||||
case x509.HostnameError:
|
||||
errorIf(err, "Unable to establish secure connection to %s", serverAddr)
|
||||
}
|
||||
|
||||
return nil, &net.OpError{
|
||||
Op: "dial-http",
|
||||
Net: serverAddr + serviceEndpoint,
|
||||
Addr: nil,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Check for network errors writing over the dialed conn.
|
||||
if _, err = io.WriteString(conn, "CONNECT "+serviceEndpoint+" HTTP/1.0\n\n"); err != nil {
|
||||
conn.Close()
|
||||
return nil, &net.OpError{
|
||||
Op: "dial-http",
|
||||
Net: serverAddr + serviceEndpoint,
|
||||
Addr: nil,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to read the HTTP response for the HTTP method CONNECT, upon
|
||||
// success return the RPC connection instance.
|
||||
resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
})
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, &net.OpError{
|
||||
Op: "dial-http",
|
||||
Net: serverAddr + serviceEndpoint,
|
||||
Addr: nil,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
if resp.Status != connectSuccessMessage {
|
||||
conn.Close()
|
||||
return nil, errors.New("unexpected HTTP response: " + resp.Status)
|
||||
}
|
||||
|
||||
// Initialize rpc client.
|
||||
return rpc.NewClient(conn), nil
|
||||
}
|
||||
|
||||
@@ -16,12 +16,7 @@
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
import "testing"
|
||||
|
||||
// Tests authorized RPC client.
|
||||
func TestAuthRPCClient(t *testing.T) {
|
||||
@@ -58,81 +53,3 @@ func TestAuthRPCClient(t *testing.T) {
|
||||
t.Fatalf("Unexpected node value %s, but expected %s", authRPC.ServiceEndpoint(), authCfg.serviceEndpoint)
|
||||
}
|
||||
}
|
||||
|
||||
// Test rpc dial test.
|
||||
func TestRPCDial(t *testing.T) {
|
||||
prevRootCAs := globalRootCAs
|
||||
defer func() {
|
||||
globalRootCAs = prevRootCAs
|
||||
}()
|
||||
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
testServer := StartTestServer(t, "")
|
||||
defer testServer.Stop()
|
||||
|
||||
cert, key, err := generateTLSCertKey("127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set global root CAs.
|
||||
globalRootCAs = x509.NewCertPool()
|
||||
globalRootCAs.AppendCertsFromPEM(cert)
|
||||
|
||||
testServerTLS := StartTestTLSServer(t, "", cert, key)
|
||||
defer testServerTLS.Stop()
|
||||
|
||||
adminEndpoint := path.Join(minioReservedBucketPath, adminPath)
|
||||
testCases := []struct {
|
||||
serverAddr string
|
||||
serverEndpoint string
|
||||
success bool
|
||||
secure bool
|
||||
}{
|
||||
// Empty server addr should fail.
|
||||
{
|
||||
serverAddr: "",
|
||||
serverEndpoint: adminEndpoint,
|
||||
success: false,
|
||||
},
|
||||
// Unexpected server addr should fail.
|
||||
{
|
||||
serverAddr: "example.com",
|
||||
serverEndpoint: adminEndpoint,
|
||||
success: false,
|
||||
},
|
||||
// Server addr connects but fails for CONNECT call.
|
||||
{
|
||||
serverAddr: "example.com:80",
|
||||
serverEndpoint: "/",
|
||||
success: false,
|
||||
},
|
||||
// Successful connecting to insecure RPC server.
|
||||
{
|
||||
serverAddr: testServer.Server.Listener.Addr().String(),
|
||||
serverEndpoint: adminEndpoint,
|
||||
success: true,
|
||||
},
|
||||
// Successful connecting to secure RPC server.
|
||||
{
|
||||
serverAddr: testServerTLS.Server.Listener.Addr().String(),
|
||||
serverEndpoint: adminEndpoint,
|
||||
success: true,
|
||||
secure: true,
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
_, err = rpcDial(testCase.serverAddr, testCase.serverEndpoint, testCase.secure)
|
||||
if err != nil && testCase.success {
|
||||
t.Errorf("Test %d: Expected success but found failure instead %s", i+1, err)
|
||||
}
|
||||
if err == nil && !testCase.success {
|
||||
t.Errorf("Test %d: Expected failure but found success instead", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ 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 {
|
||||
@@ -29,10 +30,14 @@ func (b AuthRPCServer) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Return an error if token is not valid.
|
||||
if !isAuthTokenValid(args.AuthToken) {
|
||||
return errAuthentication
|
||||
// Authenticate using JWT.
|
||||
token, err := authenticateNode(args.Username, args.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Return the token.
|
||||
reply.AuthToken = token
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+47
-18
@@ -17,7 +17,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -27,12 +26,8 @@ func TestLogin(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test config - %v", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
creds := globalServerConfig.GetCredential()
|
||||
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer removeAll(rootPath)
|
||||
creds := serverConfig.GetCredential()
|
||||
ls := AuthRPCServer{}
|
||||
testCases := []struct {
|
||||
args LoginRPCArgs
|
||||
@@ -42,8 +37,9 @@ func TestLogin(t *testing.T) {
|
||||
// Valid case.
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: globalRPCAPIVersion,
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: nil,
|
||||
@@ -51,26 +47,59 @@ func TestLogin(t *testing.T) {
|
||||
// Valid username, password and request time, not version.
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: semVersion{3, 0, 0},
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: "INVALID-" + Version,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: errRPCAPIVersionUnsupported,
|
||||
expectedErr: errServerVersionMismatch,
|
||||
},
|
||||
// Valid username, password and version, not request time
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
AuthToken: token,
|
||||
Version: globalRPCAPIVersion,
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
},
|
||||
skewTime: 20 * time.Minute,
|
||||
expectedErr: errServerTimeMismatch,
|
||||
},
|
||||
// Invalid token, fails with authentication error
|
||||
// Invalid username length
|
||||
{
|
||||
args: LoginRPCArgs{
|
||||
AuthToken: "",
|
||||
Version: globalRPCAPIVersion,
|
||||
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,
|
||||
},
|
||||
skewTime: 0,
|
||||
expectedErr: errAuthentication,
|
||||
@@ -78,7 +107,7 @@ func TestLogin(t *testing.T) {
|
||||
}
|
||||
for i, test := range testCases {
|
||||
reply := LoginRPCReply{}
|
||||
test.args.RequestTime = UTCNow().Add(test.skewTime)
|
||||
test.args.RequestTime = time.Now().Add(test.skewTime).UTC()
|
||||
err := ls.Login(&test.args, &reply)
|
||||
if err != test.expectedErr {
|
||||
t.Errorf("Test %d: Expected error %v but received %v",
|
||||
|
||||
+77
-75
@@ -21,16 +21,32 @@ import (
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
// Prepare XL/FS backend for benchmark.
|
||||
// Prepare benchmark backend
|
||||
func prepareBenchmarkBackend(instanceType string) (ObjectLayer, []string, error) {
|
||||
return prepareTestBackend(instanceType)
|
||||
switch instanceType {
|
||||
// Total number of disks for FS backend is set to 1.
|
||||
case FSTestStr:
|
||||
obj, disk, err := prepareFS()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return obj, []string{disk}, nil
|
||||
// Total number of disks for XL backend is set to 16.
|
||||
case XLTestStr:
|
||||
return prepareXL()
|
||||
}
|
||||
obj, disk, err := prepareFS()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return obj, []string{disk}, nil
|
||||
}
|
||||
|
||||
// Benchmark utility functions for ObjectLayer.PutObject().
|
||||
@@ -40,7 +56,7 @@ func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucketWithLocation(bucket, "")
|
||||
err = obj.MakeBucket(bucket)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -50,23 +66,20 @@ 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)
|
||||
|
||||
md5hex := getMD5Hash(textData)
|
||||
sha256hex := ""
|
||||
|
||||
metadata["md5Sum"] = getMD5Hash(textData)
|
||||
sha256sum := ""
|
||||
// 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),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
objInfo, err := obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata, sha256sum)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
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)
|
||||
if objInfo.MD5Sum != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.MD5Sum, metadata["md5Sum"])
|
||||
}
|
||||
}
|
||||
// Benchmark ends here. Stop timer.
|
||||
@@ -82,29 +95,28 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
||||
object := getRandomObjectName()
|
||||
|
||||
// create bucket.
|
||||
err = obj.MakeBucketWithLocation(bucket, "")
|
||||
err = obj.MakeBucket(bucket)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
objSize := 128 * humanize.MiByte
|
||||
|
||||
// PutObjectPart returns etag of the object inserted.
|
||||
// etag variable is assigned with that value.
|
||||
var etag, uploadID string
|
||||
// PutObjectPart returns md5Sum of the object inserted.
|
||||
// md5Sum variable is assigned with that value.
|
||||
var md5Sum, uploadID string
|
||||
// get text data generated for number of bytes equal to object size.
|
||||
textData := generateBytesData(objSize)
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for NewMultipartUpload.
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = getMD5Hash(textData)
|
||||
sha256sum := ""
|
||||
uploadID, err = obj.NewMultipartUpload(bucket, object, metadata)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
md5hex := getMD5Hash(textData)
|
||||
sha256hex := ""
|
||||
|
||||
var textPartData []byte
|
||||
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
|
||||
b.ReportAllocs()
|
||||
@@ -119,15 +131,15 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
||||
} else {
|
||||
textPartData = textData[j*partSize:]
|
||||
}
|
||||
md5hex = getMD5Hash([]byte(textPartData))
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = getMD5Hash([]byte(textPartData))
|
||||
var partInfo PartInfo
|
||||
partInfo, err = obj.PutObjectPart(bucket, object, uploadID, j,
|
||||
mustGetHashReader(b, bytes.NewBuffer(textPartData), int64(len(textPartData)), md5hex, sha256hex))
|
||||
partInfo, err = obj.PutObjectPart(bucket, object, uploadID, j, int64(len(textPartData)), bytes.NewBuffer(textPartData), metadata["md5Sum"], sha256sum)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
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)
|
||||
if partInfo.ETag != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, md5Sum, metadata["md5Sum"])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +153,7 @@ func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
defer removeAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -160,7 +172,7 @@ func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
defer removeAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -179,7 +191,7 @@ func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
defer removeAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -199,37 +211,33 @@ func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
defer removeAll(rootPath)
|
||||
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucketWithLocation(bucket, "")
|
||||
err = obj.MakeBucket(bucket)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
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 := ""
|
||||
|
||||
sha256sum := ""
|
||||
for i := 0; i < 10; i++ {
|
||||
// get text data generated for number of bytes equal to object size.
|
||||
textData := generateBytesData(objSize)
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for PutObject.
|
||||
// PutObject is the functions which writes the data onto the FS/XL backend.
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = getMD5Hash(textData)
|
||||
// insert the object.
|
||||
var objInfo ObjectInfo
|
||||
objInfo, err = obj.PutObject(bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
objInfo, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata, sha256sum)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
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)
|
||||
if objInfo.MD5Sum != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.MD5Sum, metadata["md5Sum"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +247,7 @@ func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var buffer = new(bytes.Buffer)
|
||||
err = obj.GetObject(bucket, "object"+strconv.Itoa(i%10), 0, int64(objSize), buffer, "")
|
||||
err = obj.GetObject(bucket, "object"+strconv.Itoa(i%10), 0, int64(objSize), buffer)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
@@ -253,7 +261,7 @@ func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
func getRandomByte() []byte {
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
// seeding the random number generator.
|
||||
rand.Seed(UTCNow().UnixNano())
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
var b byte
|
||||
// pick a character randomly.
|
||||
b = letterBytes[rand.Intn(len(letterBytes))]
|
||||
@@ -272,7 +280,7 @@ func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
defer removeAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -291,7 +299,7 @@ func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
defer removeAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
@@ -311,12 +319,12 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
defer removeAll(rootPath)
|
||||
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucketWithLocation(bucket, "")
|
||||
err = obj.MakeBucket(bucket)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -326,10 +334,8 @@ 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)
|
||||
|
||||
md5hex := getMD5Hash([]byte(textData))
|
||||
sha256hex := ""
|
||||
|
||||
metadata["md5Sum"] = getMD5Hash([]byte(textData))
|
||||
sha256sum := ""
|
||||
// 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.
|
||||
@@ -339,13 +345,12 @@ 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),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
objInfo, err := obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata, sha256sum)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if objInfo.ETag != md5hex {
|
||||
b.Fatalf("Write no: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", objInfo.ETag, md5hex)
|
||||
if objInfo.MD5Sum != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", objInfo.MD5Sum, metadata["md5Sum"])
|
||||
}
|
||||
i++
|
||||
}
|
||||
@@ -362,36 +367,33 @@ func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
defer removeAll(rootPath)
|
||||
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucketWithLocation(bucket, "")
|
||||
err = obj.MakeBucket(bucket)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// get text data generated for number of bytes equal to object size.
|
||||
textData := generateBytesData(objSize)
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for PutObject.
|
||||
// PutObject is the functions which writes the data onto the FS/XL backend.
|
||||
metadata := make(map[string]string)
|
||||
|
||||
md5hex := getMD5Hash([]byte(textData))
|
||||
sha256hex := ""
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
// get text data generated for number of bytes equal to object size.
|
||||
textData := generateBytesData(objSize)
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for PutObject.
|
||||
// PutObject is the functions which writes the data onto the FS/XL backend.
|
||||
metadata := make(map[string]string)
|
||||
metadata["md5Sum"] = getMD5Hash([]byte(textData))
|
||||
sha256sum := ""
|
||||
// insert the object.
|
||||
var objInfo ObjectInfo
|
||||
objInfo, err = obj.PutObject(bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
objInfo, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata, sha256sum)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
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)
|
||||
if objInfo.MD5Sum != metadata["md5Sum"] {
|
||||
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.MD5Sum, metadata["md5Sum"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +404,7 @@ func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
err = obj.GetObject(bucket, "object"+strconv.Itoa(i), 0, int64(objSize), ioutil.Discard, "")
|
||||
err = obj.GetObject(bucket, "object"+strconv.Itoa(i), 0, int64(objSize), ioutil.Discard)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
||||
@@ -1,68 +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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// BrowserFlag - wrapper bool type.
|
||||
type BrowserFlag bool
|
||||
|
||||
// String - returns string of BrowserFlag.
|
||||
func (bf BrowserFlag) String() string {
|
||||
if bf {
|
||||
return "on"
|
||||
}
|
||||
|
||||
return "off"
|
||||
}
|
||||
|
||||
// MarshalJSON - converts BrowserFlag into JSON data.
|
||||
func (bf BrowserFlag) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(bf.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON - parses given data into BrowserFlag.
|
||||
func (bf *BrowserFlag) UnmarshalJSON(data []byte) (err error) {
|
||||
var s string
|
||||
if err = json.Unmarshal(data, &s); err == nil {
|
||||
b := BrowserFlag(true)
|
||||
if s == "" {
|
||||
// Empty string is treated as valid.
|
||||
*bf = b
|
||||
} else if b, err = ParseBrowserFlag(s); err == nil {
|
||||
*bf = b
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ParseBrowserFlag - parses string into BrowserFlag.
|
||||
func ParseBrowserFlag(s string) (bf BrowserFlag, err error) {
|
||||
if s == "on" {
|
||||
bf = true
|
||||
} else if s == "off" {
|
||||
bf = false
|
||||
} else {
|
||||
err = fmt.Errorf("invalid value ‘%s’ for BrowserFlag", s)
|
||||
}
|
||||
|
||||
return bf, err
|
||||
}
|
||||
@@ -1,137 +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 (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test BrowserFlag.String()
|
||||
func TestBrowserFlagString(t *testing.T) {
|
||||
var bf BrowserFlag
|
||||
|
||||
testCases := []struct {
|
||||
flag BrowserFlag
|
||||
expectedResult string
|
||||
}{
|
||||
{bf, "off"},
|
||||
{BrowserFlag(true), "on"},
|
||||
{BrowserFlag(false), "off"},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
str := testCase.flag.String()
|
||||
if testCase.expectedResult != str {
|
||||
t.Fatalf("expected: %v, got: %v", testCase.expectedResult, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test BrowserFlag.MarshalJSON()
|
||||
func TestBrowserFlagMarshalJSON(t *testing.T) {
|
||||
var bf BrowserFlag
|
||||
|
||||
testCases := []struct {
|
||||
flag BrowserFlag
|
||||
expectedResult string
|
||||
}{
|
||||
{bf, `"off"`},
|
||||
{BrowserFlag(true), `"on"`},
|
||||
{BrowserFlag(false), `"off"`},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
data, _ := testCase.flag.MarshalJSON()
|
||||
if testCase.expectedResult != string(data) {
|
||||
t.Fatalf("expected: %v, got: %v", testCase.expectedResult, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test BrowserFlag.UnmarshalJSON()
|
||||
func TestBrowserFlagUnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
data []byte
|
||||
expectedResult BrowserFlag
|
||||
expectedErr error
|
||||
}{
|
||||
{[]byte(`{}`), BrowserFlag(false), errors.New("json: cannot unmarshal object into Go value of type string")},
|
||||
{[]byte(`["on"]`), BrowserFlag(false), errors.New("json: cannot unmarshal array into Go value of type string")},
|
||||
{[]byte(`"junk"`), BrowserFlag(false), errors.New("invalid value ‘junk’ for BrowserFlag")},
|
||||
{[]byte(`"true"`), BrowserFlag(false), errors.New("invalid value ‘true’ for BrowserFlag")},
|
||||
{[]byte(`"false"`), BrowserFlag(false), errors.New("invalid value ‘false’ for BrowserFlag")},
|
||||
{[]byte(`"ON"`), BrowserFlag(false), errors.New("invalid value ‘ON’ for BrowserFlag")},
|
||||
{[]byte(`"OFF"`), BrowserFlag(false), errors.New("invalid value ‘OFF’ for BrowserFlag")},
|
||||
{[]byte(`""`), BrowserFlag(true), nil},
|
||||
{[]byte(`"on"`), BrowserFlag(true), nil},
|
||||
{[]byte(`"off"`), BrowserFlag(false), nil},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
var flag BrowserFlag
|
||||
err := (&flag).UnmarshalJSON(testCase.data)
|
||||
if testCase.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
} else if testCase.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
|
||||
if err == nil && testCase.expectedResult != flag {
|
||||
t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test ParseBrowserFlag()
|
||||
func TestParseBrowserFlag(t *testing.T) {
|
||||
testCases := []struct {
|
||||
flagStr string
|
||||
expectedResult BrowserFlag
|
||||
expectedErr error
|
||||
}{
|
||||
{"", BrowserFlag(false), errors.New("invalid value ‘’ for BrowserFlag")},
|
||||
{"junk", BrowserFlag(false), errors.New("invalid value ‘junk’ for BrowserFlag")},
|
||||
{"true", BrowserFlag(false), errors.New("invalid value ‘true’ for BrowserFlag")},
|
||||
{"false", BrowserFlag(false), errors.New("invalid value ‘false’ for BrowserFlag")},
|
||||
{"ON", BrowserFlag(false), errors.New("invalid value ‘ON’ for BrowserFlag")},
|
||||
{"OFF", BrowserFlag(false), errors.New("invalid value ‘OFF’ for BrowserFlag")},
|
||||
{"on", BrowserFlag(true), nil},
|
||||
{"off", BrowserFlag(false), nil},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
bf, err := ParseBrowserFlag(testCase.flagStr)
|
||||
if testCase.expectedErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
||||
} else if testCase.expectedErr.Error() != err.Error() {
|
||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
||||
}
|
||||
|
||||
if err == nil && testCase.expectedResult != bf {
|
||||
t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, bf)
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-20
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,21 +17,38 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Login handler implements JWT login token generator, which upon login request
|
||||
// along with username and password is generated.
|
||||
func (br *browserPeerAPIHandlers) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
|
||||
// Validate LoginRPCArgs
|
||||
if err := args.IsValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Authenticate using JWT.
|
||||
token, err := authenticateWeb(args.Username, args.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Return the token.
|
||||
reply.AuthToken = token
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAuthPeerArgs - Arguments collection for SetAuth RPC call
|
||||
type SetAuthPeerArgs struct {
|
||||
// For Auth
|
||||
AuthRPCArgs
|
||||
|
||||
// New credentials that receiving peer should update to.
|
||||
Creds auth.Credentials
|
||||
Creds credential
|
||||
}
|
||||
|
||||
// SetAuthPeer - Update to new credentials sent from a peer Minio
|
||||
@@ -46,23 +63,16 @@ func (br *browserPeerAPIHandlers) SetAuthPeer(args SetAuthPeerArgs, reply *AuthR
|
||||
return err
|
||||
}
|
||||
|
||||
if !args.Creds.IsValid() {
|
||||
return fmt.Errorf("Invalid credential passed")
|
||||
if err := validateAuthKeys(args.Creds.AccessKey, args.Creds.SecretKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Acquire lock before updating global configuration.
|
||||
globalServerConfigMu.Lock()
|
||||
defer globalServerConfigMu.Unlock()
|
||||
|
||||
// Update credentials in memory
|
||||
prevCred := globalServerConfig.SetCredential(args.Creds)
|
||||
serverConfig.SetCredential(args.Creds)
|
||||
|
||||
// Save credentials to config file
|
||||
if err := globalServerConfig.Save(); err != nil {
|
||||
// Save the current creds when failed to update.
|
||||
globalServerConfig.SetCredential(prevCred)
|
||||
|
||||
errorIf(err, "Unable to update the config with new credentials sent from browser RPC.")
|
||||
if err := serverConfig.Save(); err != nil {
|
||||
errorIf(err, "Error updating config file with new credentials sent from browser RPC.")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -70,7 +80,7 @@ func (br *browserPeerAPIHandlers) SetAuthPeer(args SetAuthPeerArgs, reply *AuthR
|
||||
}
|
||||
|
||||
// Sends SetAuthPeer RPCs to all peers in the Minio cluster
|
||||
func updateCredsOnPeers(creds auth.Credentials) map[string]error {
|
||||
func updateCredsOnPeers(creds credential) map[string]error {
|
||||
// Get list of peer addresses (from globalS3Peers)
|
||||
peers := []string{}
|
||||
for _, p := range globalS3Peers {
|
||||
@@ -81,7 +91,7 @@ func updateCredsOnPeers(creds auth.Credentials) map[string]error {
|
||||
errs := make([]error, len(peers))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
serverCred := globalServerConfig.GetCredential()
|
||||
serverCred := serverConfig.GetCredential()
|
||||
// Launch go routines to send request to each peer in parallel.
|
||||
for ix := range peers {
|
||||
wg.Add(1)
|
||||
@@ -101,7 +111,7 @@ func updateCredsOnPeers(creds auth.Credentials) map[string]error {
|
||||
secretKey: serverCred.SecretKey,
|
||||
serverAddr: peers[ix],
|
||||
secureConn: globalIsSSL,
|
||||
serviceEndpoint: path.Join(minioReservedBucketPath, browserPeerPath),
|
||||
serviceEndpoint: path.Join(reservedBucket, browserPeerPath),
|
||||
serviceName: "BrowserPeer",
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,8 +19,7 @@ package cmd
|
||||
import (
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"time"
|
||||
)
|
||||
|
||||
// API suite container common to both FS and XL.
|
||||
@@ -31,20 +30,21 @@ type TestRPCBrowserPeerSuite struct {
|
||||
}
|
||||
|
||||
// Setting up the test suite and starting the Test server.
|
||||
func (s *TestRPCBrowserPeerSuite) SetUpSuite(t *testing.T) {
|
||||
s.testServer = StartTestBrowserPeerRPCServer(t, s.serverType)
|
||||
func (s *TestRPCBrowserPeerSuite) SetUpSuite(c *testing.T) {
|
||||
s.testServer = StartTestBrowserPeerRPCServer(c, s.serverType)
|
||||
s.testAuthConf = authConfig{
|
||||
serverAddr: s.testServer.Server.Listener.Addr().String(),
|
||||
accessKey: s.testServer.AccessKey,
|
||||
secretKey: s.testServer.SecretKey,
|
||||
serviceEndpoint: path.Join(minioReservedBucketPath, browserPeerPath),
|
||||
serviceEndpoint: path.Join(reservedBucket, browserPeerPath),
|
||||
serviceName: "BrowserPeer",
|
||||
}
|
||||
}
|
||||
|
||||
// TeatDownSuite - called implicitly by after all tests are run in
|
||||
// browser peer rpc suite.
|
||||
func (s *TestRPCBrowserPeerSuite) TearDownSuite(t *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) {
|
||||
s.testServer.Stop()
|
||||
}
|
||||
|
||||
@@ -63,20 +63,18 @@ func TestBrowserPeerRPC(t *testing.T) {
|
||||
// Tests for browser peer rpc.
|
||||
func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
|
||||
// Construct RPC call arguments.
|
||||
creds, err := auth.CreateCredentials("abcd1", "abcd1234")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create credential. %v", err)
|
||||
creds := credential{
|
||||
AccessKey: "abcd1",
|
||||
SecretKey: "abcd1234",
|
||||
}
|
||||
|
||||
// Validate for invalid token.
|
||||
args := SetAuthPeerArgs{Creds: creds}
|
||||
rclient := newAuthRPCClient(s.testAuthConf)
|
||||
args.AuthToken = "garbage"
|
||||
rclient := newRPCClient(s.testAuthConf.serverAddr, s.testAuthConf.serviceEndpoint, false)
|
||||
defer rclient.Close()
|
||||
if err = rclient.Login(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rclient.authToken = "garbage"
|
||||
if err = rclient.Call("BrowserPeer.SetAuthPeer", &args, &AuthRPCReply{}); err != nil {
|
||||
err := rclient.Call("BrowserPeer.SetAuthPeer", &args, &AuthRPCReply{})
|
||||
if err != nil {
|
||||
if err.Error() != errInvalidToken.Error() {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -92,25 +90,36 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
|
||||
}
|
||||
|
||||
// Validate for failure in login handler with previous credentials.
|
||||
rclient = newAuthRPCClient(s.testAuthConf)
|
||||
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,
|
||||
Version: Version,
|
||||
RequestTime: time.Now().UTC(),
|
||||
}
|
||||
rclient.authToken = token
|
||||
if err = rclient.Login(); err != nil {
|
||||
rreply := &LoginRPCReply{}
|
||||
err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply)
|
||||
if err != nil {
|
||||
if err.Error() != errInvalidAccessKeyID.Error() {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
token, err = authenticateNode(creds.AccessKey, creds.SecretKey)
|
||||
// Validate for success in loing handled with valid credetnails.
|
||||
rargs = &LoginRPCArgs{
|
||||
Username: creds.AccessKey,
|
||||
Password: creds.SecretKey,
|
||||
Version: Version,
|
||||
RequestTime: time.Now().UTC(),
|
||||
}
|
||||
rreply = &LoginRPCReply{}
|
||||
err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rclient.authToken = token
|
||||
if err = rclient.Login(); 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,9 +17,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
router "github.com/gorilla/mux"
|
||||
"net/rpc"
|
||||
|
||||
"github.com/minio/minio/pkg/errors"
|
||||
router "github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Set up an RPC endpoint that receives browser related calls. The
|
||||
@@ -37,15 +37,15 @@ type browserPeerAPIHandlers struct {
|
||||
|
||||
// Register RPC router
|
||||
func registerBrowserPeerRPCRouter(mux *router.Router) error {
|
||||
bpHandlers := &browserPeerAPIHandlers{AuthRPCServer{}}
|
||||
bpHandlers := &browserPeerAPIHandlers{}
|
||||
|
||||
bpRPCServer := newRPCServer()
|
||||
bpRPCServer := rpc.NewServer()
|
||||
err := bpRPCServer.RegisterName("BrowserPeer", bpHandlers)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
return traceError(err)
|
||||
}
|
||||
|
||||
bpRouter := mux.NewRoute().PathPrefix(minioReservedBucketPath).Subrouter()
|
||||
bpRouter := mux.NewRoute().PathPrefix(reservedBucket).Subrouter()
|
||||
bpRouter.Path(browserPeerPath).Handler(bpRPCServer)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -40,6 +40,13 @@ func validateListObjectsArgs(prefix, marker, delimiter string, maxKeys int) APIE
|
||||
if delimiter != "" && delimiter != "/" {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
// Marker is set validate pre-condition.
|
||||
if marker != "" {
|
||||
// Marker not common with prefix is not implemented.
|
||||
if !hasPrefix(marker, prefix) {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
}
|
||||
// Success.
|
||||
return ErrNone
|
||||
}
|
||||
@@ -62,7 +69,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(r, bucket, "s3:ListBucket", globalServerConfig.GetRegion()); s3Error != ErrNone {
|
||||
if s3Error := checkRequestAuthType(r, bucket, "s3:ListBucket", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -77,35 +84,23 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
||||
// Then we need to use 'start-after' as marker instead.
|
||||
marker = startAfter
|
||||
}
|
||||
|
||||
// Validate the query params before beginning to serve the request.
|
||||
// fetch-owner is not validated since it is a boolean
|
||||
if s3Error := validateListObjectsArgs(prefix, marker, delimiter, maxKeys); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Inititate a list objects operation based on the input params.
|
||||
// On success would return back ListObjectsInfo object to be
|
||||
// marshalled into S3 compatible XML header.
|
||||
listObjectsV2Info, err := objectAPI.ListObjectsV2(bucket, prefix, marker, delimiter, maxKeys, fetchOwner, startAfter)
|
||||
listObjectsInfo, err := objectAPI.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to list objects.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range listObjectsV2Info.Objects {
|
||||
if listObjectsV2Info.Objects[i].IsEncrypted() {
|
||||
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].DecryptedSize()
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsV2Info.NextContinuationToken, startAfter,
|
||||
delimiter, fetchOwner, listObjectsV2Info.IsTruncated, maxKeys, listObjectsV2Info.Objects, listObjectsV2Info.Prefixes)
|
||||
response := generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter, fetchOwner, maxKeys, listObjectsInfo)
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, encodeResponse(response))
|
||||
@@ -127,7 +122,7 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(r, bucket, "s3:ListBucket", globalServerConfig.GetRegion()); s3Error != ErrNone {
|
||||
if s3Error := checkRequestAuthType(r, bucket, "s3:ListBucket", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -135,12 +130,7 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
||||
// Extract all the litsObjectsV1 query params to their native values.
|
||||
prefix, marker, delimiter, maxKeys, _ := getListObjectsV1Args(r.URL.Query())
|
||||
|
||||
// Validate the maxKeys lowerbound. When maxKeys > 1000, S3 returns 1000 but
|
||||
// does not throw an error.
|
||||
if maxKeys < 0 {
|
||||
writeErrorResponse(w, ErrInvalidMaxKeys, r.URL)
|
||||
return
|
||||
} // Validate all the query params before beginning to serve the request.
|
||||
// Validate all the query params before beginning to serve the request.
|
||||
if s3Error := validateListObjectsArgs(prefix, marker, delimiter, maxKeys); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
@@ -151,20 +141,10 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
||||
// marshalled into S3 compatible XML header.
|
||||
listObjectsInfo, err := objectAPI.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to list objects.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range listObjectsInfo.Objects {
|
||||
if listObjectsInfo.Objects[i].IsEncrypted() {
|
||||
listObjectsInfo.Objects[i].Size, err = listObjectsInfo.Objects[i].DecryptedSize()
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, maxKeys, listObjectsInfo)
|
||||
|
||||
// Write success response.
|
||||
|
||||
+117
-188
@@ -19,30 +19,24 @@ package cmd
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
mux "github.com/gorilla/mux"
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/pkg/errors"
|
||||
"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, sourceIP string, queryParams url.Values) (s3Error APIErrorCode) {
|
||||
func enforceBucketPolicy(bucket, action, resource, referer string, queryParams url.Values) (s3Error APIErrorCode) {
|
||||
// Verify if bucket actually exists
|
||||
objAPI := newObjectLayerFn()
|
||||
if err := checkBucketExist(bucket, objAPI); err != nil {
|
||||
err = errors.Cause(err)
|
||||
if err := checkBucketExist(bucket, newObjectLayerFn()); err != nil {
|
||||
err = errorCause(err)
|
||||
switch err.(type) {
|
||||
case BucketNameInvalid:
|
||||
// Return error for invalid bucket name.
|
||||
@@ -57,11 +51,8 @@ func enforceBucketPolicy(bucket, action, resource, referer, sourceIP string, que
|
||||
}
|
||||
|
||||
// Fetch bucket policy, if policy is not set return access denied.
|
||||
p, err := objAPI.GetBucketPolicy(bucket)
|
||||
if err != nil {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
if reflect.DeepEqual(p, emptyBucketPolicy) {
|
||||
policy := globalBucketPolicies.GetBucketPolicy(bucket)
|
||||
if policy == nil {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
@@ -69,7 +60,7 @@ func enforceBucketPolicy(bucket, action, resource, referer, sourceIP string, que
|
||||
arn := bucketARNPrefix + strings.TrimSuffix(strings.TrimPrefix(resource, "/"), "/")
|
||||
|
||||
// Get conditions for policy verification.
|
||||
conditionKeyMap := make(policy.ConditionKeyMap)
|
||||
conditionKeyMap := make(map[string]set.StringSet)
|
||||
for queryParam := range queryParams {
|
||||
conditionKeyMap[queryParam] = set.CreateStringSet(queryParams.Get(queryParam))
|
||||
}
|
||||
@@ -78,30 +69,27 @@ func enforceBucketPolicy(bucket, action, resource, referer, sourceIP string, que
|
||||
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, p.Statements) {
|
||||
if !bucketPolicyEvalStatements(action, arn, conditionKeyMap, policy.Statements) {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Check if the action is allowed on the bucket/prefix.
|
||||
func isBucketActionAllowed(action, bucket, prefix string, objectAPI ObjectLayer) bool {
|
||||
|
||||
bp, err := objectAPI.GetBucketPolicy(bucket)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if reflect.DeepEqual(bp, emptyBucketPolicy) {
|
||||
func isBucketActionAllowed(action, bucket, prefix string) bool {
|
||||
policy := globalBucketPolicies.GetBucketPolicy(bucket)
|
||||
if policy == nil {
|
||||
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, bp.Statements)
|
||||
if !bucketPolicyEvalStatements(action, resource, conditionKeyMap, policy.Statements) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetBucketLocationHandler - GET Bucket location.
|
||||
@@ -120,21 +108,15 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
|
||||
s3Error := checkRequestAuthType(r, bucket, "s3:GetBucketLocation", globalMinioDefaultRegion)
|
||||
if s3Error == ErrInvalidRegion {
|
||||
// Clients like boto3 send getBucketLocation() call signed with region that is configured.
|
||||
s3Error = checkRequestAuthType(r, "", "s3:GetBucketLocation", globalServerConfig.GetRegion())
|
||||
s3Error = checkRequestAuthType(r, "", "s3:GetBucketLocation", serverConfig.GetRegion())
|
||||
}
|
||||
if s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
if err := bucketLock.GetRLock(globalObjectTimeout); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
defer bucketLock.RUnlock()
|
||||
|
||||
if _, err := objectAPI.GetBucketInfo(bucket); err != nil {
|
||||
errorIf(err, "Unable to fetch bucket info.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -142,7 +124,7 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
|
||||
// Generate response.
|
||||
encodedSuccessResponse := encodeResponse(LocationResponse{})
|
||||
// Get current region.
|
||||
region := globalServerConfig.GetRegion()
|
||||
region := serverConfig.GetRegion()
|
||||
if region != globalMinioDefaultRegion {
|
||||
encodedSuccessResponse = encodeResponse(LocationResponse{
|
||||
Location: region,
|
||||
@@ -171,7 +153,7 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(r, bucket, "s3:ListBucketMultipartUploads", globalServerConfig.GetRegion()); s3Error != ErrNone {
|
||||
if s3Error := checkRequestAuthType(r, bucket, "s3:ListBucketMultipartUploads", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -191,6 +173,7 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter,
|
||||
|
||||
listMultipartsInfo, err := objectAPI.ListMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to list multipart uploads.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -217,7 +200,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
||||
s3Error := checkRequestAuthType(r, "", "", globalMinioDefaultRegion)
|
||||
if s3Error == ErrInvalidRegion {
|
||||
// Clients like boto3 send listBuckets() call signed with region that is configured.
|
||||
s3Error = checkRequestAuthType(r, "", "", globalServerConfig.GetRegion())
|
||||
s3Error = checkRequestAuthType(r, "", "", serverConfig.GetRegion())
|
||||
}
|
||||
if s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
@@ -226,6 +209,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
||||
// Invoke the list buckets.
|
||||
bucketsInfo, err := objectAPI.ListBuckets()
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to list buckets.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -249,14 +233,9 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
var authError APIErrorCode
|
||||
if authError = checkRequestAuthType(r, bucket, "s3:DeleteObject", globalServerConfig.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
|
||||
}
|
||||
if s3Error := checkRequestAuthType(r, bucket, "s3:DeleteObject", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Content-Length is required and should be non-zero
|
||||
@@ -299,15 +278,6 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
wg.Add(1)
|
||||
go func(i int, obj ObjectIdentifier) {
|
||||
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
|
||||
}
|
||||
dErr := objectAPI.DeleteObject(bucket, obj.ObjectName)
|
||||
if dErr != nil {
|
||||
dErrs[i] = dErr
|
||||
@@ -326,12 +296,13 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
deletedObjects = append(deletedObjects, object)
|
||||
continue
|
||||
}
|
||||
if _, ok := errors.Cause(err).(ObjectNotFound); ok {
|
||||
if _, ok := errorCause(err).(ObjectNotFound); ok {
|
||||
// If the object is not found it should be
|
||||
// accounted as deleted as per S3 spec.
|
||||
deletedObjects = append(deletedObjects, object)
|
||||
continue
|
||||
}
|
||||
errorIf(err, "Unable to delete object. %s", object.ObjectName)
|
||||
// Error during delete should be collected separately.
|
||||
deleteErrors = append(deleteErrors, DeleteError{
|
||||
Code: errorCodeResponse[toAPIErrorCode(err)].Code,
|
||||
@@ -347,13 +318,6 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, encodedSuccessResponse)
|
||||
|
||||
// Get host and port from Request.RemoteAddr failing which
|
||||
// fill them with empty strings.
|
||||
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
host, port = "", ""
|
||||
}
|
||||
|
||||
// Notify deleted event for objects.
|
||||
for _, dobj := range deletedObjects {
|
||||
eventNotify(eventData{
|
||||
@@ -362,10 +326,9 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
ObjInfo: ObjectInfo{
|
||||
Name: dobj.ObjectName,
|
||||
},
|
||||
ReqParams: extractReqParams(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
Host: host,
|
||||
Port: port,
|
||||
ReqParams: map[string]string{
|
||||
"sourceIPAddress": r.RemoteAddr,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -381,7 +344,11 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
// PutBucket does not have any bucket action.
|
||||
s3Error := checkRequestAuthType(r, "", "", globalServerConfig.GetRegion())
|
||||
s3Error := checkRequestAuthType(r, "", "", globalMinioDefaultRegion)
|
||||
if s3Error == ErrInvalidRegion {
|
||||
// Clients like boto3 send putBucket() call signed with region that is configured.
|
||||
s3Error = checkRequestAuthType(r, "", "", serverConfig.GetRegion())
|
||||
}
|
||||
if s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
@@ -390,30 +357,21 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
// Parse incoming location constraint.
|
||||
location, s3Error := parseLocationConstraint(r)
|
||||
if s3Error != ErrNone {
|
||||
// Validate if incoming location constraint is valid, reject
|
||||
// requests which do not follow valid region requirements.
|
||||
if s3Error := isValidLocationConstraint(r); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate if location sent by the client is valid, reject
|
||||
// requests which do not follow valid region requirements.
|
||||
if !isValidLocation(location) {
|
||||
writeErrorResponse(w, ErrInvalidRegion, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
if err := bucketLock.GetLock(globalObjectTimeout); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
bucketLock.Lock()
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
// Proceed to creating a bucket.
|
||||
err := objectAPI.MakeBucketWithLocation(bucket, location)
|
||||
err := objectAPI.MakeBucket(bucket)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to create a bucket.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -435,24 +393,12 @@ 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.
|
||||
@@ -491,25 +437,15 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
// Close multipart file
|
||||
defer fileBody.Close()
|
||||
|
||||
formValues.Set("Bucket", bucket)
|
||||
bucket := mux.Vars(r)["bucket"]
|
||||
formValues["Bucket"] = bucket
|
||||
|
||||
if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") {
|
||||
if fileName != "" && strings.Contains(formValues["Key"], "${filename}") {
|
||||
// S3 feature to replace ${filename} found in Key form field
|
||||
// by the filename attribute passed in multipart
|
||||
formValues.Set("Key", strings.Replace(formValues.Get("Key"), "${filename}", fileName, -1))
|
||||
}
|
||||
object := formValues.Get("Key")
|
||||
|
||||
successRedirect := formValues.Get("success_action_redirect")
|
||||
successStatus := formValues.Get("success_action_status")
|
||||
var redirectURL *url.URL
|
||||
if successRedirect != "" {
|
||||
redirectURL, err = url.Parse(successRedirect)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL)
|
||||
return
|
||||
}
|
||||
formValues["Key"] = strings.Replace(formValues["Key"], "${filename}", fileName, -1)
|
||||
}
|
||||
object := formValues["Key"]
|
||||
|
||||
// Verify policy signature.
|
||||
apiErr := doesPolicySignatureMatch(formValues)
|
||||
@@ -518,7 +454,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy"))
|
||||
policyBytes, err := base64.StdEncoding.DecodeString(formValues["Policy"])
|
||||
if err != nil {
|
||||
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL)
|
||||
return
|
||||
@@ -541,103 +477,77 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
lengthRange := postPolicyForm.Conditions.ContentLengthRange
|
||||
if lengthRange.Valid {
|
||||
if fileSize < lengthRange.Min {
|
||||
errorIf(err, "Unable to create object.")
|
||||
writeErrorResponse(w, toAPIErrorCode(errDataTooSmall), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if fileSize > lengthRange.Max || isMaxObjectSize(fileSize) {
|
||||
if fileSize > lengthRange.Max || fileSize > maxObjectSize {
|
||||
errorIf(err, "Unable to create object.")
|
||||
writeErrorResponse(w, toAPIErrorCode(errDataTooLarge), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Extract metadata to be saved from received Form.
|
||||
metadata, err := extractMetadataFromHeader(formValues)
|
||||
if err != nil {
|
||||
errorIf(err, "found invalid http request header")
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
metadata := extractMetadataFromForm(formValues)
|
||||
|
||||
hashReader, err := hash.NewReader(fileBody, fileSize, "", "")
|
||||
sha256sum := ""
|
||||
|
||||
objectLock := globalNSMutex.NewNSLock(bucket, object)
|
||||
objectLock.Lock()
|
||||
defer objectLock.Unlock()
|
||||
|
||||
objInfo, err := objectAPI.PutObject(bucket, object, fileSize, fileBody, metadata, sha256sum)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to initialize hashReader.")
|
||||
errorIf(err, "Unable to create object.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
w.Header().Set("ETag", "\""+objInfo.MD5Sum+"\"")
|
||||
w.Header().Set("Location", getObjectLocation(bucket, object))
|
||||
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if hasSSECustomerHeader(formValues) && !hasSuffix(object, slashSeparator) { // handle SSE-C requests
|
||||
var reader io.Reader
|
||||
var key []byte
|
||||
key, err = ParseSSECustomerHeader(formValues)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
reader, err = newEncryptReader(hashReader, key, metadata)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
info := ObjectInfo{Size: fileSize}
|
||||
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "") // do not try to verify encrypted content
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
successRedirect := formValues[http.CanonicalHeaderKey("success_action_redirect")]
|
||||
successStatus := formValues[http.CanonicalHeaderKey("success_action_status")]
|
||||
|
||||
if successStatus == "" && successRedirect == "" {
|
||||
writeSuccessNoContent(w)
|
||||
} else {
|
||||
if successRedirect != "" {
|
||||
redirectURL := successRedirect + "?" + fmt.Sprintf("bucket=%s&key=%s&etag=%s",
|
||||
bucket,
|
||||
getURLEncodedName(object),
|
||||
getURLEncodedName("\""+objInfo.MD5Sum+"\""))
|
||||
|
||||
writeRedirectSeeOther(w, redirectURL)
|
||||
} else {
|
||||
// Decide what http response to send depending on success_action_status parameter
|
||||
switch successStatus {
|
||||
case "201":
|
||||
resp := encodeResponse(PostResponse{
|
||||
Bucket: bucket,
|
||||
Key: object,
|
||||
ETag: "\"" + objInfo.MD5Sum + "\"",
|
||||
Location: getObjectLocation(bucket, object),
|
||||
})
|
||||
writeResponse(w, http.StatusCreated, resp, "application/xml")
|
||||
case "200":
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
default:
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
objInfo, err := objectAPI.PutObject(bucket, object, hashReader, metadata)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
location := getObjectLocation(r, globalDomainName, bucket, object)
|
||||
w.Header().Set("ETag", `"`+objInfo.ETag+`"`)
|
||||
w.Header().Set("Location", location)
|
||||
|
||||
// Get host and port from Request.RemoteAddr.
|
||||
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
host, port = "", ""
|
||||
}
|
||||
|
||||
// Notify object created event.
|
||||
defer eventNotify(eventData{
|
||||
Type: ObjectCreatedPost,
|
||||
Bucket: objInfo.Bucket,
|
||||
ObjInfo: objInfo,
|
||||
ReqParams: extractReqParams(r),
|
||||
UserAgent: r.UserAgent(),
|
||||
Host: host,
|
||||
Port: port,
|
||||
eventNotify(eventData{
|
||||
Type: ObjectCreatedPost,
|
||||
Bucket: bucket,
|
||||
ObjInfo: objInfo,
|
||||
ReqParams: map[string]string{
|
||||
"sourceIPAddress": r.RemoteAddr,
|
||||
},
|
||||
})
|
||||
|
||||
if successRedirect != "" {
|
||||
// Replace raw query params..
|
||||
redirectURL.RawQuery = getRedirectPostRawQuery(objInfo)
|
||||
writeRedirectSeeOther(w, redirectURL.String())
|
||||
return
|
||||
}
|
||||
|
||||
// Decide what http response to send depending on success_action_status parameter
|
||||
switch successStatus {
|
||||
case "201":
|
||||
resp := encodeResponse(PostResponse{
|
||||
Bucket: objInfo.Bucket,
|
||||
Key: objInfo.Name,
|
||||
ETag: `"` + objInfo.ETag + `"`,
|
||||
Location: location,
|
||||
})
|
||||
writeResponse(w, http.StatusCreated, resp, "application/xml")
|
||||
case "200":
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
default:
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
}
|
||||
|
||||
// HeadBucketHandler - HEAD Bucket
|
||||
@@ -656,12 +566,17 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(r, bucket, "s3:ListBucket", globalServerConfig.GetRegion()); s3Error != ErrNone {
|
||||
if s3Error := checkRequestAuthType(r, bucket, "s3:ListBucket", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponseHeadersOnly(w, s3Error)
|
||||
return
|
||||
}
|
||||
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.RLock()
|
||||
defer bucketLock.RUnlock()
|
||||
|
||||
if _, err := objectAPI.GetBucketInfo(bucket); err != nil {
|
||||
errorIf(err, "Unable to fetch bucket info.")
|
||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(err))
|
||||
return
|
||||
}
|
||||
@@ -678,7 +593,7 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
// DeleteBucket does not have any bucket action.
|
||||
if s3Error := checkRequestAuthType(r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone {
|
||||
if s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -686,12 +601,26 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
bucketLock.Lock()
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
// Attempt to delete bucket.
|
||||
if err := objectAPI.DeleteBucket(bucket); err != nil {
|
||||
errorIf(err, "Unable to delete a bucket.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete bucket access policy, if present - ignore any errors.
|
||||
_ = removeBucketPolicy(bucket, objectAPI)
|
||||
|
||||
// Delete notification config, if present - ignore any errors.
|
||||
_ = removeNotificationConfig(bucket, objectAPI)
|
||||
|
||||
// Delete listener config, if present - ignore any errors.
|
||||
_ = removeListenerConfig(bucket, objectAPI)
|
||||
|
||||
// Write success response.
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
+22
-76
@@ -24,8 +24,6 @@ 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.
|
||||
@@ -34,7 +32,8 @@ func TestGetBucketLocationHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
credentials credential, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// test cases with sample input and expected output.
|
||||
testCases := []struct {
|
||||
@@ -69,7 +68,7 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
locationResponse: []byte(""),
|
||||
errorResponse: APIErrorResponse{
|
||||
Resource: "/" + bucketName + "/",
|
||||
Code: "InvalidAccessKeyId",
|
||||
Code: "InvalidAccessKeyID",
|
||||
Message: "The access key ID you provided does not exist in our records.",
|
||||
},
|
||||
shouldPass: false,
|
||||
@@ -154,7 +153,7 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
|
||||
// sets the bucket policy using the policy statement generated from `getReadOnlyBucketStatement` so that the
|
||||
// unsigned request goes through and its validated again.
|
||||
ExecObjectLayerAPIAnonTest(t, obj, "TestGetBucketLocationHandler", bucketName, "", instanceType, apiRouter, anonReq, getReadOnlyBucketStatement)
|
||||
ExecObjectLayerAPIAnonTest(t, "TestGetBucketLocationHandler", bucketName, "", instanceType, apiRouter, anonReq, getReadOnlyBucketStatement)
|
||||
|
||||
// HTTP request for testing when `objectLayer` is set to `nil`.
|
||||
// There is no need to use an existing bucket and valid input for creating the request
|
||||
@@ -178,7 +177,8 @@ func TestHeadBucketHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
credentials credential, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// test cases with sample input and expected output.
|
||||
testCases := []struct {
|
||||
@@ -259,7 +259,7 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
|
||||
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
|
||||
// sets the bucket policy using the policy statement generated from `getReadOnlyBucketStatement` so that the
|
||||
// unsigned request goes through and its validated again.
|
||||
ExecObjectLayerAPIAnonTest(t, obj, "TestHeadBucketHandler", bucketName, "", instanceType, apiRouter, anonReq, getReadOnlyBucketStatement)
|
||||
ExecObjectLayerAPIAnonTest(t, "TestHeadBucketHandler", bucketName, "", instanceType, apiRouter, anonReq, getReadOnlyBucketStatement)
|
||||
|
||||
// HTTP request for testing when `objectLayer` is set to `nil`.
|
||||
// There is no need to use an existing bucket and valid input for creating the request
|
||||
@@ -284,7 +284,8 @@ func TestListMultipartUploadsHandler(t *testing.T) {
|
||||
|
||||
// testListMultipartUploadsHandler - Tests validate listing of multipart uploads.
|
||||
func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
credentials credential, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// Collection of non-exhaustive ListMultipartUploads test cases, valid errors
|
||||
// and success responses.
|
||||
@@ -454,6 +455,7 @@ 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)
|
||||
}
|
||||
@@ -493,7 +495,7 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
|
||||
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
|
||||
// sets the bucket policy using the policy statement generated from `getWriteOnlyBucketStatement` so that the
|
||||
// unsigned request goes through and its validated again.
|
||||
ExecObjectLayerAPIAnonTest(t, obj, "TestListMultipartUploadsHandler", bucketName, "", instanceType, apiRouter, anonReq, getWriteOnlyBucketStatement)
|
||||
ExecObjectLayerAPIAnonTest(t, "TestListMultipartUploadsHandler", bucketName, "", instanceType, apiRouter, anonReq, getWriteOnlyBucketStatement)
|
||||
|
||||
// HTTP request for testing when `objectLayer` is set to `nil`.
|
||||
// There is no need to use an existing bucket and valid input for creating the request
|
||||
@@ -521,7 +523,7 @@ func TestListBucketsHandler(t *testing.T) {
|
||||
|
||||
// testListBucketsHandler - Tests validate listing of buckets.
|
||||
func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
credentials credential, t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
@@ -591,7 +593,7 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
|
||||
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
|
||||
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
|
||||
// unsigned request goes through and its validated again.
|
||||
ExecObjectLayerAPIAnonTest(t, obj, "ListBucketsHandler", "", "", instanceType, apiRouter, anonReq, getWriteOnlyObjectStatement)
|
||||
ExecObjectLayerAPIAnonTest(t, "ListBucketsHandler", "", "", instanceType, apiRouter, anonReq, getWriteOnlyObjectStatement)
|
||||
|
||||
// HTTP request for testing when `objectLayer` is set to `nil`.
|
||||
// There is no need to use an existing bucket and valid input for creating the request
|
||||
@@ -614,7 +616,8 @@ func TestAPIDeleteMultipleObjectsHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
credentials credential, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
var err error
|
||||
// register event notifier.
|
||||
@@ -629,7 +632,8 @@ 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, mustGetHashReader(t, bytes.NewBuffer(contentBytes), int64(len(contentBytes)), "", sha256sum), nil)
|
||||
_, err = obj.PutObject(bucketName, objectName, int64(len(contentBytes)), bytes.NewBuffer(contentBytes),
|
||||
make(map[string]string), sha256sum)
|
||||
// if object upload fails stop the test.
|
||||
if err != nil {
|
||||
t.Fatalf("Put Object %d: Error uploading object: <ERROR> %v", i, err)
|
||||
@@ -646,17 +650,6 @@ 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])},
|
||||
@@ -677,10 +670,6 @@ 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
|
||||
@@ -729,32 +718,15 @@ 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
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create HTTP request for DeleteMultipleObjects: <ERROR> %v", err)
|
||||
}
|
||||
@@ -781,6 +753,8 @@ 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.
|
||||
@@ -797,31 +771,3 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
// `ExecObjectLayerAPINilTest` manages the operation.
|
||||
ExecObjectLayerAPINilTest(t, nilBucket, nilObject, instanceType, apiRouter, nilReq)
|
||||
}
|
||||
|
||||
func TestIsBucketActionAllowed(t *testing.T) {
|
||||
ExecObjectLayerAPITest(t, testIsBucketActionAllowedHandler, []string{"BucketLocation"})
|
||||
}
|
||||
|
||||
func testIsBucketActionAllowedHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
// input.
|
||||
action string
|
||||
bucket string
|
||||
prefix string
|
||||
isGlobalPoliciesNil bool
|
||||
// flag indicating whether the test should pass.
|
||||
shouldPass bool
|
||||
}{
|
||||
{"s3:GetBucketLocation", "mybucket", "abc", true, false},
|
||||
{"s3:ListObject", "mybucket", "abc", false, false},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
isAllowed := isBucketActionAllowed(testCase.action, testCase.bucket, testCase.prefix, obj)
|
||||
if isAllowed != testCase.shouldPass {
|
||||
t.Errorf("Case %d: Expected the response status to be `%t`, but instead found `%t`", i+1, testCase.shouldPass, isAllowed)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package cmd
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// BucketMetaState - Interface to update bucket metadata in-memory
|
||||
// state.
|
||||
type BucketMetaState interface {
|
||||
@@ -77,7 +79,13 @@ func (lc *localBucketMetaState) UpdateBucketPolicy(args *SetBucketPolicyPeerArgs
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
return objAPI.RefreshBucketPolicy(args.Bucket)
|
||||
|
||||
var pCh policyChange
|
||||
if err := json.Unmarshal(args.PChBytes, &pCh); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return globalBucketPolicies.SetBucketPolicy(args.Bucket, pCh)
|
||||
}
|
||||
|
||||
// localBucketMetaState.SendEvent - sends event to local event notifier via
|
||||
|
||||
@@ -67,7 +67,6 @@ 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
|
||||
@@ -95,10 +94,6 @@ const (
|
||||
ObjectCreatedCompleteMultipartUpload
|
||||
// ObjectRemovedDelete is s3:ObjectRemoved:Delete
|
||||
ObjectRemovedDelete
|
||||
// ObjectAccessedGet is s3:ObjectAccessed:Get
|
||||
ObjectAccessedGet
|
||||
// ObjectAccessedHead is s3:ObjectAccessed:Head
|
||||
ObjectAccessedHead
|
||||
)
|
||||
|
||||
// Stringer interface for event name.
|
||||
@@ -114,10 +109,6 @@ func (eventName EventName) String() string {
|
||||
return "s3:ObjectCreated:CompleteMultipartUpload"
|
||||
case ObjectRemovedDelete:
|
||||
return "s3:ObjectRemoved:Delete"
|
||||
case ObjectAccessedGet:
|
||||
return "s3:ObjectAccessed:Get"
|
||||
case ObjectAccessedHead:
|
||||
return "s3:ObjectAccessed:Head"
|
||||
default:
|
||||
return "s3:Unknown"
|
||||
}
|
||||
@@ -137,13 +128,11 @@ 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"`
|
||||
UserMetadata map[string]string `json:"userMetadata,omitempty"`
|
||||
VersionID string `json:"versionId,omitempty"`
|
||||
Sequencer string `json:"sequencer"`
|
||||
Key string `json:"key"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
ETag string `json:"eTag,omitempty"`
|
||||
VersionID string `json:"versionId,omitempty"`
|
||||
Sequencer string `json:"sequencer"`
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -179,14 +168,6 @@ const (
|
||||
eventVersion = "2.0"
|
||||
)
|
||||
|
||||
// sourceInfo represents information on the client that triggered the
|
||||
// event notification.
|
||||
type sourceInfo struct {
|
||||
Host string `json:"host"`
|
||||
Port string `json:"port"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
}
|
||||
|
||||
// NotificationEvent represents an Amazon an S3 bucket notification event.
|
||||
type NotificationEvent struct {
|
||||
EventVersion string `json:"eventVersion"`
|
||||
@@ -198,7 +179,6 @@ type NotificationEvent struct {
|
||||
RequestParameters map[string]string `json:"requestParameters"`
|
||||
ResponseElements map[string]string `json:"responseElements"`
|
||||
S3 eventMeta `json:"s3"`
|
||||
Source sourceInfo `json:"source"`
|
||||
}
|
||||
|
||||
// Represents the minio sqs type and account id's.
|
||||
@@ -209,5 +189,5 @@ type arnSQS struct {
|
||||
|
||||
// Stringer for constructing AWS ARN compatible string.
|
||||
func (m arnSQS) String() string {
|
||||
return minioSqs + globalServerConfig.GetRegion() + ":" + m.AccountID + ":" + m.Type
|
||||
return minioSqs + serverConfig.GetRegion() + ":" + m.AccountID + ":" + m.Type
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,13 +22,10 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -43,18 +40,13 @@ const (
|
||||
// not enabled on the bucket, the operation returns an empty
|
||||
// NotificationConfiguration element.
|
||||
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if !objAPI.IsNotificationSupported() {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL)
|
||||
return
|
||||
}
|
||||
if s3Error := checkRequestAuthType(r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone {
|
||||
if s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -64,19 +56,20 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
||||
|
||||
_, err := objAPI.GetBucketInfo(bucket)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to find bucket info.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt to successfully load notification config.
|
||||
nConfig, err := loadNotificationConfig(bucket, objAPI)
|
||||
if err != nil && errors.Cause(err) != errNoSuchNotifications {
|
||||
if err != nil && 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 errors.Cause(err) == errNoSuchNotifications {
|
||||
if err == errNoSuchNotifications {
|
||||
// Complies with the s3 behavior in this regard.
|
||||
nConfig = ¬ificationConfig{}
|
||||
}
|
||||
@@ -101,18 +94,13 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
||||
// By default, your bucket has no event notifications configured. That is,
|
||||
// the notification configuration will be an empty NotificationConfiguration.
|
||||
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if !objectAPI.IsNotificationSupported() {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL)
|
||||
return
|
||||
}
|
||||
if s3Error := checkRequestAuthType(r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone {
|
||||
if s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -122,6 +110,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
||||
|
||||
_, err := objectAPI.GetBucketInfo(bucket)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to find bucket info.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -161,12 +150,6 @@ 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 {
|
||||
@@ -190,9 +173,7 @@ func PutBucketNotificationConfig(bucket string, ncfg *notificationConfig, objAPI
|
||||
// Acquire a write lock on bucket before modifying its
|
||||
// configuration.
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
if err := bucketLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
bucketLock.Lock()
|
||||
// Release lock after notifying peers
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
@@ -223,73 +204,39 @@ func writeNotification(w http.ResponseWriter, notification map[string][]Notifica
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add additional CRLF characters for client to
|
||||
// differentiate the individual events properly.
|
||||
_, err = w.Write(append(notificationBytes, crlf...))
|
||||
// Make sure we have flushed, this would set Transfer-Encoding: chunked.
|
||||
w.(http.Flusher).Flush()
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CRLF character used for chunked transfer in accordance with HTTP standards.
|
||||
var crlf = []byte("\r\n")
|
||||
|
||||
// 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)
|
||||
// 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.
|
||||
for {
|
||||
select {
|
||||
case events := <-l.dataCh:
|
||||
case events := <-arnListenerCh:
|
||||
if err := writeNotification(w, map[string][]NotificationEvent{"Records": events}); err != nil {
|
||||
logClientError(err, "Unable to write notification")
|
||||
errorIf(err, "Unable to write notification to client.")
|
||||
return
|
||||
}
|
||||
case <-time.After(globalSNSConnAlive):
|
||||
if err := writeNotification(w, emptyEvent); err != nil {
|
||||
logClientError(err, "Unable to write empty notification")
|
||||
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.")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -304,11 +251,8 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
if !objAPI.IsNotificationSupported() {
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL)
|
||||
return
|
||||
}
|
||||
if s3Error := checkRequestAuthType(r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone {
|
||||
|
||||
if s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -339,21 +283,20 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
|
||||
_, err := objAPI.GetBucketInfo(bucket)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to get bucket info.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
targetServer := GetLocalPeer(globalEndpoints)
|
||||
accountID := fmt.Sprintf("%d", UTCNow().UnixNano())
|
||||
accountID := fmt.Sprintf("%d", time.Now().UTC().UnixNano())
|
||||
accountARN := fmt.Sprintf(
|
||||
"%s:%s:%s:%s-%s",
|
||||
minioTopic,
|
||||
globalServerConfig.GetRegion(),
|
||||
serverConfig.GetRegion(),
|
||||
accountID,
|
||||
snsTypeMinio,
|
||||
targetServer,
|
||||
globalMinioAddr,
|
||||
)
|
||||
|
||||
var filterRules []filterRule
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
@@ -387,11 +330,12 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
},
|
||||
}
|
||||
|
||||
// Setup a listen channel to receive notifications like
|
||||
// s3:ObjectCreated, s3:ObjectDeleted etc.
|
||||
nListenCh := newListenChan()
|
||||
// Setup a listening channel that will receive notifications
|
||||
// from the RPC handler.
|
||||
nEventCh := make(chan []NotificationEvent)
|
||||
defer close(nEventCh)
|
||||
// Add channel for listener events
|
||||
if err = globalEventNotifier.AddListenerChan(accountARN, nListenCh); err != nil {
|
||||
if err = globalEventNotifier.AddListenerChan(accountARN, nEventCh); err != nil {
|
||||
errorIf(err, "Error adding a listener!")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -401,11 +345,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 completes, events may start appearing in
|
||||
// nListenCh
|
||||
// as this call compelets, events may start appearing in
|
||||
// nEventCh
|
||||
lc := listenerConfig{
|
||||
TopicConfig: *topicCfg,
|
||||
TargetServer: targetServer,
|
||||
TargetServer: globalMinioAddr,
|
||||
}
|
||||
|
||||
err = AddBucketListenerConfig(bucket, &lc, objAPI)
|
||||
@@ -418,16 +362,8 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
// Add all common headers.
|
||||
setCommonHeaders(w)
|
||||
|
||||
// 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)
|
||||
// Start sending bucket notifications.
|
||||
sendBucketNotification(w, nEventCh)
|
||||
}
|
||||
|
||||
// AddBucketListenerConfig - Updates on disk state of listeners, and
|
||||
@@ -444,9 +380,7 @@ func AddBucketListenerConfig(bucket string, lcfg *listenerConfig, objAPI ObjectL
|
||||
// Acquire a write lock on bucket before modifying its
|
||||
// configuration.
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
if err := bucketLock.GetLock(globalOperationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
bucketLock.Lock()
|
||||
// Release lock after notifying peers
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
@@ -487,9 +421,7 @@ func RemoveBucketListenerConfig(bucket string, lcfg *listenerConfig, objAPI Obje
|
||||
// Acquire a write lock on bucket before modifying its
|
||||
// configuration.
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
if bucketLock.GetLock(globalOperationTimeout) != nil {
|
||||
return
|
||||
}
|
||||
bucketLock.Lock()
|
||||
// Release lock after notifying peers
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
|
||||
@@ -21,19 +21,12 @@ 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.
|
||||
@@ -58,7 +51,7 @@ func TestWriteNotification(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize test config %s", err)
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
defer removeAll(root)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
// Collection of test cases for each event writer.
|
||||
@@ -117,56 +110,26 @@ func TestWriteNotification(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) {
|
||||
func TestSendBucketNotification(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 os.RemoveAll(root)
|
||||
defer removeAll(root)
|
||||
|
||||
// Create a listen channel to manage notifications
|
||||
nListenCh := newListenChan()
|
||||
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)
|
||||
|
||||
// Construct notification events to be passed on the events channel.
|
||||
var events []NotificationEvent
|
||||
@@ -176,68 +139,37 @@ func TestListenChan(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
|
||||
|
||||
// Send notification events one-by-one
|
||||
go func() {
|
||||
for _, event := range events {
|
||||
nListenCh.sendNotificationEvent([]NotificationEvent{event})
|
||||
}
|
||||
}()
|
||||
// Read from the pipe connected to the ResponseWriter.
|
||||
scanner.Scan()
|
||||
notificationBytes := scanner.Bytes()
|
||||
|
||||
// Create a http.ResponseWriter that fails after len(events)
|
||||
// number of times
|
||||
trw := newTestResponseWriter(len(events))
|
||||
// Close the read-end and send an empty notification event on the channel
|
||||
// to signal sendBucketNotification to terminate.
|
||||
pr.Close()
|
||||
eventCh <- []NotificationEvent{}
|
||||
close(eventCh)
|
||||
|
||||
// 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)
|
||||
}
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
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{{}}
|
||||
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
|
||||
records := notifications["Records"]
|
||||
for i, rec := range records {
|
||||
if rec.EventName == evTypes[i].String() {
|
||||
continue
|
||||
}
|
||||
t.Errorf("Failed to receive %d event %s", i, evTypes[i].String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,12 +180,12 @@ func TestGetBucketNotificationHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testGetBucketNotificationHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
credentials credential, t *testing.T) {
|
||||
// declare sample configs
|
||||
filterRules := []filterRule{
|
||||
{
|
||||
Name: "prefix",
|
||||
Value: "minio",
|
||||
Value: globalMinioDefaultOwnerID,
|
||||
},
|
||||
{
|
||||
Name: "suffix",
|
||||
@@ -314,95 +246,6 @@ 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",
|
||||
@@ -411,7 +254,7 @@ func TestListenBucketNotificationNilHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
func testListenBucketNotificationNilHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
credentials credential, t *testing.T) {
|
||||
// get random bucket name.
|
||||
randBucket := getRandomBucketName()
|
||||
|
||||
@@ -425,7 +268,6 @@ func testListenBucketNotificationNilHandler(obj ObjectLayer, instanceType, bucke
|
||||
[]string{"*.jpg"}, []string{
|
||||
"s3:ObjectCreated:*",
|
||||
"s3:ObjectRemoved:*",
|
||||
"s3:ObjectAccessed:*",
|
||||
}), 0, nil, credentials.AccessKey, credentials.SecretKey)
|
||||
if tErr != nil {
|
||||
t.Fatalf("%s: Failed to create HTTP testRequest for ListenBucketNotification: <ERROR> %v", instanceType, tErr)
|
||||
@@ -437,28 +279,26 @@ func testListenBucketNotificationNilHandler(obj ObjectLayer, instanceType, bucke
|
||||
}
|
||||
}
|
||||
|
||||
func testRemoveNotificationConfig(obj ObjectLayer, instanceType,
|
||||
bucketName string, apiRouter http.Handler, credentials auth.Credentials,
|
||||
t *testing.T) {
|
||||
|
||||
func testRemoveNotificationConfig(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials credential, t *testing.T) {
|
||||
invalidBucket := "Invalid\\Bucket"
|
||||
// get random bucket name.
|
||||
randBucket := bucketName
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
apiRouter.ServeHTTP(testRec, testReq)
|
||||
|
||||
testCases := []struct {
|
||||
bucketName string
|
||||
|
||||
@@ -16,12 +16,7 @@
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
)
|
||||
import "strings"
|
||||
|
||||
// List of valid event types.
|
||||
var suppportedEventTypes = map[string]struct{}{
|
||||
@@ -34,9 +29,6 @@ var suppportedEventTypes = map[string]struct{}{
|
||||
// Object removed event types.
|
||||
"s3:ObjectRemoved:*": {},
|
||||
"s3:ObjectRemoved:Delete": {},
|
||||
"s3:ObjectAccessed:Get": {},
|
||||
"s3:ObjectAccessed:Head": {},
|
||||
"s3:ObjectAccessed:*": {},
|
||||
}
|
||||
|
||||
// checkEvent - checks if an event is supported.
|
||||
@@ -112,25 +104,20 @@ func checkARN(arn, arnType string) APIErrorCode {
|
||||
if !strings.HasPrefix(arn, arnType) {
|
||||
return ErrARNNotification
|
||||
}
|
||||
strs := strings.SplitN(arn, ":", -1)
|
||||
if len(strs) != 6 {
|
||||
return ErrARNNotification
|
||||
if !strings.HasPrefix(arn, arnType+serverConfig.GetRegion()+":") {
|
||||
return ErrRegionNotification
|
||||
}
|
||||
|
||||
// Server region is allowed to be empty by default,
|
||||
// in such a scenario ARN region is not validating
|
||||
// allowing all regions.
|
||||
if sregion := globalServerConfig.GetRegion(); sregion != "" {
|
||||
region := strs[3]
|
||||
if region != sregion {
|
||||
return ErrRegionNotification
|
||||
account := strings.SplitN(strings.TrimPrefix(arn, arnType+serverConfig.GetRegion()+":"), ":", 2)
|
||||
switch len(account) {
|
||||
case 1:
|
||||
// This means ARN is malformed, account should have min of 2elements.
|
||||
return ErrARNNotification
|
||||
case 2:
|
||||
// Account topic id or topic name cannot be empty.
|
||||
if account[0] == "" || account[1] == "" {
|
||||
return ErrARNNotification
|
||||
}
|
||||
}
|
||||
accountID := strs[4]
|
||||
resource := strs[5]
|
||||
if accountID == "" || resource == "" {
|
||||
return ErrARNNotification
|
||||
}
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
@@ -146,34 +133,27 @@ func isValidQueueID(queueARN string) bool {
|
||||
// Is Queue identifier valid?.
|
||||
|
||||
if isAMQPQueue(sqsARN) { // AMQP eueue.
|
||||
amqpN := globalServerConfig.Notify.GetAMQPByID(sqsARN.AccountID)
|
||||
amqpN := serverConfig.Notify.GetAMQPByID(sqsARN.AccountID)
|
||||
return amqpN.Enable && amqpN.URL != ""
|
||||
} else if isMQTTQueue(sqsARN) {
|
||||
mqttN := globalServerConfig.Notify.GetMQTTByID(sqsARN.AccountID)
|
||||
return mqttN.Enable && mqttN.Broker != ""
|
||||
} else if isNATSQueue(sqsARN) {
|
||||
natsN := globalServerConfig.Notify.GetNATSByID(sqsARN.AccountID)
|
||||
natsN := serverConfig.Notify.GetNATSByID(sqsARN.AccountID)
|
||||
return natsN.Enable && natsN.Address != ""
|
||||
} else if isElasticQueue(sqsARN) { // Elastic queue.
|
||||
elasticN := globalServerConfig.Notify.GetElasticSearchByID(sqsARN.AccountID)
|
||||
elasticN := serverConfig.Notify.GetElasticSearchByID(sqsARN.AccountID)
|
||||
return elasticN.Enable && elasticN.URL != ""
|
||||
} else if isRedisQueue(sqsARN) { // Redis queue.
|
||||
redisN := globalServerConfig.Notify.GetRedisByID(sqsARN.AccountID)
|
||||
redisN := serverConfig.Notify.GetRedisByID(sqsARN.AccountID)
|
||||
return redisN.Enable && redisN.Addr != ""
|
||||
} else if isPostgreSQLQueue(sqsARN) {
|
||||
pgN := globalServerConfig.Notify.GetPostgreSQLByID(sqsARN.AccountID)
|
||||
pgN := serverConfig.Notify.GetPostgreSQLByID(sqsARN.AccountID)
|
||||
// Postgres can work with only default conn. info.
|
||||
return pgN.Enable
|
||||
} else if isMySQLQueue(sqsARN) {
|
||||
msqlN := globalServerConfig.Notify.GetMySQLByID(sqsARN.AccountID)
|
||||
// Mysql can work with only default conn. info.
|
||||
return msqlN.Enable
|
||||
} else if isKafkaQueue(sqsARN) {
|
||||
kafkaN := globalServerConfig.Notify.GetKafkaByID(sqsARN.AccountID)
|
||||
kafkaN := serverConfig.Notify.GetKafkaByID(sqsARN.AccountID)
|
||||
return (kafkaN.Enable && len(kafkaN.Brokers) > 0 &&
|
||||
kafkaN.Topic != "")
|
||||
} else if isWebhookQueue(sqsARN) {
|
||||
webhookN := globalServerConfig.Notify.GetWebhookByID(sqsARN.AccountID)
|
||||
webhookN := serverConfig.Notify.GetWebhookByID(sqsARN.AccountID)
|
||||
return webhookN.Enable && webhookN.Endpoint != ""
|
||||
}
|
||||
return false
|
||||
@@ -220,14 +200,16 @@ func validateQueueConfigs(queueConfigs []queueConfig) APIErrorCode {
|
||||
|
||||
// Check all the queue configs for any duplicates.
|
||||
func checkDuplicateQueueConfigs(configs []queueConfig) APIErrorCode {
|
||||
queueConfigARNS := set.NewStringSet()
|
||||
var queueConfigARNS []string
|
||||
|
||||
// Navigate through each configs and count the entries.
|
||||
for _, config := range configs {
|
||||
queueConfigARNS.Add(config.QueueARN)
|
||||
queueConfigARNS = append(queueConfigARNS, config.QueueARN)
|
||||
}
|
||||
|
||||
if len(queueConfigARNS) != len(configs) {
|
||||
// Check if there are any duplicate counts.
|
||||
if err := checkDuplicateStrings(queueConfigARNS); err != nil {
|
||||
errorIf(err, "Invalid queue configs found.")
|
||||
return ErrOverlappingConfigs
|
||||
}
|
||||
|
||||
@@ -239,12 +221,6 @@ 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
|
||||
@@ -264,54 +240,34 @@ func validateNotificationConfig(nConfig notificationConfig) APIErrorCode {
|
||||
// Unmarshals input value of AWS ARN format into minioSqs object.
|
||||
// Returned value represents minio sqs types, currently supported are
|
||||
// - amqp
|
||||
// - mqtt
|
||||
// - nats
|
||||
// - elasticsearch
|
||||
// - redis
|
||||
// - postgresql
|
||||
// - mysql
|
||||
// - kafka
|
||||
// - webhook
|
||||
func unmarshalSqsARN(queueARN string) (mSqs arnSQS) {
|
||||
strs := strings.SplitN(queueARN, ":", -1)
|
||||
if len(strs) != 6 {
|
||||
return
|
||||
mSqs = arnSQS{}
|
||||
if !strings.HasPrefix(queueARN, minioSqs+serverConfig.GetRegion()+":") {
|
||||
return mSqs
|
||||
}
|
||||
|
||||
// Server region is allowed to be empty by default,
|
||||
// in such a scenario ARN region is not validating
|
||||
// allowing all regions.
|
||||
if sregion := globalServerConfig.GetRegion(); sregion != "" {
|
||||
region := strs[3]
|
||||
if region != sregion {
|
||||
return
|
||||
}
|
||||
}
|
||||
sqsType := strs[5]
|
||||
switch sqsType {
|
||||
case queueTypeAMQP:
|
||||
sqsType := strings.TrimPrefix(queueARN, minioSqs+serverConfig.GetRegion()+":")
|
||||
switch {
|
||||
case strings.HasSuffix(sqsType, queueTypeAMQP):
|
||||
mSqs.Type = queueTypeAMQP
|
||||
case queueTypeMQTT:
|
||||
mSqs.Type = queueTypeMQTT
|
||||
case queueTypeNATS:
|
||||
case strings.HasSuffix(sqsType, queueTypeNATS):
|
||||
mSqs.Type = queueTypeNATS
|
||||
case queueTypeElastic:
|
||||
case strings.HasSuffix(sqsType, queueTypeElastic):
|
||||
mSqs.Type = queueTypeElastic
|
||||
case queueTypeRedis:
|
||||
case strings.HasSuffix(sqsType, queueTypeRedis):
|
||||
mSqs.Type = queueTypeRedis
|
||||
case queueTypePostgreSQL:
|
||||
case strings.HasSuffix(sqsType, queueTypePostgreSQL):
|
||||
mSqs.Type = queueTypePostgreSQL
|
||||
case queueTypeMySQL:
|
||||
mSqs.Type = queueTypeMySQL
|
||||
case queueTypeKafka:
|
||||
case strings.HasSuffix(sqsType, queueTypeKafka):
|
||||
mSqs.Type = queueTypeKafka
|
||||
case queueTypeWebhook:
|
||||
case strings.HasSuffix(sqsType, queueTypeWebhook):
|
||||
mSqs.Type = queueTypeWebhook
|
||||
default:
|
||||
errorIf(errors.New("invalid SQS type"), "SQS type: %s", sqsType)
|
||||
} // Add more queues here.
|
||||
|
||||
mSqs.AccountID = strs[4]
|
||||
|
||||
return
|
||||
mSqs.AccountID = strings.TrimSuffix(sqsType, ":"+mSqs.Type)
|
||||
return mSqs
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -223,7 +222,7 @@ func TestQueueARN(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
defer removeAll(rootPath)
|
||||
|
||||
testCases := []struct {
|
||||
queueARN string
|
||||
@@ -260,6 +259,11 @@ func TestQueueARN(t *testing.T) {
|
||||
queueARN: "arn:minio:sns:us-east-1:1:listen",
|
||||
errCode: ErrARNNotification,
|
||||
},
|
||||
// Invalid region 'us-west-1' in queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-west-1:1:redis",
|
||||
errCode: ErrRegionNotification,
|
||||
},
|
||||
// Invalid queue name empty in queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:",
|
||||
@@ -294,37 +298,6 @@ func TestQueueARN(t *testing.T) {
|
||||
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Test when server region is set.
|
||||
rootPath, err = newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
testCases = []struct {
|
||||
queueARN string
|
||||
errCode APIErrorCode
|
||||
}{
|
||||
// Incorrect region should produce error.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-west-1:1:webhook",
|
||||
errCode: ErrRegionNotification,
|
||||
},
|
||||
// Correct region should not produce error.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:webhook",
|
||||
errCode: ErrNone,
|
||||
},
|
||||
}
|
||||
|
||||
// Validate all tests for queue arn.
|
||||
for i, testCase := range testCases {
|
||||
errCode := checkQueueARN(testCase.queueARN)
|
||||
if testCase.errCode != errCode {
|
||||
t.Errorf("Test %d: Expected \"%d\", got \"%d\"", i+1, testCase.errCode, errCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test unmarshal queue arn.
|
||||
@@ -333,7 +306,7 @@ func TestUnmarshalSQSARN(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
defer removeAll(rootPath)
|
||||
|
||||
testCases := []struct {
|
||||
queueARN string
|
||||
@@ -359,16 +332,16 @@ func TestUnmarshalSQSARN(t *testing.T) {
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:amqp",
|
||||
Type: "amqp",
|
||||
},
|
||||
// Valid mqtt queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:mqtt",
|
||||
Type: "mqtt",
|
||||
},
|
||||
// Invalid empty queue arn.
|
||||
{
|
||||
queueARN: "",
|
||||
Type: "",
|
||||
},
|
||||
// Invalid region 'us-west-1' in queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-west-1:1:redis",
|
||||
Type: "",
|
||||
},
|
||||
// Partial queue arn.
|
||||
{
|
||||
queueARN: "arn:minio:sqs:",
|
||||
@@ -388,33 +361,4 @@ func TestUnmarshalSQSARN(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test when the server region is set.
|
||||
rootPath, err = newTestConfig("us-east-1")
|
||||
if err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
testCases = []struct {
|
||||
queueARN string
|
||||
Type string
|
||||
}{
|
||||
// Incorrect region in ARN returns empty mSqs.Type
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-west-1:1:webhook",
|
||||
Type: "",
|
||||
},
|
||||
// Correct regionin ARN returns valid mSqs.Type
|
||||
{
|
||||
queueARN: "arn:minio:sqs:us-east-1:1:webhook",
|
||||
Type: "webhook",
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
mSqs := unmarshalSqsARN(testCase.queueARN)
|
||||
if testCase.Type != mSqs.Type {
|
||||
t.Errorf("Test %d: Expected \"%s\", got \"%s\"", i+1, testCase.Type, mSqs.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,18 +17,16 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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/policy"
|
||||
"github.com/minio/minio/pkg/errors"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
)
|
||||
|
||||
@@ -37,8 +35,7 @@ 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 policy.ConditionKeyMap,
|
||||
statements []policy.Statement) bool {
|
||||
func bucketPolicyEvalStatements(action string, resource string, conditions map[string]set.StringSet, statements []policyStatement) bool {
|
||||
for _, statement := range statements {
|
||||
if bucketPolicyMatchStatement(action, resource, conditions, statement) {
|
||||
if statement.Effect == "Allow" {
|
||||
@@ -54,8 +51,7 @@ func bucketPolicyEvalStatements(action string, resource string, conditions polic
|
||||
}
|
||||
|
||||
// Verify if action, resource and conditions match input policy statement.
|
||||
func bucketPolicyMatchStatement(action string, resource string, conditions policy.ConditionKeyMap,
|
||||
statement policy.Statement) bool {
|
||||
func bucketPolicyMatchStatement(action string, resource string, conditions map[string]set.StringSet, statement policyStatement) bool {
|
||||
// Verify if action, resource and condition match in given statement.
|
||||
return (bucketPolicyActionMatch(action, statement) &&
|
||||
bucketPolicyResourceMatch(resource, statement) &&
|
||||
@@ -63,7 +59,7 @@ func bucketPolicyMatchStatement(action string, resource string, conditions polic
|
||||
}
|
||||
|
||||
// Verify if given action matches with policy statement.
|
||||
func bucketPolicyActionMatch(action string, statement policy.Statement) bool {
|
||||
func bucketPolicyActionMatch(action string, statement policyStatement) bool {
|
||||
return !statement.Actions.FuncMatch(actionMatch, action).IsEmpty()
|
||||
}
|
||||
|
||||
@@ -85,20 +81,8 @@ 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 policy.Statement) bool {
|
||||
func bucketPolicyResourceMatch(resource string, statement policyStatement) 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.
|
||||
@@ -107,20 +91,17 @@ func bucketPolicyResourceMatch(resource string, statement policy.Statement) bool
|
||||
}
|
||||
|
||||
// Verify if given condition matches with policy statement.
|
||||
func bucketPolicyConditionMatch(conditions policy.ConditionKeyMap, statement policy.Statement) bool {
|
||||
func bucketPolicyConditionMatch(conditions map[string]set.StringSet, statement policyStatement) 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
|
||||
@@ -178,37 +159,6 @@ func bucketPolicyConditionMatch(conditions policy.ConditionKeyMap, statement pol
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +176,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone {
|
||||
if s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -237,6 +187,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
// Before proceeding validate if bucket exists.
|
||||
_, err := objAPI.GetBucketInfo(bucket)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to find bucket info.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -262,26 +213,10 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
policyInfo := policy.BucketAccessPolicy{}
|
||||
if err = json.Unmarshal(policyBytes, &policyInfo); err != nil {
|
||||
writeErrorResponse(w, ErrInvalidPolicyDocument, r.URL)
|
||||
return
|
||||
}
|
||||
// Parse check bucket policy.
|
||||
if s3Error := checkBucketPolicyResources(bucket, policyInfo); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = objAPI.SetBucketPolicy(bucket, policyInfo); err != nil {
|
||||
err = errors.Cause(err)
|
||||
switch err.(type) {
|
||||
case NotImplemented:
|
||||
// Return error for invalid bucket name.
|
||||
writeErrorResponse(w, ErrNotImplemented, r.URL)
|
||||
default:
|
||||
writeErrorResponse(w, ErrMalformedPolicy, r.URL)
|
||||
}
|
||||
// Parse validate and save bucket policy.
|
||||
if s3Error := parseAndPersistBucketPolicy(bucket, policyBytes, objAPI); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -300,7 +235,7 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone {
|
||||
if s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -311,14 +246,20 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
||||
// Before proceeding validate if bucket exists.
|
||||
_, err := objAPI.GetBucketInfo(bucket)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to find bucket info.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete bucket access policy, by passing an empty policy
|
||||
// struct.
|
||||
if err := objAPI.DeleteBucketPolicy(bucket); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
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)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -337,7 +278,7 @@ func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(r, "", "", globalServerConfig.GetRegion()); s3Error != ErrNone {
|
||||
if s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion()); s3Error != ErrNone {
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -348,24 +289,24 @@ func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
// Before proceeding validate if bucket exists.
|
||||
_, err := objAPI.GetBucketInfo(bucket)
|
||||
if err != nil {
|
||||
errorIf(err, "Unable to find bucket info.")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Read bucket access policy.
|
||||
policy, err := objAPI.GetBucketPolicy(bucket)
|
||||
policy, err := readBucketPolicy(bucket, objAPI)
|
||||
if err != nil {
|
||||
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)
|
||||
errorIf(err, "Unable to read bucket policy.")
|
||||
switch err.(type) {
|
||||
case BucketPolicyNotFound:
|
||||
writeErrorResponse(w, ErrNoSuchBucketPolicy, r.URL)
|
||||
default:
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Write to client.
|
||||
w.Write(policyBytes)
|
||||
fmt.Fprint(w, policy)
|
||||
}
|
||||
|
||||
@@ -25,17 +25,15 @@ 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) policy.Statement {
|
||||
statement := policy.Statement{}
|
||||
generateStatement := func(resource string) policyStatement {
|
||||
statement := policyStatement{}
|
||||
statement.Resources = set.CreateStringSet([]string{resource}...)
|
||||
return statement
|
||||
}
|
||||
@@ -47,7 +45,7 @@ func TestBucketPolicyResourceMatch(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
resourceToMatch string
|
||||
statement policy.Statement
|
||||
statement policyStatement
|
||||
expectedResourceMatch bool
|
||||
}{
|
||||
// Test case 1-4.
|
||||
@@ -87,7 +85,7 @@ func TestBucketPolicyResourceMatch(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestBucketPolicyActionMatch - Test validates whether given action on the
|
||||
// bucket/object matches the allowed actions in policy.Statement.
|
||||
// bucket/object matches the allowed actions in policyStatement.
|
||||
// This test preserves the allowed actions for all 3 sets of policies, that is read-write,read-only, write-only.
|
||||
// The intention of the test is to catch any changes made to allowed action for on eof the above 3 major policy groups mentioned.
|
||||
func TestBucketPolicyActionMatch(t *testing.T) {
|
||||
@@ -96,7 +94,7 @@ func TestBucketPolicyActionMatch(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
action string
|
||||
statement policy.Statement
|
||||
statement policyStatement
|
||||
expectedResult bool
|
||||
}{
|
||||
// s3:GetBucketLocation is the action necessary to be present in the bucket policy to allow
|
||||
@@ -249,10 +247,11 @@ func TestPutBucketPolicyHandler(t *testing.T) {
|
||||
|
||||
// testPutBucketPolicyHandler - Test for Bucket policy end point.
|
||||
func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
credentials credential, t *testing.T) {
|
||||
initBucketPolicies(obj)
|
||||
|
||||
bucketName1 := fmt.Sprintf("%s-1", bucketName)
|
||||
if err := obj.MakeBucketWithLocation(bucketName1, ""); err != nil {
|
||||
if err := obj.MakeBucket(bucketName1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -429,7 +428,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
||||
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
|
||||
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
|
||||
// unsigned request goes through and its validated again.
|
||||
ExecObjectLayerAPIAnonTest(t, obj, "PutBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getWriteOnlyObjectStatement)
|
||||
ExecObjectLayerAPIAnonTest(t, "PutBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getWriteOnlyObjectStatement)
|
||||
|
||||
// HTTP request for testing when `objectLayer` is set to `nil`.
|
||||
// There is no need to use an existing bucket and valid input for creating the request
|
||||
@@ -446,7 +445,6 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
||||
// execute the object layer set to `nil` test.
|
||||
// `ExecObjectLayerAPINilTest` manages the operation.
|
||||
ExecObjectLayerAPINilTest(t, nilBucket, "", instanceType, apiRouter, nilReq)
|
||||
|
||||
}
|
||||
|
||||
// Wrapper for calling Get Bucket Policy HTTP handler tests for both XL multiple disks and single node setup.
|
||||
@@ -456,7 +454,10 @@ 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 auth.Credentials, t *testing.T) {
|
||||
credentials credential, t *testing.T) {
|
||||
// initialize bucket policy.
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// template for constructing HTTP request body for PUT bucket policy.
|
||||
bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Action":["s3:GetBucketLocation","s3:ListBucket"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s"],"Sid":""},{"Action":["s3:GetObject"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s/this*"],"Sid":""}]}`
|
||||
|
||||
@@ -616,7 +617,7 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
||||
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
|
||||
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
|
||||
// unsigned request goes through and its validated again.
|
||||
ExecObjectLayerAPIAnonTest(t, obj, "GetBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getReadOnlyObjectStatement)
|
||||
ExecObjectLayerAPIAnonTest(t, "GetBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getReadOnlyObjectStatement)
|
||||
|
||||
// HTTP request for testing when `objectLayer` is set to `nil`.
|
||||
// There is no need to use an existing bucket and valid input for creating the request
|
||||
@@ -642,7 +643,9 @@ func TestDeleteBucketPolicyHandler(t *testing.T) {
|
||||
|
||||
// testDeleteBucketPolicyHandler - Test for Delete bucket policy end point.
|
||||
func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
credentials credential, t *testing.T) {
|
||||
// initialize bucket policy.
|
||||
initBucketPolicies(obj)
|
||||
|
||||
// template for constructing HTTP request body for PUT bucket policy.
|
||||
bucketPolicyTemplate := `{
|
||||
@@ -819,7 +822,7 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
||||
// ExecObjectLayerAPIAnonTest - Calls the HTTP API handler using the anonymous request, validates the ErrAccessDeniedResponse,
|
||||
// sets the bucket policy using the policy statement generated from `getWriteOnlyObjectStatement` so that the
|
||||
// unsigned request goes through and its validated again.
|
||||
ExecObjectLayerAPIAnonTest(t, obj, "DeleteBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getReadOnlyObjectStatement)
|
||||
ExecObjectLayerAPIAnonTest(t, "DeleteBucketPolicyHandler", bucketName, "", instanceType, apiRouter, anonReq, getReadOnlyObjectStatement)
|
||||
|
||||
// HTTP request for testing when `objectLayer` is set to `nil`.
|
||||
// There is no need to use an existing bucket and valid input for creating the request
|
||||
@@ -840,28 +843,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 policy.Statement.Conditions.
|
||||
// obtain the inner map[string]set.StringSet for policyStatement.Conditions .
|
||||
getInnerMap := func(key2, value string) map[string]set.StringSet {
|
||||
innerMap := make(map[string]set.StringSet)
|
||||
innerMap[key2] = set.CreateStringSet(value)
|
||||
return innerMap
|
||||
}
|
||||
|
||||
// obtain policy.Statement with Conditions set.
|
||||
getStatementWithCondition := func(key1, key2, value string) policy.Statement {
|
||||
// obtain policyStatement with Conditions set.
|
||||
getStatementWithCondition := func(key1, key2, value string) policyStatement {
|
||||
innerMap := getInnerMap(key2, value)
|
||||
// to set policyStatment.Conditions .
|
||||
conditions := make(policy.ConditionMap)
|
||||
conditions := make(map[string]map[string]set.StringSet)
|
||||
conditions[key1] = innerMap
|
||||
// new policy statement.
|
||||
statement := policy.Statement{}
|
||||
statement := policyStatement{}
|
||||
// set the condition.
|
||||
statement.Conditions = conditions
|
||||
return statement
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
statementCondition policy.Statement
|
||||
statementCondition policyStatement
|
||||
condition map[string]set.StringSet
|
||||
|
||||
expectedMatch bool
|
||||
@@ -967,34 +970,6 @@ 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 {
|
||||
|
||||
+98
-35
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -26,16 +26,12 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
)
|
||||
|
||||
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"),
|
||||
var conditionKeyActionMap = map[string]set.StringSet{
|
||||
"s3:prefix": set.CreateStringSet("s3:ListBucket"),
|
||||
"s3:max-keys": set.CreateStringSet("s3:ListBucket"),
|
||||
}
|
||||
|
||||
// supportedActionMap - lists all the actions supported by minio.
|
||||
@@ -44,25 +40,50 @@ var supportedActionMap = set.CreateStringSet("*", "s3:*", "s3:GetObject",
|
||||
"s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts")
|
||||
|
||||
// supported Conditions type.
|
||||
var supportedConditionsType = set.CreateStringSet("StringEquals", "StringNotEquals", "StringLike", "StringNotLike", "IpAddress", "NotIpAddress")
|
||||
var supportedConditionsType = set.CreateStringSet("StringEquals", "StringNotEquals", "StringLike", "StringNotLike")
|
||||
|
||||
// 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", "aws:SourceIp")
|
||||
var supportedConditionsKey = set.CreateStringSet("s3:prefix", "s3:max-keys", "aws:Referer")
|
||||
|
||||
// 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 actions.IsEmpty() {
|
||||
if len(actions) == 0 {
|
||||
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
|
||||
@@ -85,7 +106,7 @@ func isValidEffect(effect string) (err error) {
|
||||
// isValidResources - are valid resources.
|
||||
func isValidResources(resources set.StringSet) (err error) {
|
||||
// Statement resources cannot be empty.
|
||||
if resources.IsEmpty() {
|
||||
if len(resources) == 0 {
|
||||
err = errors.New("Resource list cannot be empty")
|
||||
return err
|
||||
}
|
||||
@@ -103,17 +124,60 @@ func isValidResources(resources set.StringSet) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidPrincipals - are valid principals.
|
||||
func isValidPrincipals(principal policy.User) (err error) {
|
||||
if principal.AWS.IsEmpty() {
|
||||
return errors.New("Principal cannot be empty")
|
||||
// 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)
|
||||
}
|
||||
if diff := principal.AWS.Difference(set.CreateStringSet("*")); !diff.IsEmpty() {
|
||||
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
|
||||
}
|
||||
if unsuppPrincipals := principals.Difference(set.CreateStringSet([]string{"*"}...)); !unsuppPrincipals.IsEmpty() {
|
||||
// Minio does not support or implement IAM, "*" is the only valid value.
|
||||
// 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)
|
||||
// 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)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -121,7 +185,7 @@ func isValidPrincipals(principal policy.User) (err error) {
|
||||
|
||||
// isValidConditions - returns nil if the given conditions valid and
|
||||
// corresponding error otherwise.
|
||||
func isValidConditions(actions set.StringSet, conditions policy.ConditionMap) (err error) {
|
||||
func isValidConditions(actions set.StringSet, conditions map[string]map[string]set.StringSet) (err error) {
|
||||
// Verify conditions should be valid. Validate if only
|
||||
// supported condition keys are present and return error
|
||||
// otherwise.
|
||||
@@ -175,7 +239,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 policy.BucketAccessPolicy) APIErrorCode {
|
||||
func checkBucketPolicyResources(bucket string, bucketPolicy *bucketPolicy) APIErrorCode {
|
||||
// Validate statements for special actions and collect resources
|
||||
// for others to validate nesting.
|
||||
var resourceMap = set.NewStringSet()
|
||||
@@ -230,27 +294,27 @@ func checkBucketPolicyResources(bucket string, bucketPolicy policy.BucketAccessP
|
||||
|
||||
// parseBucketPolicy - parses and validates if bucket policy is of
|
||||
// proper JSON and follows allowed restrictions with policy standards.
|
||||
func parseBucketPolicy(bucketPolicyReader io.Reader, bktPolicy *policy.BucketAccessPolicy) (err error) {
|
||||
func parseBucketPolicy(bucketPolicyReader io.Reader, policy *bucketPolicy) (err error) {
|
||||
// Parse bucket policy reader.
|
||||
decoder := json.NewDecoder(bucketPolicyReader)
|
||||
if err = decoder.Decode(bktPolicy); err != nil {
|
||||
if err = decoder.Decode(&policy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Policy version cannot be empty.
|
||||
if len(bktPolicy.Version) == 0 {
|
||||
if len(policy.Version) == 0 {
|
||||
err = errors.New("Policy version cannot be empty")
|
||||
return err
|
||||
}
|
||||
|
||||
// Policy statements cannot be empty.
|
||||
if len(bktPolicy.Statements) == 0 {
|
||||
if len(policy.Statements) == 0 {
|
||||
err = errors.New("Policy statement cannot be empty")
|
||||
return err
|
||||
}
|
||||
|
||||
// Loop through all policy statements and validate entries.
|
||||
for _, statement := range bktPolicy.Statements {
|
||||
for _, statement := range policy.Statements {
|
||||
// Statement effect should be valid.
|
||||
if err := isValidEffect(statement.Effect); err != nil {
|
||||
return err
|
||||
@@ -275,20 +339,19 @@ func parseBucketPolicy(bucketPolicyReader io.Reader, bktPolicy *policy.BucketAcc
|
||||
|
||||
// Separate deny and allow statements, so that we can apply deny
|
||||
// statements in the beginning followed by Allow statements.
|
||||
var denyStatements []policy.Statement
|
||||
var allowStatements []policy.Statement
|
||||
for _, statement := range bktPolicy.Statements {
|
||||
var denyStatements []policyStatement
|
||||
var allowStatements []policyStatement
|
||||
for _, statement := range policy.Statements {
|
||||
if statement.Effect == "Deny" {
|
||||
denyStatements = append(denyStatements, statement)
|
||||
continue
|
||||
}
|
||||
|
||||
// else if statement.Effect == "Allow"
|
||||
allowStatements = append(allowStatements, statement)
|
||||
}
|
||||
|
||||
// Deny statements are enforced first once matched.
|
||||
bktPolicy.Statements = append(denyStatements, allowStatements...)
|
||||
policy.Statements = append(denyStatements, allowStatements...)
|
||||
|
||||
// Return successfully parsed policy structure.
|
||||
return nil
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user