Compare commits

..

13 Commits

Author SHA1 Message Date
Minio Trusted 29b49f9343 Bump to new release. 2017-01-24 19:18:54 -08:00
Minio Trusted f8e4700a11 Merge remote-tracking branch 'origin/master' into release 2017-01-24 19:13:39 -08:00
Minio Trusted 4c5c00c640 Bump to new release 2017-01-24 18:54:28 -08:00
Minio Trusted 0be44133d4 Merge remote-tracking branch 'origin/master' into release 2017-01-24 18:42:52 -08:00
Minio Trusted 817e763416 Bump to new release. 2016-12-13 09:24:36 -08:00
Minio Trusted 0abab5305f Merge branch 'master' into release 2016-12-13 09:22:41 -08:00
Minio Trusted e6e855a7cf Bump to new release. 2016-12-12 15:49:31 -08:00
Minio Trusted 6fca1d164c Merge branch 'master' into release 2016-12-12 15:48:03 -08:00
Minio Trusted 9b76eba37e Bump to new release RELEASE.2016-12-12T18-35-43Z 2016-12-12 10:51:35 -08:00
Minio Trusted 03714d4809 Merge branch 'master' into release 2016-12-12 10:50:37 -08:00
Minio Trusted 3a73ea3f54 Bump to new release 2016-11-25 18:38:44 -08:00
Harshavardhana 9625629fc7 Merge branch 'master' into release 2016-11-25 18:23:36 -08:00
Minio Trusted 8507ce2a87 Bump to new release. 2016-11-23 18:19:40 -08:00
1394 changed files with 25527 additions and 328155 deletions
+2 -2
View File
@@ -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:
-4
View File
@@ -1,4 +0,0 @@
{
"asi": true,
"esnext": true
}
+2 -20
View File
@@ -1,9 +1,6 @@
go_import_path: github.com/minio/minio
sudo: required
services:
- docker
dist: trusty
language: go
@@ -16,27 +13,12 @@ env:
- ARCH=i686
script:
## Run all the tests
- make
- diff -au <(gofmt -d cmd) <(printf "")
- diff -au <(gofmt -d pkg) <(printf "")
- make test GOFLAGS="-timeout 15m -race -v"
- make test GOFLAGS="-race"
- make coverage
# Refer https://blog.hypriot.com/post/setup-simple-ci-pipeline-for-arm-images/
# push image
- >
if [ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$ARCH" == "x86_64" ]; then
docker run --rm --privileged multiarch/qemu-user-static:register --reset
docker build -t minio/minio:edge-armhf . -f Dockerfile.armhf
docker build -t minio/minio:edge-aarch64 . -f Dockerfile.aarch64
docker login -u="$DOCKER_USER" -p="$DOCKER_PASS"
docker push minio/minio:edge-armhf
docker push minio/minio:edge-aarch64
fi
after_success:
- bash <(curl -s https://codecov.io/bash)
go:
- 1.7.5
- 1.7.4
+11 -24
View File
@@ -1,30 +1,17 @@
FROM alpine:3.5
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
COPY . /go/src/app
WORKDIR /go/src/github.com/minio/
RUN \
apk add --no-cache ca-certificates && \
apk add --no-cache --virtual .build-deps git go musl-dev && \
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
go get -v -d github.com/minio/minio && \
cd /go/src/github.com/minio/minio && \
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
rm -rf /go/pkg /go/src /usr/local/go && apk del .build-deps
RUN \
apk add --no-cache git && \
go-wrapper download && \
go-wrapper install -ldflags "-X github.com/minio/minio/cmd.Version=2017-01-25T03:14:52Z -X github.com/minio/minio/cmd.ReleaseTag=RELEASE.2017-01-25T03-14-52Z -X github.com/minio/minio/cmd.CommitID=f8e4700a11065967242b8857045ce7dee607722d" && \
mkdir -p /export/docker && \
rm -rf /go/pkg /go/src && \
apk del git
EXPOSE 9000
COPY buildscripts/docker-entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
ENTRYPOINT ["minio"]
VOLUME ["/export"]
CMD ["minio"]
-30
View File
@@ -1,30 +0,0 @@
FROM resin/aarch64-alpine:3.5
MAINTAINER Minio Inc <dev@minio.io>
ENV GOPATH /go
ENV PATH $PATH:$GOPATH/bin
ENV CGO_ENABLED 0
WORKDIR /go/src/github.com/minio/
RUN \
apk add --no-cache ca-certificates && \
apk add --no-cache --virtual .build-deps git go musl-dev && \
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
go get -v -d github.com/minio/minio && \
cd /go/src/github.com/minio/minio && \
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
rm -rf /go/pkg /go/src /usr/local/go && apk del .build-deps
EXPOSE 9000
COPY buildscripts/docker-entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
VOLUME ["/export"]
CMD ["minio"]
-30
View File
@@ -1,30 +0,0 @@
FROM resin/armhf-alpine:3.5
MAINTAINER Minio Inc <dev@minio.io>
ENV GOPATH /go
ENV PATH $PATH:$GOPATH/bin
ENV CGO_ENABLED 0
WORKDIR /go/src/github.com/minio/
RUN \
apk add --no-cache ca-certificates && \
apk add --no-cache --virtual .build-deps git go musl-dev && \
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
go get -v -d github.com/minio/minio && \
cd /go/src/github.com/minio/minio && \
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
rm -rf /go/pkg /go/src /usr/local/go && apk del .build-deps
EXPOSE 9000
COPY buildscripts/docker-entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
VOLUME ["/export"]
CMD ["minio"]
-26
View File
@@ -1,26 +0,0 @@
FROM alpine:3.5
MAINTAINER Minio Inc <dev@minio.io>
COPY buildscripts/docker-entrypoint.sh buildscripts/healthcheck.sh /usr/bin/
RUN \
apk add --no-cache ca-certificates && \
apk add --no-cache --virtual .build-deps curl && \
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
curl https://dl.minio.io/server/minio/release/linux-amd64/minio > /usr/bin/minio && \
chmod +x /usr/bin/minio && \
chmod +x /usr/bin/docker-entrypoint.sh && \
chmod +x /usr/bin/healthcheck.sh
EXPOSE 9000
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
VOLUME ["/export"]
HEALTHCHECK --interval=30s --timeout=5s \
CMD /usr/bin/healthcheck.sh
CMD ["minio"]
-25
View File
@@ -1,25 +0,0 @@
FROM resin/aarch64-alpine:3.5
MAINTAINER Minio Inc <dev@minio.io>
COPY buildscripts/docker-entrypoint.sh buildscripts/healthcheck.sh /usr/bin/
RUN \
apk add --no-cache ca-certificates && \
apk add --no-cache --virtual .build-deps curl && \
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
curl https://dl.minio.io/server/minio/release/linux-arm64/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"]
-25
View File
@@ -1,25 +0,0 @@
FROM resin/armhf-alpine:3.5
MAINTAINER Minio Inc <dev@minio.io>
COPY buildscripts/docker-entrypoint.sh buildscripts/healthcheck.sh /usr/bin/
RUN \
apk add --no-cache ca-certificates && \
apk add --no-cache --virtual .build-deps curl && \
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
curl https://dl.minio.io/server/minio/release/linux-arm/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"]
+83 -42
View File
@@ -1,50 +1,102 @@
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 "Check deps"
@echo "Checking deps:"
@(env bash $(PWD)/buildscripts/checkdeps.sh)
@echo "Checking project is in GOPATH"
@(env bash $(PWD)/buildscripts/checkgopath.sh)
getdeps: checks
@echo "Installing golint" && go get -u github.com/golang/lint/golint
@echo "Installing gocyclo" && go get -u github.com/fzipp/gocyclo
@echo "Installing deadcode" && go get -u github.com/remyoudompheng/go-misc/deadcode
@echo "Installing misspell" && go get -u github.com/client9/misspell/cmd/misspell
@echo "Installing ineffassign" && go get -u github.com/gordonklaus/ineffassign
@echo "Installing golint:" && go get -u github.com/golang/lint/golint
@echo "Installing gocyclo:" && go get -u github.com/fzipp/gocyclo
@echo "Installing deadcode:" && go get -u github.com/remyoudompheng/go-misc/deadcode
@echo "Installing misspell:" && go get -u github.com/client9/misspell/cmd/misspell
@echo "Installing ineffassign:" && go get -u github.com/gordonklaus/ineffassign
verifiers: getdeps vet fmt lint cyclo 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:
@${GOPATH}/bin/deadcode
@@ -53,43 +105,32 @@ spelling:
@${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 all minio testing"
@go test $(GOFLAGS) .
test: build
@echo "Running all minio testing:"
@go test $(GOFLAGS) github.com/minio/minio/cmd...
@go test $(GOFLAGS) github.com/minio/minio/pkg...
coverage: build
@echo "Running all coverage for minio"
@echo "Running all coverage for minio:"
@./buildscripts/go-coverage.sh
# Builds minio locally.
build:
@echo "Building minio to $(PWD)/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 at $(GOPATH)/bin/minio ..."
@cp $(PWD)/minio $(GOPATH)/bin/minio
@echo "Check 'minio -h' for help."
install: gomake-all
release: verifiers
@MINIO_RELEASE=RELEASE ./buildscripts/build.sh
@@ -98,7 +139,7 @@ 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 -rf build
@rm -rf release
+13 -28
View File
@@ -18,26 +18,19 @@ 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
brew install minio
minio server ~/Photos
```
#### Note
If you previously installed minio using `brew install minio` then reinstall minio from `minio/stable/minio` official repo. Homebrew builds are unstable due to golang 1.8 bugs.
```
brew uninstall minio
brew install minio/stable/minio
```
### 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 ~/Photos
@@ -47,11 +40,9 @@ chmod 755 minio
### Binary Download
| Platform| Architecture | URL|
| ----------| -------- | ------|
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio |
| |32-bit Intel|https://dl.minio.io/server/minio/release/linux-386/minio |
| |32-bit ARM|https://dl.minio.io/server/minio/release/linux-arm/minio |
| |64-bit ARM|https://dl.minio.io/server/minio/release/linux-arm64/minio |
| |32-bit ARMv6|https://dl.minio.io/server/minio/release/linux-arm6vl/minio |
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio|
||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|
```sh
chmod +x minio
./minio server ~/Photos
@@ -61,38 +52,32 @@ chmod +x minio
### Binary Download
| Platform| Architecture | URL|
| ----------| -------- | ------|
|Microsoft Windows|64-bit|https://dl.minio.io/server/minio/release/windows-amd64/minio.exe |
| |32-bit|https://dl.minio.io/server/minio/release/windows-386/minio.exe |
|Microsoft Windows|64-bit|https://dl.minio.io/server/minio/release/windows-amd64/minio.exe|
||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)
```sh
pkg install minio
sysrc minio_enable=yes
sysrc minio_disks=/home/user/Photos
service minio start
```
### Binary Download
| Platform| Architecture | URL|
| ----------| -------- | ------|
|FreeBSD|64-bit|https://dl.minio.io/server/minio/release/freebsd-amd64/minio |
|FreeBSD|64-bit|https://dl.minio.io/server/minio/release/freebsd-amd64/minio|
```sh
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
-2
View File
@@ -11,8 +11,6 @@ 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|
|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|
+3 -6
View File
@@ -11,12 +11,11 @@ clone_folder: c:\gopath\src\github.com\minio\minio
# Environment variables
environment:
GOROOT: c:\go17
GOPATH: c:\gopath
# scripts that run after cloning repository
install:
- set PATH=%GOPATH%\bin;c:\go17\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,9 +35,9 @@ test_script:
# Unit tests
- ps: Add-AppveyorTest "Unit Tests" -Outcome Running
- mkdir build\coverage
- go test -v -timeout 17m -race github.com/minio/minio/cmd...
- go test -timeout 15m -v -race github.com/minio/minio/cmd...
- go test -v -race github.com/minio/minio/pkg...
- go test -v -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd
- go test -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd
- ps: Update-AppveyorTest "Unit Tests" -Outcome Passed
after_test:
+7 -8
View File
@@ -1,9 +1,8 @@
{
"presets": [
"es2015",
"react"
],
"plugins": [
"transform-object-rest-spread"
]
}
"presets": [
"es2015",
"react"
],
"plugins": ["transform-object-rest-spread"]
}
+11 -46
View File
@@ -4,69 +4,34 @@
## Installation
### Install yarn
### Install yarn:
```sh
curl -o- -L https://yarnpkg.com/install.sh | bash
yarn
$ curl -o- -L https://yarnpkg.com/install.sh | bash
$ yarn
```
### Install `go-bindata` and `go-bindata-assetfs`
### Install `go-bindata` and `go-bindata-assetfs`.
If you do not have a working Golang environment, please follow [Install Golang](https://docs.minio.io/docs/how-to-install-golang)
```sh
go get github.com/jteeuwen/go-bindata/...
go get github.com/elazarl/go-bindata-assetfs/...
$ go get github.com/jteeuwen/go-bindata/...
$ go get github.com/elazarl/go-bindata-assetfs/...
```
## Generating Assets
## Generating Assets.
### Generate ui-assets.go
```sh
yarn release
$ yarn release
```
This generates ui-assets.go in the current direcotry. Now do `make` in the parent directory to build the minio binary with the newly generated ui-assets.go
This generates ui-assets.go in the current directory. Now do `make` in the parent directory to build the minio binary with the newly generated ``ui-assets.go``
### Run Minio Browser with live reload
### Run Minio Browser with live reload.
```sh
yarn dev
$ yarn dev
```
Open [http://localhost:8080/minio/](http://localhost:8080/minio/) in your browser to play with the application
### Run Minio Browser with live reload on custom port
Edit `browser/webpack.config.js`
```diff
diff --git a/browser/webpack.config.js b/browser/webpack.config.js
index 3ccdaba..9496c56 100644
--- a/browser/webpack.config.js
+++ b/browser/webpack.config.js
@@ -58,6 +58,7 @@ var exports = {
historyApiFallback: {
index: '/minio/'
},
+ port: 8888,
proxy: {
'/minio/webrpc': {
target: 'http://localhost:9000',
@@ -97,7 +98,7 @@ var exports = {
if (process.env.NODE_ENV === 'dev') {
exports.entry = [
'webpack/hot/dev-server',
- 'webpack-dev-server/client?http://localhost:8080',
+ 'webpack-dev-server/client?http://localhost:8888',
path.resolve(__dirname, 'app/index.js')
]
}
```
```sh
yarn dev
```
Open [http://localhost:8888/minio/](http://localhost:8888/minio/) in your browser to play with the application
+2 -2
View File
@@ -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'
@@ -45,6 +44,7 @@ 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)
+1 -1
View File
@@ -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.
+19 -142
View File
@@ -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,10 +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 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'
@@ -53,10 +56,6 @@ export const SET_POLICIES = 'SET_POLICIES'
export const SET_SHARE_OBJECT = 'SET_SHARE_OBJECT'
export const DELETE_CONFIRMATION = 'DELETE_CONFIRMATION'
export const SET_PREFIX_WRITABLE = 'SET_PREFIX_WRITABLE'
export const REMOVE_OBJECT = 'REMOVE_OBJECT'
export const CHECKED_OBJECTS_ADD = 'CHECKED_OBJECTS_ADD'
export const CHECKED_OBJECTS_REMOVE = 'CHECKED_OBJECTS_REMOVE'
export const CHECKED_OBJECTS_RESET = 'CHECKED_OBJECTS_RESET'
export const showDeleteConfirmation = (object) => {
return {
@@ -78,12 +77,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
}
}
@@ -99,17 +97,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,
@@ -117,11 +113,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({
@@ -214,13 +206,6 @@ export const showAlert = alert => {
}
}
export const removeObject = object => {
return {
type: REMOVE_OBJECT,
object
}
}
export const setSidebarStatus = (status) => {
return {
type: SET_SIDEBAR_STATUS,
@@ -242,25 +227,10 @@ export const setVisibleBuckets = visibleBuckets => {
}
}
const appendObjects = (objects, marker, istruncated) => {
return {
type: APPEND_OBJECTS,
objects,
marker,
istruncated
}
}
export const setObjects = (objects) => {
return {
type: SET_OBJECTS,
objects,
}
}
export const resetObjects = () => {
return {
type: RESET_OBJECTS
objects
}
}
@@ -314,63 +284,22 @@ export const selectBucket = (newCurrentBucket, prefix) => {
}
}
export const listObjects = () => {
return (dispatch, getState) => {
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
if (!objects)
objects = []
objects = objects.map(object => {
object.name = object.name.replace(`${currentPath}`, '');
return object
})
dispatch(appendObjects(objects, res.nextmarker, res.istruncated))
dispatch(setPrefixWritable(res.writable))
dispatch(setLoadBucket(''))
dispatch(setLoadPath(''))
})
.catch(err => {
dispatch(showAlert({
type: 'danger',
message: err.message
}))
dispatch(setLoadBucket(''))
dispatch(setLoadPath(''))
// Use browserHistory.replace instead of push so that browser back button works fine.
browserHistory.replace(`${minioBrowserPrefix}/login`)
})
}
}
export const selectPrefix = prefix => {
return (dispatch, getState) => {
const {currentBucket, web} = getState()
dispatch(resetObjects())
dispatch(setLoadPath(prefix))
web.ListObjects({
bucketName: currentBucket,
prefix,
marker: ""
prefix
})
.then(res => {
let objects = res.objects
if (!objects)
objects = []
objects = objects.map(object => {
object.name = object.name.replace(`${prefix}`, '');
return object
})
dispatch(appendObjects(
objects,
res.nextmarker,
res.istruncated
dispatch(setObjects(
utils.sortObjectsByName(objects.map(object => {
object.name = object.name.replace(`${prefix}`, ''); return object
}))
))
dispatch(setPrefixWritable(res.writable))
dispatch(setSortNameOrder(false))
@@ -385,8 +314,8 @@ export const selectPrefix = prefix => {
}))
dispatch(setLoadBucket(''))
dispatch(setLoadPath(''))
// Use browserHistory.replace instead of push so that browser back button works fine.
browserHistory.replace(`${minioBrowserPrefix}/login`)
// Use browserHistory.replace instead of push so that browser back button works fine.
browserHistory.replace(`${minioBrowserPrefix}/login`)
})
}
}
@@ -430,37 +359,6 @@ 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()
@@ -614,24 +512,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
}
}
+65 -165
View File
@@ -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'
@@ -46,7 +47,6 @@ import * as mime from '../mime'
import { minioBrowserPrefix } from '../constants'
import CopyToClipboard from 'react-copy-to-clipboard'
import storage from 'local-storage-fallback'
import InfiniteScroll from 'react-infinite-scroller';
export default class Browse extends React.Component {
componentDidMount() {
@@ -68,7 +68,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))
})
@@ -110,6 +110,9 @@ export default class Browse extends React.Component {
if (!decPathname.endsWith('/'))
decPathname += '/'
if (decPathname === minioBrowserPrefix + '/') {
dispatch(actions.setCurrentBucket(''))
dispatch(actions.setCurrentPath(''))
dispatch(actions.setObjects([]))
return
}
let obj = utils.pathSlice(decPathname)
@@ -137,11 +140,6 @@ export default class Browse extends React.Component {
this.props.dispatch(actions.setVisibleBuckets(buckets.filter(bucket => bucket.indexOf(e.target.value) > -1)))
}
listObjects() {
const {dispatch} = this.props
dispatch(actions.listObjects())
}
selectPrefix(e, prefix) {
e.preventDefault()
const {dispatch, currentPath, web, currentBucket} = this.props
@@ -150,16 +148,7 @@ export default class Browse extends React.Component {
if (prefix === currentPath) return
browserHistory.push(utils.pathJoin(currentBucket, encPrefix))
} else {
// Download the selected file.
web.CreateURLToken()
.then(res => {
let url = `${window.location.origin}/minio/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')}`
}
}
@@ -235,29 +224,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.selectPrefix(currentPath))
})
.catch(e => dispatch(actions.showAlert({
type: 'danger',
@@ -285,8 +259,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() {
@@ -323,6 +296,11 @@ export default class Browse extends React.Component {
browserHistory.push(`${minioBrowserPrefix}/login`)
}
landingPage(e) {
e.preventDefault()
this.props.dispatch(actions.selectBucket(this.props.buckets[0]))
}
fullScreen(e) {
e.preventDefault()
let el = document.documentElement
@@ -378,78 +356,26 @@ 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
}
web.CreateURLToken()
.then(res => {
let requestUrl = location.origin + "/minio/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
const {policies, currentBucket, currentPath} = this.props
const {deleteConfirmation} = this.props
const {shareObject} = this.props
const {web, prefixWritable, istruncated} = this.props
const {web, prefixWritable} = this.props
// Don't always show the SettingsModal. This is done here instead of in
// SettingsModal.js so as to allow for #componentWillMount to handle
@@ -488,33 +414,30 @@ export default class Browse extends React.Component {
let freePercent = free * 100 / total
if (web.LoggedIn()) {
browserDropdownButton = <BrowserDropdown fullScreenFunc={ this.fullScreen.bind(this) }
aboutFunc={ this.showAbout.bind(this) }
settingsFunc={ this.showSettings.bind(this) }
logoutFunc={ this.logout.bind(this) } />
browserDropdownButton = <BrowserDropdown fullScreen={ this.fullScreen.bind(this) }
showAbout={ this.showAbout.bind(this) }
showSettings={ this.showSettings.bind(this) }
logout={ this.logout.bind(this) } />
} else {
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>
}
@@ -558,6 +481,7 @@ export default class Browse extends React.Component {
</OverlayTrigger>
</Dropdown.Menu>
</Dropdown>
}
return (
@@ -565,19 +489,12 @@ export default class Browse extends React.Component {
'file-explorer': true,
'toggled': sidebarStatus
}) }>
<SideBar searchBuckets={ this.searchBuckets.bind(this) }
<SideBar landingPage={ this.landingPage.bind(this) }
searchBuckets={ this.searchBuckets.bind(this) }
selectBucket={ this.selectBucket.bind(this) }
clickOutside={ this.hideSidebar.bind(this) }
showPolicy={ this.showBucketPolicy.bind(this) } />
<div className="fe-body">
<div className={ 'list-actions' + (classNames({
' list-actions-toggled': checkedObjects.length > 0
})) }>
<span className="la-label"><i className="fa fa-check-circle" /> { checkedObjects.length } Objects selected</span>
<span className="la-actions pull-right"><button onClick={ this.downloadSelected.bind(this) }> Download all as zip </button></span>
<span className="la-actions pull-right"><button onClick={ this.showDeleteConfirmation.bind(this) }> Delete selected </button></span>
<i className="la-close fa fa-times" onClick={ this.clearSelected.bind(this) }></i>
</div>
<Dropzone>
{ alertBox }
<header className="fe-header-mobile hidden-lg hidden-md">
@@ -603,8 +520,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,
@@ -613,7 +529,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,
@@ -622,7 +538,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,
@@ -631,24 +547,14 @@ 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">
<InfiniteScroll loadMore={ this.listObjects.bind(this) }
hasMore={ istruncated }
useWindow={ true }
initialLoad={ false }>
<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 } />
</InfiniteScroll>
<div className="text-center" style={ { display: (istruncated && currentBucket) ? 'block' : 'none' } }>
<span>Loading...</span>
</div>
<ObjectsList dataType={ this.dataType.bind(this) }
selectPrefix={ this.selectPrefix.bind(this) }
showDeleteConfirmation={ this.showDeleteConfirmation.bind(this) }
shareObject={ this.shareObject.bind(this) } />
</div>
<UploadModal />
{ createButton }
@@ -760,11 +666,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>
@@ -773,14 +679,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>
@@ -788,34 +692,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>
+7 -7
View File
@@ -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.
@@ -18,7 +18,7 @@ import React from 'react'
import connect from 'react-redux/lib/components/connect'
import Dropdown from 'react-bootstrap/lib/Dropdown'
let BrowserDropdown = ({fullScreenFunc, aboutFunc, settingsFunc, logoutFunc}) => {
let BrowserDropdown = ({fullScreen, showAbout, showSettings, logout}) => {
return (
<li>
<Dropdown pullRight id="top-right-menu">
@@ -27,10 +27,10 @@ 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>
<a href="" onClick={ fullScreen }>Fullscreen <i className="fa fa-expand"></i></a>
</li>
<li>
<a target="_blank" href="https://docs.minio.io/">Documentation <i className="fa fa-book"></i></a>
@@ -39,13 +39,13 @@ let BrowserDropdown = ({fullScreenFunc, aboutFunc, settingsFunc, logoutFunc}) =>
<a target="_blank" href="https://slack.minio.io">Ask for help <i className="fa fa-question-circle"></i></a>
</li>
<li>
<a href="" onClick={ aboutFunc }>About <i className="fa fa-info-circle"></i></a>
<a href="" onClick={ showAbout }>About <i className="fa fa-info-circle"></i></a>
</li>
<li>
<a href="" onClick={ settingsFunc }>Settings <i className="fa fa-cog"></i></a>
<a href="" onClick={ showSettings }>Settings <i className="fa fa-cog"></i></a>
</li>
<li>
<a href="" onClick={ logoutFunc }>Sign Out <i className="fa fa-sign-out"></i></a>
<a href="" onClick={ logout }>Sign Out <i className="fa fa-sign-out"></i></a>
</li>
</Dropdown.Menu>
</Dropdown>
+1 -1
View File
@@ -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 -1
View File
@@ -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.
+2 -3
View File
@@ -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 -1
View File
@@ -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.
+6 -1
View File
@@ -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"
+17 -38
View File
@@ -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)
+2 -2
View File
@@ -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.
@@ -29,7 +29,7 @@ let Path = ({currentBucket, currentPath, selectPrefix}) => {
}
return (
<h2><span className="main"><a onClick={ (e) => selectPrefix(e, '') } href="">{ currentBucket }</a></span>{ path }</h2>
<h2><span className="main"><a onClick={ (e) => selectPrefix(e, '') } href="">{ currentBucket }</a></span>{ path }</h2>
)
}
+1 -1
View File
@@ -77,4 +77,4 @@ class Policy extends Component {
}
}
export default connect(state => state)(Policy)
export default connect(state => state)(Policy)
+4 -19
View File
@@ -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)
+17 -6
View File
@@ -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()
+4 -4
View File
@@ -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.
@@ -22,7 +22,7 @@ import connect from 'react-redux/lib/components/connect'
import logo from '../../img/logo.svg'
let SideBar = ({visibleBuckets, loadBucket, currentBucket, selectBucket, searchBuckets, sidebarStatus, clickOutside, showPolicy}) => {
let SideBar = ({visibleBuckets, loadBucket, currentBucket, selectBucket, searchBuckets, landingPage, sidebarStatus, clickOutside, showPolicy}) => {
const list = visibleBuckets.map((bucket, i) => {
return <li className={ classNames({
@@ -44,8 +44,8 @@ let SideBar = ({visibleBuckets, loadBucket, currentBucket, selectBucket, searchB
'toggled': sidebarStatus
}) }>
<div className="fes-header clearfix hidden-sm hidden-xs">
<img src={ logo } alt="" />
<h2>Minio Browser</h2>
<a href="" onClick={ landingPage }><img src={ logo } alt="" />
<h2>Minio Browser</h2></a>
</div>
<div className="fes-list">
<div className="input-group ig-dark ig-left ig-search" style={ { display: web.LoggedIn() ? 'block' : 'none' } }>
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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.
+4 -34
View File
@@ -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,7 +21,6 @@ export default (state = {
buckets: [],
visibleBuckets: [],
objects: [],
istruncated: true,
storageInfo: {},
serverInfo: {},
currentBucket: '',
@@ -54,10 +53,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) {
@@ -77,18 +75,8 @@ export default (state = {
case actions.SET_CURRENT_BUCKET:
newState.currentBucket = action.currentBucket
break
case actions.APPEND_OBJECTS:
newState.objects = [...newState.objects, ...action.objects]
newState.marker = action.marker
newState.istruncated = action.istruncated
break
case actions.SET_OBJECTS:
newState.objects = [...action.objects]
break
case actions.RESET_OBJECTS:
newState.objects = []
newState.marker = ""
newState.istruncated = false
newState.objects = action.objects
break
case actions.SET_CURRENT_PATH:
newState.currentPath = action.currentPath
@@ -183,24 +171,6 @@ export default (state = {
case actions.SET_PREFIX_WRITABLE:
newState.prefixWritable = action.prefixWritable
break
case actions.REMOVE_OBJECT:
let idx = newState.objects.findIndex(object => object.name === action.object)
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 -1
View File
@@ -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 -4
View File
@@ -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.
@@ -112,9 +112,6 @@ export default class Web {
return res
})
}
CreateURLToken() {
return this.makeCall('CreateURLToken')
}
GetBucketPolicy(args) {
return this.makeCall('GetBucketPolicy', args)
}
-4
View File
@@ -35,10 +35,6 @@
width: 100%;
}
.btn-white {
.btn-variant(#fff, darken(@text-color, 20%));
}
.btn-link {
.btn-variant(#eee, #545454);
}
+3 -16
View File
@@ -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;
+5 -3
View File
@@ -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
View File
@@ -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(#f1c3, '\f1c3'); }
&[data-type=image] { .list-type(#f06292, '\f1c5'); }
&[data-type=video] { .list-type(#f8c363, '\f1c8'); }
&[data-type=other] { .list-type(#afafaf, '\f016'); }
&[data-type=text] { .list-type(#8a8a8a, '\f0f6'); }
&[data-type=doc] { .list-type(#2196f5, '\f1c2'); }
&[data-type=presentation] { .list-type(#896ea6, '\f1c4'); }
&[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: 20;
opacity: 0;
cursor: pointer;
&:checked {
& ~ .fis-icon {
background-color: #32393F;
&:before {
opacity: 0;
}
}
& ~ .fis-helper {
&:before {
.scale(0);
}
&:after {
.scale(1);
}
}
}
}
}
.fis-icon {
display: inline-block;
vertical-align: top;
border-radius: 50%;
width: 35px;
height: 35px;
.transition(background-color);
.transition-duration(250ms);
&:before {
width: 100%;
height: 100%;
text-align: center;
position: absolute;
border-radius: 50%;
font-family: @font-family-icon;
line-height: 35px;
font-size: 16px;
color: @white;
.transition(all);
.transition-duration(300ms);
font-style: normal;
}
}
.fis-helper {
&:before,
&:after {
position: absolute;
.transition(all);
.transition-duration(250ms);
}
&:before {
content: '';
width: 15px;
height: 15px;
border: 2px solid @white;
z-index: 10;
border-radius: 2px;
top: 10px;
left: 10px;
opacity: 0;
}
&:after {
font-family: @font-family-icon;
content: '\f00c';
top: 8px;
left: 9px;
color: @white;
font-size: 14px;
.scale(0);
}
}
/*--------------------------
Files and Folders
@@ -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 {
-14
View File
@@ -99,18 +99,4 @@
content: '7 days';
right: 0;
}
}
.modal-aheader {
height: 100px;
&:before {
height: 0 !important;
}
.modal-dialog {
margin: 0;
vertical-align: top;
}
}
+1 -1
View File
@@ -49,4 +49,4 @@
z-index: 1;
-webkit-animation: zoomIn 250ms, spin 700ms 250ms infinite linear;
animation: zoomIn 250ms, spin 700ms 250ms infinite linear;
}
}
+8 -8
View File
@@ -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;
@@ -139,10 +139,10 @@
position: absolute;
top: 0;
right: 0;
width: 35px;
width: 40px;
height: 100%;
cursor: pointer;
background: url(../../img/more-h-light.svg) no-repeat top 20px left;
background: url(../../img/more-h-light.svg) no-repeat left;
}
/* Scrollbar */
+3 -10
View File
@@ -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
View File
@@ -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)
+4 -7
View File
@@ -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,14 +26,12 @@
"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",
"copy-webpack-plugin": "^0.3.3",
"css-loader": "^0.23.1",
"esformatter": "^0.10.0",
"esformatter-jsx": "^7.4.1",
"esformatter-jsx-ignore": "^1.0.6",
"expect": "^1.20.2",
"history": "^1.17.0",
@@ -79,7 +77,6 @@
"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"
}
}
+55 -65
View File
File diff suppressed because one or more lines are too long
+1 -5
View File
@@ -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: [
+1022 -1407
View File
File diff suppressed because it is too large Load Diff
+50 -38
View File
@@ -5,7 +5,7 @@ _init() {
LDFLAGS=$(go run buildscripts/gen-ldflags.go)
# Extract release tag
release_tag=$(echo $LDFLAGS | awk {'print $6'} | cut -f2 -d=)
release_tag=$(echo $LDFLAGS | awk {'print $4'} | cut -f2 -d=)
# Verify release tag.
if [ -z "$release_tag" ]; then
@@ -28,7 +28,6 @@ _init() {
## System binaries
CP=`which cp`
SHASUM=`which shasum`
SHA256SUM="${SHASUM} -a 256"
SED=`which sed`
}
@@ -44,36 +43,58 @@ go_build() {
release_bin="$release_str/$os-$arch/$(basename $package).$release_tag"
# Release binary downloadable name
release_real_bin="$release_str/$os-$arch/$(basename $package)"
# Release sha1sum name
release_shasum="$release_str/$os-$arch/$(basename $package).${release_tag}.shasum"
# Release sha1sum default
release_shasum_default="$release_str/$os-$arch/$(basename $package).shasum"
# Release sha256sum name
release_sha256sum="$release_str/$os-$arch/$(basename $package).${release_tag}.sha256sum"
# Release sha256sum default
release_sha256sum_default="$release_str/$os-$arch/$(basename $package).sha256sum"
# Release shasum name
release_shasum="$release_str/$os-$arch/$(basename $package).shasum"
# Go build to build the binary.
CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build --ldflags "${LDFLAGS}" -o $release_bin
if [ "${arch}" == "arm" ]; then
# Release binary downloadable name
release_real_bin_6="$release_str/$os-${arch}6vl/$(basename $package)"
# Create copy
if [ $os == "windows" ]; then
$CP -p $release_bin ${release_real_bin}.exe
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
$CP -p $release_bin $release_real_bin
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
# Calculate sha1sum
shasum_str=$(${SHASUM} ${release_bin})
echo ${shasum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_shasum
$CP -p $release_shasum $release_shasum_default
# Calculate sha256sum
sha256sum_str=$(${SHA256SUM} ${release_bin})
echo ${sha256sum_str} | $SED "s/$release_str\/$os-$arch\///g" > $release_sha256sum
$CP -p $release_sha256sum $release_sha256sum_default
}
main() {
@@ -90,18 +111,9 @@ main() {
go_build ${each_osarch}
done
else
local found=0
for each_osarch in ${SUPPORTED_OSARCH}; do
if [ "$chosen_osarch" = "$each_osarch" ]; then
found=1
fi
for each_osarch in $(echo $chosen_osarch | sed 's/,/ /g'); do
go_build ${each_osarch}
done
if [ ${found} -eq 1 ]; then
go_build ${chosen_osarch}
else
echo "Unknown architecture \"${chosen_osarch}\""
exit 1
fi
fi
}
+2 -7
View File
@@ -25,11 +25,6 @@ _init() {
OSX_VERSION="10.8"
KNAME=$(uname -s)
ARCH=$(uname -m)
case "${KNAME}" in
SunOS )
ARCH=$(isainfo -k)
;;
esac
}
## FIXME:
@@ -102,7 +97,7 @@ assert_is_supported_arch() {
assert_is_supported_os() {
case "${KNAME}" in
Linux | FreeBSD | OpenBSD | NetBSD | DragonFly | SunOS )
Linux | FreeBSD )
return
;;
Darwin )
@@ -118,7 +113,7 @@ assert_is_supported_os() {
*)
echo "ERROR"
echo "OS '${KNAME}' is not supported."
echo "Supported OS: [Linux, FreeBSD, OpenBSD, NetBSD, Darwin, DragonFly]"
echo "Supported OS: [Linux, FreeBSD, Darwin]"
exit 1
esac
}
+2
View File
@@ -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"
-81
View File
@@ -1,81 +0,0 @@
#!/bin/sh
#
# 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.
#
# If command starts with an option, prepend minio.
if [ "${1}" != "minio" ]; then
if [ -n "${1}" ]; then
set -- minio "$@"
fi
fi
# Wait for all the hosts to come online and have
# their DNS entries populated properly.
docker_wait_hosts() {
hosts="$@"
num_hosts=0
# Count number of hosts in arguments.
for host in $hosts; do
[ $(echo "$host" | grep -E "^http") ] || continue
num_hosts=$((num_hosts+1))
done
if [ $num_hosts -gt 0 ]; then
echo -n "Waiting for all hosts to resolve..."
while true; do
x=0
for host in $hosts; do
[ $(echo "$host" | grep -E "^http") ] || continue
# Extract the domain.
host=$(echo $host | sed -e 's/^http[s]\?:\/\/\([^\/]\+\).*/\1/')
echo -n .
val=$(ping -c 1 $host 2>/dev/null)
if [ $? != 0 ]; then
echo "Failed to lookup $host"
continue
fi
x=$((x+1))
done
# Provided hosts same as successful hosts, should break out.
test $x -eq $num_hosts && break
echo "Failed to resolve hosts.. retrying after 1 second."
sleep 1
done
echo "All hosts are resolving proceeding to initialize Minio."
fi
}
## Look for docker secrets in default documented location.
docker_secrets_env() {
local MINIO_ACCESS_KEY_FILE="/run/secrets/access_key"
local MINIO_SECRET_KEY_FILE="/run/secrets/secret_key"
if [ -f $MINIO_ACCESS_KEY_FILE -a -f $MINIO_SECRET_KEY_FILE ]; then
if [ -f $MINIO_ACCESS_KEY_FILE ]; then
export MINIO_ACCESS_KEY="$(cat "$MINIO_ACCESS_KEY_FILE")"
fi
if [ -f $MINIO_SECRET_KEY_FILE ]; then
export MINIO_SECRET_KEY="$(cat "$MINIO_SECRET_KEY_FILE")"
fi
fi
}
## Set access env from secrets if necessary.
docker_secrets_env
## Wait for all the hosts to come online.
docker_wait_hosts "$@"
exec "$@"
+2 -2
View File
@@ -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]
-43
View File
@@ -1,43 +0,0 @@
#!/bin/sh
#
# 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.
#
_init () {
scheme="http://"
address="127.0.0.1:9000"
resource="/minio/index.html"
}
HealthCheckMain () {
# Get the http response code
http_response=$(curl -s -k -o /dev/null -I -w "%{http_code}" ${scheme}${address}${resource})
# Get the http response body
http_response_body=$(curl -k -s ${scheme}${address}${resource})
# server returns response 403 and body "SSL required" if non-TLS connection is attempted on a TLS-configured server.
# change the scheme and try again
if [ "$http_response" = "403" ] && [ "$http_response_body" = "SSL required" ]; then
scheme="https://"
http_response=$(curl -s -k -o /dev/null -I -w "%{http_code}" ${scheme}${address}${resource})
fi
# If http_repsonse is 200 - server is up.
# When MINIO_BROWSER is set to off, curl responds with 404. We assume that the the server is up
[ "$http_response" = "200" ] || [ "$http_response" = "404" ]
}
_init && HealthCheckMain
+56 -489
View File
@@ -17,41 +17,33 @@
package cmd
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path"
"strconv"
"sync"
"time"
)
const (
minioAdminOpHeader = "X-Minio-Operation"
minioConfigTmpFormat = "config-%s.json"
minioAdminOpHeader = "X-Minio-Operation"
)
// Type-safe query params.
type mgmtQueryKey string
// Only valid query params for mgmt admin APIs.
// Only valid query params for list/clear locks management APIs.
const (
mgmtBucket mgmtQueryKey = "bucket"
mgmtObject mgmtQueryKey = "object"
mgmtPrefix mgmtQueryKey = "prefix"
mgmtLockDuration mgmtQueryKey = "duration"
mgmtDelimiter mgmtQueryKey = "delimiter"
mgmtMarker mgmtQueryKey = "marker"
mgmtKeyMarker mgmtQueryKey = "key-marker"
mgmtMaxKey mgmtQueryKey = "max-key"
mgmtDryRun mgmtQueryKey = "dry-run"
mgmtUploadIDMarker mgmtQueryKey = "upload-id-marker"
mgmtMaxUploads mgmtQueryKey = "max-uploads"
mgmtUploadID mgmtQueryKey = "upload-id"
mgmtBucket mgmtQueryKey = "bucket"
mgmtObject mgmtQueryKey = "object"
mgmtPrefix mgmtQueryKey = "prefix"
mgmtOlderThan mgmtQueryKey = "older-than"
mgmtDelimiter mgmtQueryKey = "delimiter"
mgmtMarker mgmtQueryKey = "marker"
mgmtMaxKey mgmtQueryKey = "max-key"
mgmtDryRun mgmtQueryKey = "dry-run"
)
// ServerVersion - server version
@@ -62,8 +54,8 @@ type ServerVersion struct {
// ServerStatus - contains the response of service status API
type ServerStatus struct {
StorageInfo StorageInfo `json:"storageInfo"`
ServerVersion ServerVersion `json:"serverVersion"`
Uptime time.Duration `json:"uptime"`
}
// ServiceStatusHandler - GET /?service
@@ -78,22 +70,15 @@ func (adminAPI adminAPIHandlers) ServiceStatusHandler(w http.ResponseWriter, r *
return
}
// Fetch storage backend information
storageInfo := newObjectLayerFn().StorageInfo()
// Fetch server version
serverVersion := ServerVersion{Version: Version, CommitID: CommitID}
// Fetch uptimes from all peers. This may fail to due to lack
// of read-quorum availability.
uptime, err := getPeerUptimes(globalAdminPeers)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
errorIf(err, "Possibly failed to get uptime from majority of servers.")
return
}
// Create API response
serverStatus := ServerStatus{
StorageInfo: storageInfo,
ServerVersion: serverVersion,
Uptime: uptime,
}
// Marshal API response
@@ -146,8 +131,8 @@ func (adminAPI adminAPIHandlers) ServiceCredentialsHandler(w http.ResponseWriter
}
// Avoid setting new credentials when they are already passed
// by the environment.
if globalIsEnvCreds {
// by the environnement
if globalEnvAccessKey != "" || globalEnvSecretKey != "" {
writeErrorResponse(w, ErrMethodNotAllowed, r.URL)
return
}
@@ -168,20 +153,25 @@ func (adminAPI adminAPIHandlers) ServiceCredentialsHandler(w http.ResponseWriter
return
}
creds, err := createCredential(req.Username, req.Password)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
// Check passed credentials
cred, err := getCredential(req.Username, req.Password)
switch err {
case errInvalidAccessKeyLength:
writeErrorResponse(w, ErrAdminInvalidAccessKey, r.URL)
return
case errInvalidSecretKeyLength:
writeErrorResponse(w, ErrAdminInvalidSecretKey, r.URL)
return
}
// Notify all other Minio peers to update credentials
updateErrs := updateCredsOnPeers(creds)
updateErrs := updateCredsOnPeers(cred)
for peer, err := range updateErrs {
errorIf(err, "Unable to update credentials on peer %s.", peer)
}
// Update local credentials in memory.
serverConfig.SetCredential(creds)
// Update local credentials
serverConfig.SetCredential(cred)
if err = serverConfig.Save(); err != nil {
writeErrorResponse(w, ErrInternalError, r.URL)
return
@@ -191,119 +181,11 @@ func (adminAPI adminAPIHandlers) ServiceCredentialsHandler(w http.ResponseWriter
w.WriteHeader(http.StatusOK)
}
// ServerProperties holds some server information such as, version, region
// uptime, etc..
type ServerProperties struct {
Uptime time.Duration `json:"uptime"`
Version string `json:"version"`
CommitID string `json:"commitID"`
Region string `json:"region"`
SQSARN []string `json:"sqsARN"`
}
// ServerConnStats holds transferred bytes from/to the server
type ServerConnStats struct {
TotalInputBytes uint64 `json:"transferred"`
TotalOutputBytes uint64 `json:"received"`
Throughput uint64 `json:"throughput,omitempty"`
}
// ServerHTTPMethodStats holds total number of HTTP operations from/to the server,
// including the average duration the call was spent.
type ServerHTTPMethodStats struct {
Count uint64 `json:"count"`
AvgDuration string `json:"avgDuration"`
}
// ServerHTTPStats holds all type of http operations performed to/from the server
// including their average execution time.
type ServerHTTPStats struct {
TotalHEADStats ServerHTTPMethodStats `json:"totalHEADs"`
SuccessHEADStats ServerHTTPMethodStats `json:"successHEADs"`
TotalGETStats ServerHTTPMethodStats `json:"totalGETs"`
SuccessGETStats ServerHTTPMethodStats `json:"successGETs"`
TotalPUTStats ServerHTTPMethodStats `json:"totalPUTs"`
SuccessPUTStats ServerHTTPMethodStats `json:"successPUTs"`
TotalPOSTStats ServerHTTPMethodStats `json:"totalPOSTs"`
SuccessPOSTStats ServerHTTPMethodStats `json:"successPOSTs"`
TotalDELETEStats ServerHTTPMethodStats `json:"totalDELETEs"`
SuccessDELETEStats ServerHTTPMethodStats `json:"successDELETEs"`
}
// ServerInfoData holds storage, connections and other
// information of a given server.
type ServerInfoData struct {
StorageInfo StorageInfo `json:"storage"`
ConnStats ServerConnStats `json:"network"`
HTTPStats ServerHTTPStats `json:"http"`
Properties ServerProperties `json:"server"`
}
// ServerInfo holds server information result of one node
type ServerInfo struct {
Error string `json:"error"`
Addr string `json:"addr"`
Data *ServerInfoData `json:"data"`
}
// ServerInfoHandler - GET /?info
// ----------
// Get server information
func (adminAPI adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Request) {
// Authenticate request
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Web service response
reply := make([]ServerInfo, len(globalAdminPeers))
var wg sync.WaitGroup
// Gather server information for all nodes
for i, p := range globalAdminPeers {
wg.Add(1)
// Gather information from a peer in a goroutine
go func(idx int, peer adminPeer) {
defer wg.Done()
// Initialize server info at index
reply[idx] = ServerInfo{Addr: peer.addr}
serverInfoData, err := peer.cmdRunner.ServerInfoData()
if err != nil {
errorIf(err, "Unable to get server info from %s.", peer.addr)
reply[idx].Error = err.Error()
return
}
reply[idx].Data = &serverInfoData
}(i, p)
}
wg.Wait()
// Marshal API response
jsonBytes, err := json.Marshal(reply)
if err != nil {
writeErrorResponse(w, ErrInternalError, r.URL)
errorIf(err, "Failed to marshal storage info into json.")
return
}
// Reply with storage information (across nodes in a
// distributed setup) as json.
writeSuccessResponseJSON(w, jsonBytes)
}
// validateLockQueryParams - Validates query params for list/clear locks management APIs.
func validateLockQueryParams(vars url.Values) (string, string, time.Duration, APIErrorCode) {
bucket := vars.Get(string(mgmtBucket))
prefix := vars.Get(string(mgmtPrefix))
durationStr := vars.Get(string(mgmtLockDuration))
relTimeStr := vars.Get(string(mgmtOlderThan))
// N B empty bucket name is invalid
if !IsValidBucketName(bucket) {
@@ -316,24 +198,24 @@ func validateLockQueryParams(vars url.Values) (string, string, time.Duration, AP
// If older-than parameter was empty then set it to 0s to list
// all locks older than now.
if durationStr == "" {
durationStr = "0s"
if relTimeStr == "" {
relTimeStr = "0s"
}
duration, err := time.ParseDuration(durationStr)
relTime, err := time.ParseDuration(relTimeStr)
if err != nil {
errorIf(err, "Failed to parse duration passed as query value.")
return "", "", time.Duration(0), ErrInvalidDuration
}
return bucket, prefix, duration, ErrNone
return bucket, prefix, relTime, ErrNone
}
// ListLocksHandler - GET /?lock&bucket=mybucket&prefix=myprefix&duration=duration
// ListLocksHandler - GET /?lock&bucket=mybucket&prefix=myprefix&older-than=rel_time
// - bucket is a mandatory query parameter
// - prefix and older-than are optional query parameters
// HTTP header x-minio-operation: list
// ---------
// Lists locks held on a given bucket, prefix and duration it was held for.
// Lists locks held on a given bucket, prefix and relative time.
func (adminAPI adminAPIHandlers) ListLocksHandler(w http.ResponseWriter, r *http.Request) {
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
@@ -342,15 +224,15 @@ func (adminAPI adminAPIHandlers) ListLocksHandler(w http.ResponseWriter, r *http
}
vars := r.URL.Query()
bucket, prefix, duration, adminAPIErr := validateLockQueryParams(vars)
bucket, prefix, relTime, adminAPIErr := validateLockQueryParams(vars)
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Fetch lock information of locks matching bucket/prefix that
// are available for longer than duration.
volLocks, err := listPeerLocksInfo(globalAdminPeers, bucket, prefix, duration)
// are available since relTime.
volLocks, err := listPeerLocksInfo(globalAdminPeers, bucket, prefix, relTime)
if err != nil {
writeErrorResponse(w, ErrInternalError, r.URL)
errorIf(err, "Failed to fetch lock information from remote nodes.")
@@ -366,16 +248,16 @@ func (adminAPI adminAPIHandlers) ListLocksHandler(w http.ResponseWriter, r *http
}
// Reply with list of locks held on bucket, matching prefix
// held longer than duration supplied, as json.
// older than relTime supplied, as json.
writeSuccessResponseJSON(w, jsonBytes)
}
// ClearLocksHandler - POST /?lock&bucket=mybucket&prefix=myprefix&duration=duration
// ClearLocksHandler - POST /?lock&bucket=mybucket&prefix=myprefix&older-than=relTime
// - bucket is a mandatory query parameter
// - prefix and older-than are optional query parameters
// HTTP header x-minio-operation: clear
// ---------
// Clear locks held on a given bucket, prefix and duration it was held for.
// Clear locks held on a given bucket, prefix and relative time.
func (adminAPI adminAPIHandlers) ClearLocksHandler(w http.ResponseWriter, r *http.Request) {
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
@@ -384,15 +266,15 @@ func (adminAPI adminAPIHandlers) ClearLocksHandler(w http.ResponseWriter, r *htt
}
vars := r.URL.Query()
bucket, prefix, duration, adminAPIErr := validateLockQueryParams(vars)
bucket, prefix, relTime, adminAPIErr := validateLockQueryParams(vars)
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Fetch lock information of locks matching bucket/prefix that
// are held for longer than duration.
volLocks, err := listPeerLocksInfo(globalAdminPeers, bucket, prefix, duration)
// are available since relTime.
volLocks, err := listPeerLocksInfo(globalAdminPeers, bucket, prefix, relTime)
if err != nil {
writeErrorResponse(w, ErrInternalError, r.URL)
errorIf(err, "Failed to fetch lock information from remote nodes.")
@@ -407,7 +289,7 @@ func (adminAPI adminAPIHandlers) ClearLocksHandler(w http.ResponseWriter, r *htt
return
}
// Remove lock matching bucket/prefix held longer than duration.
// Remove lock matching bucket/prefix older than relTime.
for _, volLock := range volLocks {
globalNSMutex.ForceUnlock(volLock.Bucket, volLock.Object)
}
@@ -416,57 +298,8 @@ func (adminAPI adminAPIHandlers) ClearLocksHandler(w http.ResponseWriter, r *htt
writeSuccessResponseJSON(w, jsonBytes)
}
// ListUploadsHealHandler - similar to listObjectsHealHandler
// GET
// /?heal&bucket=mybucket&prefix=myprefix&key-marker=mymarker&upload-id-marker=myuploadid&delimiter=mydelimiter&max-uploads=1000
// - bucket is mandatory query parameter
// - rest are optional query parameters List upto maxKey objects that
// need healing in a given bucket matching the given prefix.
func (adminAPI adminAPIHandlers) ListUploadsHealHandler(w http.ResponseWriter, r *http.Request) {
// Get object layer instance.
objLayer := newObjectLayerFn()
if objLayer == nil {
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
return
}
// Validate request signature.
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Validate query params.
vars := r.URL.Query()
bucket := vars.Get(string(mgmtBucket))
prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, _ := getBucketMultipartResources(r.URL.Query())
if err := checkListMultipartArgs(bucket, prefix, keyMarker, uploadIDMarker, delimiter, objLayer); err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
if maxUploads <= 0 || maxUploads > maxUploadsList {
writeErrorResponse(w, ErrInvalidMaxUploads, r.URL)
return
}
// Get the list objects to be healed.
listMultipartInfos, err := objLayer.ListUploadsHeal(bucket, prefix,
keyMarker, uploadIDMarker, delimiter, maxUploads)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
listResponse := generateListMultipartUploadsResponse(bucket, listMultipartInfos)
// Write success response.
writeSuccessResponseXML(w, encodeResponse(listResponse))
}
// extractListObjectsHealQuery - Validates query params for heal objects list management API.
func extractListObjectsHealQuery(vars url.Values) (string, string, string, string, int, APIErrorCode) {
// validateHealQueryParams - Validates query params for heal list management API.
func validateHealQueryParams(vars url.Values) (string, string, string, string, int, APIErrorCode) {
bucket := vars.Get(string(mgmtBucket))
prefix := vars.Get(string(mgmtPrefix))
marker := vars.Get(string(mgmtMarker))
@@ -483,13 +316,10 @@ func extractListObjectsHealQuery(vars url.Values) (string, string, string, strin
return "", "", "", "", 0, ErrInvalidObjectName
}
// check if maxKey is a valid integer, if present.
var maxKey int
var err error
if maxKeyStr != "" {
if maxKey, err = strconv.Atoi(maxKeyStr); err != nil {
return "", "", "", "", 0, ErrInvalidMaxKeys
}
// check if maxKey is a valid integer.
maxKey, err := strconv.Atoi(maxKeyStr)
if err != nil {
return "", "", "", "", 0, ErrInvalidMaxKeys
}
// Validate prefix, marker, delimiter and maxKey.
@@ -522,7 +352,7 @@ func (adminAPI adminAPIHandlers) ListObjectsHealHandler(w http.ResponseWriter, r
// Validate query params.
vars := r.URL.Query()
bucket, prefix, marker, delimiter, maxKey, adminAPIErr := extractListObjectsHealQuery(vars)
bucket, prefix, marker, delimiter, maxKey, adminAPIErr := validateHealQueryParams(vars)
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
@@ -621,42 +451,6 @@ func isDryRun(qval url.Values) bool {
return false
}
// healResult - represents result of a heal operation like
// heal-object, heal-upload.
type healResult struct {
State healState `json:"state"`
}
// healState - different states of heal operation
type healState int
const (
// healNone - none of the disks healed
healNone healState = iota
// healPartial - some disks were healed, others were offline
healPartial
// healOK - all disks were healed
healOK
)
// newHealResult - returns healResult given number of disks healed and
// number of disks offline
func newHealResult(numHealedDisks, numOfflineDisks int) healResult {
var state healState
switch {
case numHealedDisks == 0:
state = healNone
case numOfflineDisks > 0:
state = healPartial
default:
state = healOK
}
return healResult{State: state}
}
// HealObjectHandler - POST /?heal&bucket=mybucket&object=myobject&dry-run
// - x-minio-operation = object
// - bucket and object are both mandatory query parameters
@@ -699,95 +493,14 @@ func (adminAPI adminAPIHandlers) HealObjectHandler(w http.ResponseWriter, r *htt
return
}
numOfflineDisks, numHealedDisks, err := objLayer.HealObject(bucket, object)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
jsonBytes, err := json.Marshal(newHealResult(numHealedDisks, numOfflineDisks))
err := objLayer.HealObject(bucket, object)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// Return 200 on success.
writeSuccessResponseJSON(w, jsonBytes)
}
// HealUploadHandler - POST /?heal&bucket=mybucket&object=myobject&upload-id=myuploadID&dry-run
// - x-minio-operation = upload
// - bucket, object and upload-id are mandatory query parameters
// Heal a given upload, if present.
func (adminAPI adminAPIHandlers) HealUploadHandler(w http.ResponseWriter, r *http.Request) {
// Get object layer instance.
objLayer := newObjectLayerFn()
if objLayer == nil {
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
return
}
// Validate request signature.
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
vars := r.URL.Query()
bucket := vars.Get(string(mgmtBucket))
object := vars.Get(string(mgmtObject))
uploadID := vars.Get(string(mgmtUploadID))
uploadObj := path.Join(bucket, object, uploadID)
// Validate bucket and object names as supplied via query
// parameters.
if err := checkBucketAndObjectNames(bucket, object); err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// Validate the bucket and object w.r.t backend representation
// of an upload.
if err := checkBucketAndObjectNames(minioMetaMultipartBucket,
uploadObj); err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// Check if upload exists.
if _, err := objLayer.GetObjectInfo(minioMetaMultipartBucket,
uploadObj); err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// if dry-run is set in query params then perform validations
// and return success.
if isDryRun(vars) {
writeSuccessResponseHeadersOnly(w)
return
}
//We are able to use HealObject for healing an upload since an
//ongoing upload has the same backend representation as an
//object. The 'object' corresponding to a given bucket,
//object and uploadID is
//.minio.sys/multipart/bucket/object/uploadID.
numOfflineDisks, numHealedDisks, err := objLayer.HealObject(minioMetaMultipartBucket, uploadObj)
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
jsonBytes, err := json.Marshal(newHealResult(numHealedDisks, numOfflineDisks))
if err != nil {
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// Return 200 on success.
writeSuccessResponseJSON(w, jsonBytes)
writeSuccessResponseHeadersOnly(w)
}
// HealFormatHandler - POST /?heal&dry-run
@@ -828,6 +541,7 @@ func (adminAPI adminAPIHandlers) HealFormatHandler(w http.ResponseWriter, r *htt
// Create a new set of storage instances to heal format.json.
bootstrapDisks, err := initStorageDisks(globalEndpoints)
if err != nil {
fmt.Println(traceError(err))
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
@@ -835,6 +549,7 @@ func (adminAPI adminAPIHandlers) HealFormatHandler(w http.ResponseWriter, r *htt
// Heal format.json on available storage.
err = healFormatXL(bootstrapDisks)
if err != nil {
fmt.Println(traceError(err))
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
@@ -842,6 +557,7 @@ func (adminAPI adminAPIHandlers) HealFormatHandler(w http.ResponseWriter, r *htt
// Instantiate new object layer with newly formatted storage.
newObjectAPI, err := newXLObjects(bootstrapDisks)
if err != nil {
fmt.Println(traceError(err))
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
@@ -860,152 +576,3 @@ func (adminAPI adminAPIHandlers) HealFormatHandler(w http.ResponseWriter, r *htt
// Return 200 on success.
writeSuccessResponseHeadersOnly(w)
}
// GetConfigHandler - GET /?config
// - x-minio-operation = get
// Get config.json of this minio setup.
func (adminAPI adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
// Validate request signature.
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// check if objectLayer is initialized, if not return.
if newObjectLayerFn() == nil {
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
return
}
// Get config.json from all nodes. In a single node setup, it
// returns local config.json.
configBytes, err := getPeerConfig(globalAdminPeers)
if err != nil {
errorIf(err, "Failed to get config from peers")
writeErrorResponse(w, toAdminAPIErrCode(err), r.URL)
return
}
writeSuccessResponseJSON(w, configBytes)
}
// toAdminAPIErrCode - converts errXLWriteQuorum error to admin API
// specific error.
func toAdminAPIErrCode(err error) APIErrorCode {
switch err {
case errXLWriteQuorum:
return ErrAdminConfigNoQuorum
}
return toAPIErrorCode(err)
}
// SetConfigResult - represents detailed results of a set-config
// operation.
type nodeSummary struct {
Name string `json:"name"`
ErrSet bool `json:"errSet"`
ErrMsg string `json:"errMsg"`
}
type setConfigResult struct {
NodeResults []nodeSummary `json:"nodeResults"`
Status bool `json:"status"`
}
// writeSetConfigResponse - writes setConfigResult value as json depending on the status.
func writeSetConfigResponse(w http.ResponseWriter, peers adminPeers, errs []error, status bool, reqURL *url.URL) {
var nodeResults []nodeSummary
// Build nodeResults based on error values received during
// set-config operation.
for i := range errs {
nodeResults = append(nodeResults, nodeSummary{
Name: peers[i].addr,
ErrSet: errs[i] != nil,
ErrMsg: fmt.Sprintf("%v", errs[i]),
})
}
result := setConfigResult{
Status: status,
NodeResults: nodeResults,
}
// The following elaborate json encoding is to avoid escaping
// '<', '>' in <nil>. Note: json.Encoder.Encode() adds a
// gratuitous "\n".
var resultBuf bytes.Buffer
enc := json.NewEncoder(&resultBuf)
enc.SetEscapeHTML(false)
jsonErr := enc.Encode(result)
if jsonErr != nil {
writeErrorResponse(w, toAPIErrorCode(jsonErr), reqURL)
return
}
writeSuccessResponseJSON(w, resultBuf.Bytes())
return
}
// SetConfigHandler - PUT /?config
// - x-minio-operation = set
func (adminAPI adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
// Get current object layer instance.
objectAPI := newObjectLayerFn()
if objectAPI == nil {
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
return
}
// Validate request signature.
adminAPIErr := checkRequestAuthType(r, "", "", "")
if adminAPIErr != ErrNone {
writeErrorResponse(w, adminAPIErr, r.URL)
return
}
// Read configuration bytes from request body.
configBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
errorIf(err, "Failed to read config from request body.")
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
// Write config received from request onto a temporary file on
// all nodes.
tmpFileName := fmt.Sprintf(minioConfigTmpFormat, mustGetUUID())
errs := writeTmpConfigPeers(globalAdminPeers, tmpFileName, configBytes)
// Check if the operation succeeded in quorum or more nodes.
rErr := reduceWriteQuorumErrs(errs, nil, len(globalAdminPeers)/2+1)
if rErr != nil {
writeSetConfigResponse(w, globalAdminPeers, errs, false, r.URL)
return
}
// Take a lock on minio/config.json. NB minio is a reserved
// bucket name and wouldn't conflict with normal object
// operations.
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
configLock.Lock()
defer configLock.Unlock()
// Rename the temporary config file to config.json
errs = commitConfigPeers(globalAdminPeers, tmpFileName)
rErr = reduceWriteQuorumErrs(errs, nil, len(globalAdminPeers)/2+1)
if rErr != nil {
writeSetConfigResponse(w, globalAdminPeers, errs, false, r.URL)
return
}
// serverMux (cmd/server-mux.go) implements graceful shutdown,
// where all listeners are closed and process restart/shutdown
// happens after 5s or completion of all ongoing http
// requests, whichever is earlier.
writeSetConfigResponse(w, globalAdminPeers, errs, true, r.URL)
// Restart all node for the modified config to take effect.
sendServiceCmd(globalAdminPeers, serviceRestart)
}
+69 -707
View File
@@ -20,115 +20,15 @@ import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
"time"
router "github.com/gorilla/mux"
)
var configJSON = []byte(`{
"version": "13",
"credential": {
"accessKey": "minio",
"secretKey": "minio123"
},
"region": "us-west-1",
"logger": {
"console": {
"enable": true,
"level": "fatal"
},
"file": {
"enable": false,
"fileName": "",
"level": ""
}
},
"notify": {
"amqp": {
"1": {
"enable": false,
"url": "",
"exchange": "",
"routingKey": "",
"exchangeType": "",
"mandatory": false,
"immediate": false,
"durable": false,
"internal": false,
"noWait": false,
"autoDeleted": false
}
},
"nats": {
"1": {
"enable": false,
"address": "",
"subject": "",
"username": "",
"password": "",
"token": "",
"secure": false,
"pingInterval": 0,
"streaming": {
"enable": false,
"clusterID": "",
"clientID": "",
"async": false,
"maxPubAcksInflight": 0
}
}
},
"elasticsearch": {
"1": {
"enable": false,
"url": "",
"index": ""
}
},
"redis": {
"1": {
"enable": false,
"address": "",
"password": "",
"key": ""
}
},
"postgresql": {
"1": {
"enable": false,
"connectionString": "",
"table": "",
"host": "",
"port": "",
"user": "",
"password": "",
"database": ""
}
},
"kafka": {
"1": {
"enable": false,
"brokers": null,
"topic": ""
}
},
"webhook": {
"1": {
"enable": false,
"endpoint": ""
}
}
}
}`)
// adminXLTestBed - encapsulates subsystems that need to be setup for
// admin-handler unit tests.
type adminXLTestBed struct {
@@ -143,7 +43,6 @@ type adminXLTestBed struct {
func prepareAdminXLTestBed() (*adminXLTestBed, error) {
// reset global variables to start afresh.
resetTestGlobals()
// Initialize minio server config.
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
@@ -155,10 +54,12 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
return nil, xlErr
}
// Initialize boot time
globalBootTime = UTCNow()
globalEndpoints = mustGetNewEndpointList(xlDirs...)
// Set globalEndpoints for a single node XL setup.
for _, xlDir := range xlDirs {
globalEndpoints = append(globalEndpoints, &url.URL{
Path: xlDir,
})
}
// Set globalIsXL to indicate that the setup uses an erasure code backend.
globalIsXL = true
@@ -296,8 +197,14 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) {
// Initialize admin peers to make admin RPC calls. Note: In a
// single node setup, this degenerates to a simple function
// call under the hood.
globalMinioAddr = "127.0.0.1:9000"
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
eps, err := parseStorageEndpoints([]string{"http://localhost"})
if err != nil {
t.Fatalf("Failed to parse storage end point - %v", err)
}
// Set globalMinioAddr to be able to distinguish local endpoints from remote.
globalMinioAddr = eps[0].Host
initGlobalAdminPeers(eps)
// Setting up a go routine to simulate ServerMux's
// handleServiceSignals for stop and restart commands.
@@ -317,13 +224,14 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) {
if cmd == statusCmd {
expectedInfo := ServerStatus{
StorageInfo: newObjectLayerFn().StorageInfo(),
ServerVersion: ServerVersion{Version: Version, CommitID: CommitID},
}
receivedInfo := ServerStatus{}
if jsonErr := json.Unmarshal(rec.Body.Bytes(), &receivedInfo); jsonErr != nil {
t.Errorf("Failed to unmarshal StorageInfo - %v", jsonErr)
}
if expectedInfo.ServerVersion != receivedInfo.ServerVersion {
if expectedInfo != receivedInfo {
t.Errorf("Expected storage info and received storage info differ, %v %v", expectedInfo, receivedInfo)
}
}
@@ -356,8 +264,14 @@ func TestServiceSetCreds(t *testing.T) {
// Initialize admin peers to make admin RPC calls. Note: In a
// single node setup, this degenerates to a simple function
// call under the hood.
globalMinioAddr = "127.0.0.1:9000"
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
eps, err := parseStorageEndpoints([]string{"http://localhost"})
if err != nil {
t.Fatalf("Failed to parse storage end point - %v", err)
}
// Set globalMinioAddr to be able to distinguish local endpoints from remote.
globalMinioAddr = eps[0].Host
initGlobalAdminPeers(eps)
credentials := serverConfig.GetCredential()
var body []byte
@@ -380,9 +294,11 @@ func TestServiceSetCreds(t *testing.T) {
for i, testCase := range testCases {
// Set or unset environement keys
if !testCase.EnvKeysSet {
globalIsEnvCreds = false
globalEnvAccessKey = ""
globalEnvSecretKey = ""
} else {
globalIsEnvCreds = true
globalEnvAccessKey = testCase.Username
globalEnvSecretKey = testCase.Password
}
// Construct setCreds request body
@@ -420,12 +336,12 @@ func TestServiceSetCreds(t *testing.T) {
}
// mkLockQueryVal - helper function to build lock query param.
func mkLockQueryVal(bucket, prefix, durationStr string) url.Values {
func mkLockQueryVal(bucket, prefix, relTimeStr string) url.Values {
qVal := url.Values{}
qVal.Set("lock", "")
qVal.Set(string(mgmtBucket), bucket)
qVal.Set(string(mgmtPrefix), prefix)
qVal.Set(string(mgmtLockDuration), durationStr)
qVal.Set(string(mgmtOlderThan), relTimeStr)
return qVal
}
@@ -438,47 +354,53 @@ func TestListLocksHandler(t *testing.T) {
defer adminTestBed.TearDown()
// Initialize admin peers to make admin RPC calls.
globalMinioAddr = "127.0.0.1:9000"
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
eps, err := parseStorageEndpoints([]string{"http://localhost"})
if err != nil {
t.Fatalf("Failed to parse storage end point - %v", err)
}
// Set globalMinioAddr to be able to distinguish local endpoints from remote.
globalMinioAddr = eps[0].Host
initGlobalAdminPeers(eps)
testCases := []struct {
bucket string
prefix string
duration string
relTime string
expectedStatus int
}{
// Test 1 - valid testcase
{
bucket: "mybucket",
prefix: "myobject",
duration: "1s",
relTime: "1s",
expectedStatus: http.StatusOK,
},
// Test 2 - invalid duration
{
bucket: "mybucket",
prefix: "myprefix",
duration: "invalidDuration",
relTime: "invalidDuration",
expectedStatus: http.StatusBadRequest,
},
// Test 3 - invalid bucket name
{
bucket: `invalid\\Bucket`,
prefix: "myprefix",
duration: "1h",
relTime: "1h",
expectedStatus: http.StatusBadRequest,
},
// Test 4 - invalid prefix
{
bucket: "mybucket",
prefix: `invalid\\Prefix`,
duration: "1h",
relTime: "1h",
expectedStatus: http.StatusBadRequest,
},
}
for i, test := range testCases {
queryVal := mkLockQueryVal(test.bucket, test.prefix, test.duration)
queryVal := mkLockQueryVal(test.bucket, test.prefix, test.relTime)
req, err := newTestRequest("GET", "/?"+queryVal.Encode(), 0, nil)
if err != nil {
t.Fatalf("Test %d - Failed to construct list locks request - %v", i+1, err)
@@ -507,46 +429,50 @@ func TestClearLocksHandler(t *testing.T) {
defer adminTestBed.TearDown()
// Initialize admin peers to make admin RPC calls.
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
eps, err := parseStorageEndpoints([]string{"http://localhost"})
if err != nil {
t.Fatalf("Failed to parse storage end point - %v", err)
}
initGlobalAdminPeers(eps)
testCases := []struct {
bucket string
prefix string
duration string
relTime string
expectedStatus int
}{
// Test 1 - valid testcase
{
bucket: "mybucket",
prefix: "myobject",
duration: "1s",
relTime: "1s",
expectedStatus: http.StatusOK,
},
// Test 2 - invalid duration
{
bucket: "mybucket",
prefix: "myprefix",
duration: "invalidDuration",
relTime: "invalidDuration",
expectedStatus: http.StatusBadRequest,
},
// Test 3 - invalid bucket name
{
bucket: `invalid\\Bucket`,
prefix: "myprefix",
duration: "1h",
relTime: "1h",
expectedStatus: http.StatusBadRequest,
},
// Test 4 - invalid prefix
{
bucket: "mybucket",
prefix: `invalid\\Prefix`,
duration: "1h",
relTime: "1h",
expectedStatus: http.StatusBadRequest,
},
}
for i, test := range testCases {
queryVal := mkLockQueryVal(test.bucket, test.prefix, test.duration)
queryVal := mkLockQueryVal(test.bucket, test.prefix, test.relTime)
req, err := newTestRequest("POST", "/?"+queryVal.Encode(), 0, nil)
if err != nil {
t.Fatalf("Test %d - Failed to construct clear locks request - %v", i+1, err)
@@ -715,7 +641,7 @@ func TestValidateHealQueryParams(t *testing.T) {
}
for i, test := range testCases {
vars := mkListObjectsQueryVal(test.bucket, test.prefix, test.marker, test.delimiter, test.maxKeys)
_, _, _, _, _, actualErr := extractListObjectsHealQuery(vars)
_, _, _, _, _, actualErr := validateHealQueryParams(vars)
if actualErr != test.apiErr {
t.Errorf("Test %d - Expected %v but received %v",
i+1, getAPIError(test.apiErr), getAPIError(actualErr))
@@ -731,7 +657,7 @@ func TestListObjectsHealHandler(t *testing.T) {
}
defer adminTestBed.TearDown()
err = adminTestBed.objLayer.MakeBucketWithLocation("mybucket", "")
err = adminTestBed.objLayer.MakeBucket("mybucket")
if err != nil {
t.Fatalf("Failed to make bucket - %v", err)
}
@@ -831,6 +757,9 @@ func TestListObjectsHealHandler(t *testing.T) {
}
for i, test := range testCases {
if i != 0 {
continue
}
queryVal := mkListObjectsQueryVal(test.bucket, test.prefix, test.marker, test.delimiter, test.maxKeys)
req, err := newTestRequest("GET", "/?"+queryVal.Encode(), 0, nil)
if err != nil {
@@ -859,7 +788,7 @@ func TestHealBucketHandler(t *testing.T) {
}
defer adminTestBed.TearDown()
err = adminTestBed.objLayer.MakeBucketWithLocation("mybucket", "")
err = adminTestBed.objLayer.MakeBucket("mybucket")
if err != nil {
t.Fatalf("Failed to make bucket - %v", err)
}
@@ -936,7 +865,7 @@ func TestHealObjectHandler(t *testing.T) {
// Create an object myobject under bucket mybucket.
bucketName := "mybucket"
objName := "myobject"
err = adminTestBed.objLayer.MakeBucketWithLocation(bucketName, "")
err = adminTestBed.objLayer.MakeBucket(bucketName)
if err != nil {
t.Fatalf("Failed to make bucket %s - %v", bucketName, err)
}
@@ -1023,160 +952,6 @@ func TestHealObjectHandler(t *testing.T) {
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.statusCode, rec.Code)
}
}
}
// buildAdminRequest - helper function to build an admin API request.
func buildAdminRequest(queryVal url.Values, opHdr, method string,
contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error) {
req, err := newTestRequest(method, "/?"+queryVal.Encode(), contentLength, bodySeeker)
if err != nil {
return nil, traceError(err)
}
req.Header.Set(minioAdminOpHeader, opHdr)
cred := serverConfig.GetCredential()
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
if err != nil {
return nil, traceError(err)
}
return req, nil
}
// mkHealUploadQuery - helper to build HealUploadHandler query string.
func mkHealUploadQuery(bucket, object, uploadID, dryRun string) url.Values {
queryVal := url.Values{}
queryVal.Set(string(mgmtBucket), bucket)
queryVal.Set(string(mgmtObject), object)
queryVal.Set(string(mgmtUploadID), uploadID)
queryVal.Set("heal", "")
queryVal.Set(string(mgmtDryRun), dryRun)
return queryVal
}
// TestHealUploadHandler - test for HealUploadHandler.
func TestHealUploadHandler(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
// Create an object myobject under bucket mybucket.
bucketName := "mybucket"
objName := "myobject"
err = adminTestBed.objLayer.MakeBucketWithLocation(bucketName, "")
if err != nil {
t.Fatalf("Failed to make bucket %s - %v", bucketName, err)
}
// Create a new multipart upload.
uploadID, err := adminTestBed.objLayer.NewMultipartUpload(bucketName, objName, nil)
if err != nil {
t.Fatalf("Failed to create a new multipart upload %s/%s - %v",
bucketName, objName, err)
}
// Upload a part.
partID := 1
_, err = adminTestBed.objLayer.PutObjectPart(bucketName, objName, uploadID,
partID, int64(len("hello")), bytes.NewReader([]byte("hello")), "", "")
if err != nil {
t.Fatalf("Failed to upload part %d of %s/%s - %v", partID,
bucketName, objName, err)
}
testCases := []struct {
bucket string
object string
dryrun string
statusCode int
}{
// 1. Valid test case.
{
bucket: bucketName,
object: objName,
statusCode: http.StatusOK,
},
// 2. Invalid bucket name.
{
bucket: `invalid\\Bucket`,
object: "myobject",
statusCode: http.StatusBadRequest,
},
// 3. Bucket not found.
{
bucket: "bucketnotfound",
object: "myobject",
statusCode: http.StatusNotFound,
},
// 4. Invalid object name.
{
bucket: bucketName,
object: `invalid\\Object`,
statusCode: http.StatusBadRequest,
},
// 5. Object not found.
{
bucket: bucketName,
object: "objectnotfound",
statusCode: http.StatusNotFound,
},
// 6. Valid test case with dry-run.
{
bucket: bucketName,
object: objName,
dryrun: "yes",
statusCode: http.StatusOK,
},
}
for i, test := range testCases {
// Prepare query params.
queryVal := mkHealUploadQuery(test.bucket, test.object, uploadID, test.dryrun)
req, err1 := buildAdminRequest(queryVal, "upload", http.MethodPost, 0, nil)
if err1 != nil {
t.Fatalf("Test %d - Failed to construct heal object request - %v", i+1, err1)
}
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if test.statusCode != rec.Code {
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.statusCode, rec.Code)
}
}
sample := testCases[0]
// Modify authorization header after signing to test signature
// mismatch handling.
queryVal := mkHealUploadQuery(sample.bucket, sample.object, uploadID, sample.dryrun)
req, err := buildAdminRequest(queryVal, "upload", "POST", 0, nil)
if err != nil {
t.Fatalf("Failed to construct heal object request - %v", err)
}
// Set x-amz-date to a date different than time of signing.
req.Header.Set("x-amz-date", time.Time{}.Format(iso8601Format))
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Errorf("Expected %d but received %d", http.StatusBadRequest, rec.Code)
}
// Set objectAPI to nil to test Server not initialized case.
resetGlobalObjectAPI()
queryVal = mkHealUploadQuery(sample.bucket, sample.object, uploadID, sample.dryrun)
req, err = buildAdminRequest(queryVal, "upload", "POST", 0, nil)
if err != nil {
t.Fatalf("Failed to construct heal object request - %v", err)
}
rec = httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Errorf("Expected %d but received %d", http.StatusServiceUnavailable, rec.Code)
}
}
// TestHealFormatHandler - test for HealFormatHandler.
@@ -1190,37 +965,19 @@ func TestHealFormatHandler(t *testing.T) {
// Prepare query params for heal-format mgmt REST API.
queryVal := url.Values{}
queryVal.Set("heal", "")
req, err := buildAdminRequest(queryVal, "format", "POST", 0, nil)
req, err := newTestRequest("POST", "/?"+queryVal.Encode(), 0, nil)
if err != nil {
t.Fatalf("Failed to construct heal object request - %v", err)
}
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("Expected to succeed but failed with %d", rec.Code)
}
}
// Set x-minio-operation header to format.
req.Header.Set(minioAdminOpHeader, "format")
// TestGetConfigHandler - test for GetConfigHandler.
func TestGetConfigHandler(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
// Sign the request using signature v4.
cred := serverConfig.GetCredential()
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
// Initialize admin peers to make admin RPC calls.
globalMinioAddr = "127.0.0.1:9000"
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
// Prepare query params for get-config mgmt REST API.
queryVal := url.Values{}
queryVal.Set("config", "")
req, err := buildAdminRequest(queryVal, "get", http.MethodGet, 0, nil)
if err != nil {
t.Fatalf("Failed to construct get-config object request - %v", err)
t.Fatalf("Failed to sign heal object request - %v", err)
}
rec := httptest.NewRecorder()
@@ -1228,399 +985,4 @@ func TestGetConfigHandler(t *testing.T) {
if rec.Code != http.StatusOK {
t.Errorf("Expected to succeed but failed with %d", rec.Code)
}
}
// TestSetConfigHandler - test for SetConfigHandler.
func TestSetConfigHandler(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
// Initialize admin peers to make admin RPC calls.
globalMinioAddr = "127.0.0.1:9000"
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
// SetConfigHandler restarts minio setup - need to start a
// signal receiver to receive on globalServiceSignalCh.
go testServiceSignalReceiver(restartCmd, t)
// Prepare query params for set-config mgmt REST API.
queryVal := url.Values{}
queryVal.Set("config", "")
req, err := buildAdminRequest(queryVal, "set", http.MethodPut, int64(len(configJSON)),
bytes.NewReader(configJSON))
if err != nil {
t.Fatalf("Failed to construct get-config object request - %v", err)
}
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("Expected to succeed but failed with %d", rec.Code)
}
result := setConfigResult{}
err = json.NewDecoder(rec.Body).Decode(&result)
if err != nil {
t.Fatalf("Failed to decode set config result json %v", err)
}
if !result.Status {
t.Error("Expected set-config to succeed, but failed")
}
}
func TestAdminServerInfo(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
// Initialize admin peers to make admin RPC calls.
globalMinioAddr = "127.0.0.1:9000"
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
// Prepare query params for set-config mgmt REST API.
queryVal := url.Values{}
queryVal.Set("info", "")
req, err := buildAdminRequest(queryVal, "", http.MethodGet, 0, nil)
if err != nil {
t.Fatalf("Failed to construct get-config object request - %v", err)
}
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("Expected to succeed but failed with %d", rec.Code)
}
results := []ServerInfo{}
err = json.NewDecoder(rec.Body).Decode(&results)
if err != nil {
t.Fatalf("Failed to decode set config result json %v", err)
}
if len(results) == 0 {
t.Error("Expected at least one server info result")
}
for _, serverInfo := range results {
if len(serverInfo.Addr) == 0 {
t.Error("Expected server address to be non empty")
}
if serverInfo.Error != "" {
t.Errorf("Unexpected error = %v\n", serverInfo.Error)
}
if serverInfo.Data.StorageInfo.Free == 0 {
t.Error("Expected StorageInfo.Free to be non empty")
}
if serverInfo.Data.Properties.Region != globalMinioDefaultRegion {
t.Errorf("Expected %s, got %s", globalMinioDefaultRegion, serverInfo.Data.Properties.Region)
}
}
}
// TestToAdminAPIErr - test for toAdminAPIErr helper function.
func TestToAdminAPIErr(t *testing.T) {
testCases := []struct {
err error
expectedAPIErr APIErrorCode
}{
// 1. Server not in quorum.
{
err: errXLWriteQuorum,
expectedAPIErr: ErrAdminConfigNoQuorum,
},
// 2. No error.
{
err: nil,
expectedAPIErr: ErrNone,
},
// 3. Non-admin API specific error.
{
err: errDiskNotFound,
expectedAPIErr: toAPIErrorCode(errDiskNotFound),
},
}
for i, test := range testCases {
actualErr := toAdminAPIErrCode(test.err)
if actualErr != test.expectedAPIErr {
t.Errorf("Test %d: Expected %v but received %v",
i+1, test.expectedAPIErr, actualErr)
}
}
}
func TestWriteSetConfigResponse(t *testing.T) {
rootPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
t.Fatal(err)
}
defer removeAll(rootPath)
testCases := []struct {
status bool
errs []error
}{
// 1. all nodes returned success.
{
status: true,
errs: []error{nil, nil, nil, nil},
},
// 2. some nodes returned errors.
{
status: false,
errs: []error{errDiskNotFound, nil, errDiskAccessDenied, errFaultyDisk},
},
}
testPeers := []adminPeer{
{
addr: "localhost:9001",
},
{
addr: "localhost:9002",
},
{
addr: "localhost:9003",
},
{
addr: "localhost:9004",
},
}
testURL, err := url.Parse("http://dummy.com")
if err != nil {
t.Fatalf("Failed to parse a place-holder url")
}
var actualResult setConfigResult
for i, test := range testCases {
rec := httptest.NewRecorder()
writeSetConfigResponse(rec, testPeers, test.errs, test.status, testURL)
resp := rec.Result()
jsonBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Test %d: Failed to read response %v", i+1, err)
}
err = json.Unmarshal(jsonBytes, &actualResult)
if err != nil {
t.Fatalf("Test %d: Failed to unmarshal json %v", i+1, err)
}
if actualResult.Status != test.status {
t.Errorf("Test %d: Expected status %v but received %v", i+1, test.status, actualResult.Status)
}
for p, res := range actualResult.NodeResults {
if res.Name != testPeers[p].addr {
t.Errorf("Test %d: Expected node name %s but received %s", i+1, testPeers[p].addr, res.Name)
}
expectedErrMsg := fmt.Sprintf("%v", test.errs[p])
if res.ErrMsg != expectedErrMsg {
t.Errorf("Test %d: Expected error %s but received %s", i+1, expectedErrMsg, res.ErrMsg)
}
expectedErrSet := test.errs[p] != nil
if res.ErrSet != expectedErrSet {
t.Errorf("Test %d: Expected ErrSet %v but received %v", i+1, expectedErrSet, res.ErrSet)
}
}
}
}
// mkListUploadsHealQuery - helper function to construct query values for
// listUploadsHeal.
func mkListUploadsHealQuery(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploadsStr string) url.Values {
queryVal := make(url.Values)
queryVal.Set("heal", "")
queryVal.Set(string(mgmtBucket), bucket)
queryVal.Set(string(mgmtPrefix), prefix)
queryVal.Set(string(mgmtKeyMarker), keyMarker)
queryVal.Set(string(mgmtUploadIDMarker), uploadIDMarker)
queryVal.Set(string(mgmtDelimiter), delimiter)
queryVal.Set(string(mgmtMaxUploads), maxUploadsStr)
return queryVal
}
func TestListHealUploadsHandler(t *testing.T) {
adminTestBed, err := prepareAdminXLTestBed()
if err != nil {
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
}
defer adminTestBed.TearDown()
err = adminTestBed.objLayer.MakeBucketWithLocation("mybucket", "")
if err != nil {
t.Fatalf("Failed to make bucket - %v", err)
}
// Delete bucket after running all test cases.
defer adminTestBed.objLayer.DeleteBucket("mybucket")
testCases := []struct {
bucket string
prefix string
keyMarker string
delimiter string
maxKeys string
statusCode int
expectedResp ListMultipartUploadsResponse
}{
// 1. Valid params.
{
bucket: "mybucket",
prefix: "prefix",
keyMarker: "prefix11",
delimiter: "/",
maxKeys: "10",
statusCode: http.StatusOK,
expectedResp: ListMultipartUploadsResponse{
XMLName: xml.Name{Space: "http://s3.amazonaws.com/doc/2006-03-01/", Local: "ListMultipartUploadsResult"},
Bucket: "mybucket",
KeyMarker: "prefix11",
Delimiter: "/",
Prefix: "prefix",
MaxUploads: 10,
},
},
// 2. Valid params with empty prefix.
{
bucket: "mybucket",
prefix: "",
keyMarker: "",
delimiter: "/",
maxKeys: "10",
statusCode: http.StatusOK,
expectedResp: ListMultipartUploadsResponse{
XMLName: xml.Name{Space: "http://s3.amazonaws.com/doc/2006-03-01/", Local: "ListMultipartUploadsResult"},
Bucket: "mybucket",
KeyMarker: "",
Delimiter: "/",
Prefix: "",
MaxUploads: 10,
},
},
// 3. Invalid params with invalid bucket.
{
bucket: `invalid\\Bucket`,
prefix: "prefix",
keyMarker: "prefix11",
delimiter: "/",
maxKeys: "10",
statusCode: getAPIError(ErrInvalidBucketName).HTTPStatusCode,
},
// 4. Invalid params with invalid prefix.
{
bucket: "mybucket",
prefix: `invalid\\Prefix`,
keyMarker: "prefix11",
delimiter: "/",
maxKeys: "10",
statusCode: getAPIError(ErrInvalidObjectName).HTTPStatusCode,
},
// 5. Invalid params with invalid maxKeys.
{
bucket: "mybucket",
prefix: "prefix",
keyMarker: "prefix11",
delimiter: "/",
maxKeys: "-1",
statusCode: getAPIError(ErrInvalidMaxUploads).HTTPStatusCode,
},
// 6. Invalid params with unsupported prefix marker combination.
{
bucket: "mybucket",
prefix: "prefix",
keyMarker: "notmatchingmarker",
delimiter: "/",
maxKeys: "10",
statusCode: getAPIError(ErrNotImplemented).HTTPStatusCode,
},
// 7. Invalid params with unsupported delimiter.
{
bucket: "mybucket",
prefix: "prefix",
keyMarker: "notmatchingmarker",
delimiter: "unsupported",
maxKeys: "10",
statusCode: getAPIError(ErrNotImplemented).HTTPStatusCode,
},
// 8. Invalid params with invalid max Keys
{
bucket: "mybucket",
prefix: "prefix",
keyMarker: "prefix11",
delimiter: "/",
maxKeys: "999999999999999999999999999",
statusCode: getAPIError(ErrInvalidMaxUploads).HTTPStatusCode,
},
}
for i, test := range testCases {
queryVal := mkListUploadsHealQuery(test.bucket, test.prefix, test.keyMarker, "", test.delimiter, test.maxKeys)
req, err := buildAdminRequest(queryVal, "list-uploads", http.MethodGet, 0, nil)
if err != nil {
t.Fatalf("Test %d - Failed to construct list uploads needing heal request - %v", i+1, err)
}
rec := httptest.NewRecorder()
adminTestBed.mux.ServeHTTP(rec, req)
if test.statusCode != rec.Code {
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.statusCode, rec.Code)
}
// Compare result with the expected one only when we receive 200 OK
if rec.Code == http.StatusOK {
resp := rec.Result()
xmlBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("Test %d: Failed to read response %v", i+1, err)
}
var actualResult ListMultipartUploadsResponse
err = xml.Unmarshal(xmlBytes, &actualResult)
if err != nil {
t.Errorf("Test %d: Failed to unmarshal xml %v", i+1, err)
}
if !reflect.DeepEqual(test.expectedResp, actualResult) {
t.Fatalf("Test %d: Unexpected response `%+v`, expected: `%+v`", i+1, test.expectedResp, actualResult)
}
}
}
}
// Test for newHealResult helper function.
func TestNewHealResult(t *testing.T) {
testCases := []struct {
healedDisks int
offlineDisks int
state healState
}{
// 1. No disks healed, no disks offline.
{0, 0, healNone},
// 2. No disks healed, non-zero disks offline.
{0, 1, healNone},
// 3. Non-zero disks healed, no disks offline.
{1, 0, healOK},
// 4. Non-zero disks healed, non-zero disks offline.
{1, 1, healPartial},
}
for i, test := range testCases {
actual := newHealResult(test.healedDisks, test.offlineDisks)
if actual.State != test.state {
t.Errorf("Test %d: Expected %v but received %v", i+1,
test.state, actual.State)
}
}
}
-14
View File
@@ -39,9 +39,6 @@ func registerAdminRouter(mux *router.Router) {
// Service update credentials
adminRouter.Methods("POST").Queries("service", "").Headers(minioAdminOpHeader, "set-credentials").HandlerFunc(adminAPI.ServiceCredentialsHandler)
// Info operations
adminRouter.Methods("GET").Queries("info", "").HandlerFunc(adminAPI.ServerInfoHandler)
/// Lock operations
// List Locks
@@ -53,8 +50,6 @@ func registerAdminRouter(mux *router.Router) {
// List Objects needing heal.
adminRouter.Methods("GET").Queries("heal", "").Headers(minioAdminOpHeader, "list-objects").HandlerFunc(adminAPI.ListObjectsHealHandler)
// List Uploads needing heal.
adminRouter.Methods("GET").Queries("heal", "").Headers(minioAdminOpHeader, "list-uploads").HandlerFunc(adminAPI.ListUploadsHealHandler)
// List Buckets needing heal.
adminRouter.Methods("GET").Queries("heal", "").Headers(minioAdminOpHeader, "list-buckets").HandlerFunc(adminAPI.ListBucketsHealHandler)
@@ -64,13 +59,4 @@ func registerAdminRouter(mux *router.Router) {
adminRouter.Methods("POST").Queries("heal", "").Headers(minioAdminOpHeader, "object").HandlerFunc(adminAPI.HealObjectHandler)
// Heal Format.
adminRouter.Methods("POST").Queries("heal", "").Headers(minioAdminOpHeader, "format").HandlerFunc(adminAPI.HealFormatHandler)
// Heal Uploads.
adminRouter.Methods("POST").Queries("heal", "").Headers(minioAdminOpHeader, "upload").HandlerFunc(adminAPI.HealUploadHandler)
/// Config operations
// Get config
adminRouter.Methods("GET").Queries("config", "").Headers(minioAdminOpHeader, "get").HandlerFunc(adminAPI.GetConfigHandler)
// Set Config
adminRouter.Methods("PUT").Queries("config", "").Headers(minioAdminOpHeader, "set").HandlerFunc(adminAPI.SetConfigHandler)
}
+50 -432
View File
@@ -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,30 +17,10 @@
package cmd
import (
"encoding/json"
"errors"
"fmt"
"net"
"os"
"net/url"
"path"
"path/filepath"
"reflect"
"sort"
"sync"
"time"
"github.com/minio/minio-go/pkg/set"
)
const (
// Admin service names
serviceRestartRPC = "Admin.Restart"
listLocksRPC = "Admin.ListLocks"
reInitDisksRPC = "Admin.ReInitDisks"
serverInfoDataRPC = "Admin.ServerInfoData"
getConfigRPC = "Admin.GetConfig"
writeTmpConfigRPC = "Admin.WriteTmpConfig"
commitConfigRPC = "Admin.CommitConfig"
)
// localAdminClient - represents admin operation to be executed locally.
@@ -57,12 +37,8 @@ type remoteAdminClient struct {
// commands like service stop and service restart.
type adminCmdRunner interface {
Restart() error
ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error)
ListLocks(bucket, prefix string, relTime time.Duration) ([]VolumeLockInfo, error)
ReInitDisks() error
ServerInfoData() (ServerInfoData, error)
GetConfig() ([]byte, error)
WriteTmpConfig(tmpFileName string, configBytes []byte) error
CommitConfig(tmpFileName string) error
}
// Restart - Sends a message over channel to the go-routine
@@ -73,26 +49,26 @@ func (lc localAdminClient) Restart() error {
}
// 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 (lc localAdminClient) ListLocks(bucket, prefix string, relTime time.Duration) ([]VolumeLockInfo, error) {
return listLocksInfo(bucket, prefix, relTime), nil
}
// Restart - Sends restart command to remote server via RPC.
func (rc remoteAdminClient) Restart() error {
args := AuthRPCArgs{}
reply := AuthRPCReply{}
return rc.Call(serviceRestartRPC, &args, &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) {
func (rc remoteAdminClient) ListLocks(bucket, prefix string, relTime time.Duration) ([]VolumeLockInfo, error) {
listArgs := ListLocksQuery{
bucket: bucket,
prefix: prefix,
duration: duration,
bucket: bucket,
prefix: prefix,
relTime: relTime,
}
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
@@ -109,120 +85,7 @@ func (lc localAdminClient) ReInitDisks() error {
func (rc remoteAdminClient) ReInitDisks() error {
args := AuthRPCArgs{}
reply := AuthRPCReply{}
return rc.Call(reInitDisksRPC, &args, &reply)
}
// ServerInfoData - Returns the server info of this server.
func (lc localAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
if globalBootTime.IsZero() {
return sid, 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: serverConfig.GetRegion(),
},
}, nil
}
// ServerInfo - returns the server info of the server to which the RPC call is made.
func (rc remoteAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
args := AuthRPCArgs{}
reply := ServerInfoDataReply{}
err := rc.Call(serverInfoDataRPC, &args, &reply)
if err != nil {
return sid, err
}
return reply.ServerInfoData, nil
}
// GetConfig - returns config.json of the local server.
func (lc localAdminClient) GetConfig() ([]byte, error) {
if serverConfig == nil {
return nil, errors.New("config not present")
}
return json.Marshal(serverConfig)
}
// GetConfig - returns config.json of the remote server.
func (rc remoteAdminClient) GetConfig() ([]byte, error) {
args := AuthRPCArgs{}
reply := ConfigReply{}
if err := rc.Call(getConfigRPC, &args, &reply); err != nil {
return nil, err
}
return reply.Config, nil
}
// WriteTmpConfig - writes config file content to a temporary file on
// the local server.
func (lc localAdminClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
return writeTmpConfigCommon(tmpFileName, configBytes)
}
// WriteTmpConfig - writes config file content to a temporary file on
// a remote node.
func (rc remoteAdminClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
wArgs := WriteConfigArgs{
TmpFileName: tmpFileName,
Buf: configBytes,
}
err := rc.Call(writeTmpConfigRPC, &wArgs, &WriteConfigReply{})
if err != nil {
errorIf(err, "Failed to write temporary config file.")
return err
}
return nil
}
// CommitConfig - Move the new config in tmpFileName onto config.json
// on a local node.
func (lc localAdminClient) CommitConfig(tmpFileName string) error {
configFile := getConfigFile()
tmpConfigFile := filepath.Join(getConfigDir(), tmpFileName)
err := os.Rename(tmpConfigFile, configFile)
errorIf(err, fmt.Sprintf("Failed to rename %s to %s", tmpConfigFile, configFile))
return err
}
// CommitConfig - Move the new config in tmpFileName onto config.json
// on a remote node.
func (rc remoteAdminClient) CommitConfig(tmpFileName string) error {
cArgs := CommitConfigArgs{
FileName: tmpFileName,
}
cReply := CommitConfigReply{}
err := rc.Call(commitConfigRPC, &cArgs, &cReply)
if err != nil {
errorIf(err, "Failed to rename config file.")
return err
}
return nil
return rc.Call("Admin.ReInitDisks", &args, &reply)
}
// adminPeer - represents an entity that implements Restart methods.
@@ -235,43 +98,52 @@ type adminPeer struct {
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,
func makeAdminPeers(eps []*url.URL) adminPeers {
var servicePeers []adminPeer
// map to store peers that are already added to ret
seenAddr := make(map[string]bool)
// add local (self) as peer in the array
servicePeers = append(servicePeers, adminPeer{
globalMinioAddr,
localAdminClient{},
})
seenAddr[globalMinioAddr] = true
hostSet := set.CreateStringSet(globalMinioAddr)
cred := serverConfig.GetCredential()
serviceEndpoint := path.Join(minioReservedBucketPath, adminPath)
for _, host := range GetRemotePeers(endpoints) {
if hostSet.Contains(host) {
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",
})},
})
}
servicePeers = append(servicePeers, adminPeer{
addr: ep.Host,
cmdRunner: &remoteAdminClient{newAuthRPCClient(cfg)},
})
seenAddr[ep.Host] = true
}
}
return adminPeerList
return servicePeers
}
// Initialize global adminPeer collection.
func initGlobalAdminPeers(endpoints EndpointList) {
globalAdminPeers = makeAdminPeers(endpoints)
func initGlobalAdminPeers(eps []*url.URL) {
globalAdminPeers = makeAdminPeers(eps)
}
// invokeServiceCmd - Invoke Restart command.
@@ -303,8 +175,8 @@ func sendServiceCmd(cps adminPeers, cmd serviceSignal) {
}
// listPeerLocksInfo - fetch list of locks held on the given bucket,
// matching prefix held longer than duration from all peer servers.
func listPeerLocksInfo(peers adminPeers, bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
// matching prefix older than relTime from all peer servers.
func listPeerLocksInfo(peers adminPeers, bucket, prefix string, relTime time.Duration) ([]VolumeLockInfo, error) {
// Used to aggregate volume lock information from all nodes.
allLocks := make([][]VolumeLockInfo, len(peers))
errs := make([]error, len(peers))
@@ -316,11 +188,11 @@ func listPeerLocksInfo(peers adminPeers, bucket, prefix string, duration time.Du
go func(idx int, remotePeer adminPeer) {
defer wg.Done()
// `remotePeers` is right-shifted by one position relative to `peers`
allLocks[idx], errs[idx] = remotePeer.cmdRunner.ListLocks(bucket, prefix, duration)
allLocks[idx], errs[idx] = remotePeer.cmdRunner.ListLocks(bucket, prefix, relTime)
}(i+1, remotePeer)
}
wg.Wait()
allLocks[0], errs[0] = localPeer.cmdRunner.ListLocks(bucket, prefix, duration)
allLocks[0], errs[0] = localPeer.cmdRunner.ListLocks(bucket, prefix, relTime)
// Summarizing errors received for ListLocks RPC across all
// nodes. N B the possible unavailability of quorum in errors
@@ -369,257 +241,3 @@ func reInitPeerDisks(peers adminPeers) error {
wg.Wait()
return nil
}
// uptimeSlice - used to sort uptimes in chronological order.
type uptimeSlice []struct {
err error
uptime time.Duration
}
func (ts uptimeSlice) Len() int {
return len(ts)
}
func (ts uptimeSlice) Less(i, j int) bool {
return ts[i].uptime < ts[j].uptime
}
func (ts uptimeSlice) Swap(i, j int) {
ts[i], ts[j] = ts[j], ts[i]
}
// getPeerUptimes - returns the uptime since the last time read quorum
// was established on success. Otherwise returns errXLReadQuorum.
func getPeerUptimes(peers adminPeers) (time.Duration, error) {
// In a single node Erasure or FS backend setup the uptime of
// the setup is the uptime of the single minio server
// instance.
if !globalIsDistXL {
return UTCNow().Sub(globalBootTime), nil
}
uptimes := make(uptimeSlice, len(peers))
// Get up time of all servers.
wg := sync.WaitGroup{}
for i, peer := range peers {
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
}(i, peer)
}
wg.Wait()
// Sort uptimes in chronological order.
sort.Sort(uptimes)
// Pick the readQuorum'th uptime in chronological order. i.e,
// the time at which read quorum was (re-)established.
readQuorum := len(uptimes) / 2
validCount := 0
latestUptime := time.Duration(0)
for _, uptime := range uptimes {
if uptime.err != nil {
errorIf(uptime.err, "Unable to fetch uptime")
continue
}
validCount++
if validCount >= readQuorum {
latestUptime = uptime.uptime
break
}
}
// Less than readQuorum "Admin.Uptime" RPC call returned
// successfully, so read-quorum unavailable.
if validCount < readQuorum {
return time.Duration(0), InsufficientReadQuorum{}
}
return latestUptime, nil
}
// getPeerConfig - Fetches config.json from all nodes in the setup and
// returns the one that occurs in a majority of them.
func getPeerConfig(peers adminPeers) ([]byte, error) {
if !globalIsDistXL {
return peers[0].cmdRunner.GetConfig()
}
errs := make([]error, len(peers))
configs := make([][]byte, len(peers))
// Get config from all servers.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
configs[idx], errs[idx] = peer.cmdRunner.GetConfig()
}(i, peer)
}
wg.Wait()
// Find the maximally occurring config among peers in a
// distributed setup.
serverConfigs := make([]serverConfigV13, len(peers))
for i, configBytes := range configs {
if errs[i] != nil {
continue
}
// Unmarshal the received config files.
err := json.Unmarshal(configBytes, &serverConfigs[i])
if err != nil {
errorIf(err, "Failed to unmarshal serverConfig from ", peers[i].addr)
return nil, err
}
}
configJSON, err := getValidServerConfig(serverConfigs, errs)
if err != nil {
errorIf(err, "Unable to find a valid server config")
return nil, traceError(err)
}
// Return the config.json that was present quorum or more
// number of disks.
return json.Marshal(configJSON)
}
// getValidServerConfig - finds the server config that is present in
// quorum or more number of servers.
func getValidServerConfig(serverConfigs []serverConfigV13, errs []error) (scv serverConfigV13, e error) {
// majority-based quorum
quorum := len(serverConfigs)/2 + 1
// Count the number of disks a config.json was found in.
configCounter := make([]int, len(serverConfigs))
// We group equal serverConfigs by the lowest index of the
// same value; e.g, let us take the following serverConfigs
// in a 4-node setup,
// serverConfigs == [c1, c2, c1, c1]
// configCounter == [3, 1, 0, 0]
// c1, c2 are the only distinct values that appear. c1 is
// identified by 0, the lowest index it appears in and c2 is
// identified by 1. So, we need to find the number of times
// each of these distinct values occur.
// Invariants:
// 1. At the beginning of the i-th iteration, the number of
// unique configurations seen so far is equal to the number of
// non-zero counter values in config[:i].
// 2. At the beginning of the i-th iteration, the sum of
// elements of configCounter[:i] is equal to the number of
// non-error configurations seen so far.
// For each of the serverConfig ...
for i := range serverConfigs {
// Skip nodes where getConfig failed.
if errs[i] != nil {
continue
}
// Check if it is equal to any of the configurations
// seen so far. If j == i is reached then we have an
// unseen configuration.
for j := 0; j <= i; j++ {
if j < i && configCounter[j] == 0 {
// serverConfigs[j] is known to be
// equal to a value that was already
// seen. See example above for
// clarity.
continue
} else if j < i && reflect.DeepEqual(serverConfigs[i], serverConfigs[j]) {
// serverConfigs[i] is equal to
// serverConfigs[j], update
// serverConfigs[j]'s counter since it
// is the lower index.
configCounter[j]++
break
} else if j == i {
// serverConfigs[i] is equal to no
// other value seen before. It is
// unique so far.
configCounter[i] = 1
break
} // else invariants specified above are violated.
}
}
// We find the maximally occurring server config and check if
// there is quorum.
var configJSON serverConfigV13
maxOccurrence := 0
for i, count := range configCounter {
if maxOccurrence < count {
maxOccurrence = count
configJSON = serverConfigs[i]
}
}
// If quorum nodes don't agree.
if maxOccurrence < quorum {
return scv, errXLWriteQuorum
}
return configJSON, nil
}
// Write config contents into a temporary file on all nodes.
func writeTmpConfigPeers(peers adminPeers, tmpFileName string, configBytes []byte) []error {
// For a single-node minio server setup.
if !globalIsDistXL {
err := peers[0].cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
return []error{err}
}
errs := make([]error, len(peers))
// Write config into temporary file on all nodes.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
errs[idx] = peer.cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
}(i, peer)
}
wg.Wait()
// Return bytes written and errors (if any) during writing
// temporary config file.
return errs
}
// Move config contents from the given temporary file onto config.json
// on all nodes.
func commitConfigPeers(peers adminPeers, tmpFileName string) []error {
// For a single-node minio server setup.
if !globalIsDistXL {
return []error{peers[0].cmdRunner.CommitConfig(tmpFileName)}
}
errs := make([]error, len(peers))
// Rename temporary config file into configDir/config.json on
// all nodes.
wg := sync.WaitGroup{}
for i, peer := range peers {
wg.Add(1)
go func(idx int, peer adminPeer) {
defer wg.Done()
errs[idx] = peer.cmdRunner.CommitConfig(tmpFileName)
}(i, peer)
}
wg.Wait()
// Return errors (if any) received during rename.
return errs
}
-260
View File
@@ -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 serverConfigV13
err := json.Unmarshal(config1, &c1)
if err != nil {
t.Fatalf("json unmarshal of %s failed: %v", string(config1), err)
}
err = json.Unmarshal(config2, &c2)
if err != nil {
t.Fatalf("json unmarshal of %s failed: %v", string(config2), err)
}
// Valid config.
noErrs := []error{nil, nil, nil, nil}
serverConfigs := []serverConfigV13{c1, c2, c1, c1}
validConfig, err := getValidServerConfig(serverConfigs, noErrs)
if err != nil {
t.Errorf("Expected a valid config but received %v instead", err)
}
if !reflect.DeepEqual(validConfig, c1) {
t.Errorf("Expected valid config to be %v but received %v", config1, validConfig)
}
// Invalid config - no quorum.
serverConfigs = []serverConfigV13{c1, c2, c2, c1}
_, err = getValidServerConfig(serverConfigs, noErrs)
if err != errXLWriteQuorum {
t.Errorf("Expected to fail due to lack of quorum but received %v", err)
}
// All errors
allErrs := []error{errDiskNotFound, errDiskNotFound, errDiskNotFound, errDiskNotFound}
serverConfigs = []serverConfigV13{{}, {}, {}, {}}
_, err = getValidServerConfig(serverConfigs, allErrs)
if err != errXLWriteQuorum {
t.Errorf("Expected to fail due to lack of quorum but received %v", err)
}
}
+8 -134
View File
@@ -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,12 +17,8 @@
package cmd
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"net/rpc"
"time"
router "github.com/gorilla/mux"
@@ -41,9 +37,9 @@ type adminCmd struct {
// 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
relTime time.Duration
}
// ListLocksReply - wraps ListLocks response over RPC.
@@ -52,18 +48,6 @@ type ListLocksReply struct {
volLocks []VolumeLockInfo
}
// ServerInfoDataReply - wraps the server info response over RPC.
type ServerInfoDataReply struct {
AuthRPCReply
ServerInfoData ServerInfoData
}
// ConfigReply - wraps the server config response over RPC.
type ConfigReply struct {
AuthRPCReply
Config []byte // json-marshalled bytes of serverConfigV13
}
// Restart - Restart this instance of minio server.
func (s *adminCmd) Restart(args *AuthRPCArgs, reply *AuthRPCReply) error {
if err := args.IsAuthenticated(); err != nil {
@@ -79,7 +63,7 @@ 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)
volLocks := listLocksInfo(query.bucket, query.prefix, query.relTime)
*reply = ListLocksReply{volLocks: volLocks}
return nil
}
@@ -121,126 +105,16 @@ func (s *adminCmd) ReInitDisks(args *AuthRPCArgs, reply *AuthRPCReply) error {
return nil
}
// ServerInfo - returns the server info when object layer was initialized on this server.
func (s *adminCmd) ServerInfoData(args *AuthRPCArgs, reply *ServerInfoDataReply) error {
if err := args.IsAuthenticated(); err != nil {
return err
}
if globalBootTime.IsZero() {
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: serverConfig.GetRegion(),
SQSARN: arns,
},
StorageInfo: storageInfo,
ConnStats: globalConnStats.toServerConnStats(),
HTTPStats: globalHTTPStats.toServerHTTPStats(),
}
return nil
}
// GetConfig - returns the config.json of this server.
func (s *adminCmd) GetConfig(args *AuthRPCArgs, reply *ConfigReply) error {
if err := args.IsAuthenticated(); err != nil {
return err
}
if serverConfig == nil {
return errors.New("config not present")
}
jsonBytes, err := json.Marshal(serverConfig)
if err != nil {
return err
}
reply.Config = jsonBytes
return nil
}
// WriteConfigArgs - wraps the bytes to be written and temporary file name.
type WriteConfigArgs struct {
AuthRPCArgs
TmpFileName string
Buf []byte
}
// WriteConfigReply - wraps the result of a writing config into a temporary file.
// the remote node.
type WriteConfigReply struct {
AuthRPCReply
}
func writeTmpConfigCommon(tmpFileName string, configBytes []byte) error {
tmpConfigFile := filepath.Join(getConfigDir(), tmpFileName)
err := ioutil.WriteFile(tmpConfigFile, configBytes, 0666)
errorIf(err, fmt.Sprintf("Failed to write to temporary config file %s", tmpConfigFile))
return err
}
// WriteTmpConfig - writes the supplied config contents onto the
// supplied temporary file.
func (s *adminCmd) WriteTmpConfig(wArgs *WriteConfigArgs, wReply *WriteConfigReply) error {
if err := wArgs.IsAuthenticated(); err != nil {
return err
}
return writeTmpConfigCommon(wArgs.TmpFileName, wArgs.Buf)
}
// CommitConfigArgs - wraps the config file name that needs to be
// committed into config.json on this node.
type CommitConfigArgs struct {
AuthRPCArgs
FileName string
}
// CommitConfigReply - represents response to commit of config file on
// this node.
type CommitConfigReply struct {
AuthRPCReply
}
// CommitConfig - Renames the temporary file into config.json on this node.
func (s *adminCmd) CommitConfig(cArgs *CommitConfigArgs, cReply *CommitConfigReply) error {
configFile := getConfigFile()
tmpConfigFile := filepath.Join(getConfigDir(), cArgs.FileName)
err := os.Rename(tmpConfigFile, configFile)
errorIf(err, fmt.Sprintf("Failed to rename %s to %s", tmpConfigFile, configFile))
return err
}
// registerAdminRPCRouter - registers RPC methods for service status,
// stop and restart commands.
func registerAdminRPCRouter(mux *router.Router) error {
adminRPCHandler := &adminCmd{}
adminRPCServer := newRPCServer()
adminRPCServer := rpc.NewServer()
err := adminRPCServer.RegisterName("Admin", adminRPCHandler)
if err != nil {
return traceError(err)
}
adminRouter := mux.NewRoute().PathPrefix(minioReservedBucketPath).Subrouter()
adminRouter := mux.NewRoute().PathPrefix(reservedBucket).Subrouter()
adminRouter.Path(adminPath).Handler(adminRPCServer)
return nil
}
+13 -111
View File
@@ -17,8 +17,9 @@
package cmd
import (
"encoding/json"
"net/url"
"testing"
"time"
)
func testAdminCmd(cmd cmdType, t *testing.T) {
@@ -38,7 +39,7 @@ func testAdminCmd(cmd cmdType, t *testing.T) {
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: Version,
RequestTime: UTCNow(),
RequestTime: time.Now().UTC(),
}
reply := LoginRPCReply{}
err = adminServer.Login(&args, &reply)
@@ -51,7 +52,7 @@ func testAdminCmd(cmd cmdType, t *testing.T) {
<-globalServiceSignalCh
}()
ga := AuthRPCArgs{AuthToken: reply.AuthToken}
ga := AuthRPCArgs{AuthToken: reply.AuthToken, RequestTime: time.Now().UTC()}
genReply := AuthRPCReply{}
switch cmd {
case restartCmd:
@@ -85,7 +86,9 @@ func TestReInitDisks(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
@@ -95,7 +98,7 @@ func TestReInitDisks(t *testing.T) {
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: Version,
RequestTime: UTCNow(),
RequestTime: time.Now().UTC(),
}
reply := LoginRPCReply{}
err = adminServer.Login(&args, &reply)
@@ -104,7 +107,8 @@ func TestReInitDisks(t *testing.T) {
}
authArgs := AuthRPCArgs{
AuthToken: reply.AuthToken,
AuthToken: reply.AuthToken,
RequestTime: time.Now().UTC(),
}
authReply := AuthRPCReply{}
@@ -120,7 +124,7 @@ func TestReInitDisks(t *testing.T) {
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: Version,
RequestTime: UTCNow(),
RequestTime: time.Now().UTC(),
}
fsReply := LoginRPCReply{}
err = fsAdminServer.Login(&fsArgs, &fsReply)
@@ -129,7 +133,8 @@ func TestReInitDisks(t *testing.T) {
}
authArgs = AuthRPCArgs{
AuthToken: fsReply.AuthToken,
AuthToken: fsReply.AuthToken,
RequestTime: time.Now().UTC(),
}
authReply = AuthRPCReply{}
// Attempt ReInitDisks service on a FS backend.
@@ -139,106 +144,3 @@ func TestReInitDisks(t *testing.T) {
errUnsupportedBackend, err)
}
}
// TestGetConfig - Test for GetConfig admin RPC.
func TestGetConfig(t *testing.T) {
// Reset global variables to start afresh.
resetTestGlobals()
rootPath, err := newTestConfig("us-east-1")
if err != nil {
t.Fatalf("Unable to initialize server config. %s", err)
}
defer removeAll(rootPath)
adminServer := adminCmd{}
creds := serverConfig.GetCredential()
args := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: Version,
RequestTime: UTCNow(),
}
reply := LoginRPCReply{}
err = adminServer.Login(&args, &reply)
if err != nil {
t.Fatalf("Failed to login to admin server - %v", err)
}
authArgs := AuthRPCArgs{
AuthToken: reply.AuthToken,
}
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 removeAll(rootPath)
adminServer := adminCmd{}
creds := serverConfig.GetCredential()
args := LoginRPCArgs{
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: Version,
RequestTime: UTCNow(),
}
reply := LoginRPCReply{}
err = adminServer.Login(&args, &reply)
if err != nil {
t.Fatalf("Failed to login to admin server - %v", err)
}
// Write temporary config.
buf := []byte("hello")
tmpFileName := mustGetUUID()
wArgs := WriteConfigArgs{
AuthRPCArgs: AuthRPCArgs{
AuthToken: reply.AuthToken,
},
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: reply.AuthToken,
},
FileName: tmpFileName,
}
cReply := CommitConfigReply{}
err = adminServer.CommitConfig(&cArgs, &cReply)
if err != nil {
t.Fatalf("Failed to commit config file %v", err)
}
}
+4 -71
View File
@@ -56,8 +56,6 @@ const (
ErrInvalidBucketName
ErrInvalidDigest
ErrInvalidRange
ErrInvalidCopyPartRange
ErrInvalidCopyPartRangeSource
ErrInvalidMaxKeys
ErrInvalidMaxUploads
ErrInvalidMaxParts
@@ -85,7 +83,6 @@ const (
ErrInvalidPartOrder
ErrAuthorizationHeaderMalformed
ErrMalformedPOSTRequest
ErrPOSTFileRequired
ErrSignatureVersionNotSupported
ErrBucketNotEmpty
ErrAllAccessDisabled
@@ -114,8 +111,6 @@ const (
ErrInvalidQueryParams
ErrBucketAlreadyOwnedByYou
ErrInvalidDuration
ErrNotSupported
ErrBucketAlreadyExists
// Add new error codes here.
// Bucket notification related errors.
@@ -141,7 +136,6 @@ const (
ErrObjectExistsAsDirectory
ErrPolicyNesting
ErrInvalidObjectName
ErrInvalidResourceName
ErrServerNotInitialized
// Add new extended error codes here.
// Please open a https://github.com/minio/minio/issues before adding
@@ -149,8 +143,6 @@ const (
ErrAdminInvalidAccessKey
ErrAdminInvalidSecretKey
ErrAdminConfigNoQuorum
ErrInsecureClientRequest
)
// error code to APIError structure, these fields carry respective
@@ -232,7 +224,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,
},
@@ -318,7 +310,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: {
@@ -341,11 +333,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "The body of your POST request is not well-formed multipart/form-data.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrPOSTFileRequired: {
Code: "InvalidArgument",
Description: "POST requires exactly one file upload per request.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrSignatureVersionNotSupported: {
Code: "InvalidRequest",
Description: "The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256.",
@@ -356,11 +343,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.",
@@ -501,7 +483,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
},
ErrInvalidDuration: {
Code: "InvalidDuration",
Description: "Duration provided in the request is invalid.",
Description: "Relative duration provided in the request is invalid.",
HTTPStatusCode: http.StatusBadRequest,
},
@@ -551,16 +533,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
Description: "Configurations overlap. Configurations on the same bucket cannot share a common event type.",
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,
},
/// S3 extensions.
ErrContentSHA256Mismatch: {
@@ -597,12 +569,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: {
@@ -620,16 +587,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,
},
ErrInsecureClientRequest: {
Code: "XMinioInsecureClientRequest",
Description: "Cannot respond to plain-text request from TLS-encrypted server",
HTTPStatusCode: http.StatusBadRequest,
},
// Add your error structure here.
}
@@ -649,14 +606,6 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
apiErr = ErrSignatureDoesNotMatch
case errContentSHA256Mismatch:
apiErr = ErrContentSHA256Mismatch
case errDataTooLarge:
apiErr = ErrEntityTooLarge
case errDataTooSmall:
apiErr = ErrEntityTooSmall
case errInvalidAccessKeyLength:
apiErr = ErrAdminInvalidAccessKey
case errInvalidSecretKeyLength:
apiErr = ErrAdminInvalidSecretKey
}
if apiErr != ErrNone {
@@ -669,8 +618,6 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
apiErr = ErrStorageFull
case BadDigest:
apiErr = ErrBadDigest
case AllAccessDisabled:
apiErr = ErrAllAccessDisabled
case IncompleteBody:
apiErr = ErrIncompleteBody
case ObjectExistsAsDirectory:
@@ -681,12 +628,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:
@@ -711,22 +654,12 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
apiErr = ErrNoSuchUpload
case PartTooSmall:
apiErr = ErrEntityTooSmall
case SignatureDoesNotMatch:
apiErr = ErrSignatureDoesNotMatch
case SHA256Mismatch:
apiErr = ErrContentSHA256Mismatch
case ObjectTooLarge:
apiErr = ErrEntityTooLarge
case ObjectTooSmall:
apiErr = ErrEntityTooSmall
case NotSupported:
apiErr = ErrNotSupported
case NotImplemented:
apiErr = ErrNotImplemented
case PolicyNotFound:
apiErr = ErrNoSuchBucketPolicy
case PartTooBig:
apiErr = ErrEntityTooLarge
default:
apiErr = ErrInternalError
}
-8
View File
@@ -103,14 +103,6 @@ func TestAPIErrCode(t *testing.T) {
StorageFull{},
ErrStorageFull,
},
{
NotSupported{},
ErrNotSupported,
},
{
NotImplemented{},
ErrNotImplemented,
},
{
errSignatureMismatch,
ErrSignatureDoesNotMatch,
+4 -9
View File
@@ -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.
@@ -34,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 := serverConfig.GetRegion(); region != "" {
w.Header().Set("X-Amz-Bucket-Region", region)
}
w.Header().Set("Accept-Ranges", "bytes")
}
@@ -66,8 +61,8 @@ 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.MD5Sum != "" {
w.Header().Set("ETag", "\""+objInfo.MD5Sum+"\"")
}
// Set all other user defined metadata.
+3 -2
View File
@@ -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()
+25 -35
View File
@@ -166,13 +166,12 @@ type ListBucketsResponse struct {
// Upload container for in progress multipart upload
type Upload struct {
Key string
UploadID string `xml:"UploadId"`
Initiator Initiator
Owner Owner
StorageClass string
Initiated string
HealUploadInfo *HealObjectInfo `xml:"HealObjectInfo,omitempty"`
Key string
UploadID string `xml:"UploadId"`
Initiator Initiator
Owner Owner
StorageClass string
Initiated string
}
// CommonPrefix container for prefix response in ListObjectsResponse
@@ -209,13 +208,6 @@ type CopyObjectResponse struct {
ETag string // md5sum of the copied object.
}
// CopyObjectPartResponse container returns ETag and LastModified of the successfully copied object
type CopyObjectPartResponse struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CopyPartResult" json:"-"`
LastModified string // time string of format "2006-01-02T15:04:05.000Z"
ETag string // md5sum of the copied object part.
}
// Initiator inherit from Owner struct, fields are same
type Initiator Owner
@@ -288,6 +280,8 @@ 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
@@ -310,6 +304,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 == "" {
@@ -317,8 +313,8 @@ 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
@@ -348,25 +344,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
@@ -382,14 +379,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
}
@@ -402,14 +399,6 @@ func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectR
}
}
// generates CopyObjectPartResponse from etag and lastModified time.
func generateCopyObjectPartResponse(etag string, lastModified time.Time) CopyObjectPartResponse {
return CopyObjectPartResponse{
ETag: "\"" + etag + "\"",
LastModified: lastModified.UTC().Format(timeFormatAMZLong),
}
}
// generates InitiateMultipartUploadResponse for given bucket, key and uploadID.
func generateInitiateMultipartUploadResponse(bucket, key, uploadID string) InitiateMultipartUploadResponse {
return InitiateMultipartUploadResponse{
@@ -438,7 +427,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
@@ -482,7 +473,6 @@ func generateListMultipartUploadsResponse(bucket string, multipartsInfo ListMult
newUpload.UploadID = upload.UploadID
newUpload.Key = upload.Object
newUpload.Initiated = upload.Initiated.UTC().Format(timeFormatAMZLong)
newUpload.HealUploadInfo = upload.HealUploadInfo
listMultipartUploadsResponse.Uploads[index] = newUpload
}
return listMultipartUploadsResponse
-2
View File
@@ -40,8 +40,6 @@ func registerAPIRouter(mux *router.Router) {
// 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
+28 -34
View File
@@ -63,8 +63,7 @@ func isRequestPostPolicySignatureV4(r *http.Request) bool {
// Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation.
func isRequestSignStreamingV4(r *http.Request) bool {
return r.Header.Get("x-amz-content-sha256") == streamingContentSHA256 &&
r.Method == httpPUT
return r.Header.Get("x-amz-content-sha256") == streamingContentSHA256 && r.Method == httpPUT
}
// Authorization type.
@@ -113,21 +112,20 @@ func checkRequestAuthType(r *http.Request, bucket, policyAction, region string)
// Signature V2 validation.
s3Error := isReqAuthenticatedV2(r)
if s3Error != ErrNone {
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
errorIf(errSignatureMismatch, dumpRequest(r))
}
return s3Error
case authTypeSigned, authTypePresigned:
s3Error := isReqAuthenticated(r, region)
if s3Error != ErrNone {
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
errorIf(errSignatureMismatch, dumpRequest(r))
}
return s3Error
}
if reqAuthType == authTypeAnonymous && policyAction != "" {
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
return enforceBucketPolicy(bucket, policyAction, r.URL.Path,
r.Referer(), r.URL.Query())
return enforceBucketPolicy(bucket, policyAction, r.URL)
}
// By default return ErrAccessDenied
@@ -142,16 +140,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'.
@@ -159,39 +160,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.
+12 -25
View File
@@ -36,14 +36,13 @@ func TestGetRequestAuthType(t *testing.T) {
{
req: &http.Request{
URL: &url.URL{
Host: "127.0.0.1:9000",
Host: "localhost:9000",
Scheme: httpScheme,
Path: "/",
},
Header: http.Header{
"Authorization": []string{"AWS4-HMAC-SHA256 <cred_string>"},
"X-Amz-Content-Sha256": []string{streamingContentSHA256},
"Content-Encoding": []string{streamingContentEncoding},
},
Method: "PUT",
},
@@ -54,7 +53,7 @@ func TestGetRequestAuthType(t *testing.T) {
{
req: &http.Request{
URL: &url.URL{
Host: "127.0.0.1:9000",
Host: "localhost:9000",
Scheme: httpScheme,
Path: "/",
},
@@ -69,7 +68,7 @@ func TestGetRequestAuthType(t *testing.T) {
{
req: &http.Request{
URL: &url.URL{
Host: "127.0.0.1:9000",
Host: "localhost:9000",
Scheme: httpScheme,
Path: "/",
},
@@ -84,7 +83,7 @@ func TestGetRequestAuthType(t *testing.T) {
{
req: &http.Request{
URL: &url.URL{
Host: "127.0.0.1:9000",
Host: "localhost:9000",
Scheme: httpScheme,
Path: "/",
RawQuery: "X-Amz-Credential=EXAMPLEINVALIDEXAMPL%2Fs3%2F20160314%2Fus-east-1",
@@ -97,7 +96,7 @@ func TestGetRequestAuthType(t *testing.T) {
{
req: &http.Request{
URL: &url.URL{
Host: "127.0.0.1:9000",
Host: "localhost:9000",
Scheme: httpScheme,
Path: "/",
},
@@ -308,16 +307,6 @@ func mustNewSignedRequest(method string, urlStr string, contentLength int64, bod
return req
}
func mustNewSignedBadMD5Request(method string, urlStr string, contentLength int64, body io.ReadSeeker, t *testing.T) *http.Request {
req := mustNewRequest(method, urlStr, contentLength, body, t)
req.Header.Set("Content-Md5", "YWFhYWFhYWFhYWFhYWFhCg==")
cred := serverConfig.GetCredential()
if err := signRequestV4(req, cred.AccessKey, cred.SecretKey); err != nil {
t.Fatalf("Unable to initialized new signed http request %s", err)
}
return req
}
// Tests is requested authenticated function, tests replies for s3 errors.
func TestIsReqAuthenticated(t *testing.T) {
path, err := newTestConfig(globalMinioDefaultRegion)
@@ -326,12 +315,7 @@ func TestIsReqAuthenticated(t *testing.T) {
}
defer removeAll(path)
creds, err := createCredential("myuser", "mypassword")
if err != nil {
t.Fatalf("unable create credential, %s", err)
}
serverConfig.SetCredential(creds)
serverConfig.SetCredential(credential{"myuser", "mypassword"})
// List of test cases for validating http request authentication.
testCases := []struct {
@@ -341,15 +325,18 @@ func TestIsReqAuthenticated(t *testing.T) {
// When request is nil, internal error is returned.
{nil, ErrInternalError},
// When request is unsigned, access denied is returned.
{mustNewRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrAccessDenied},
{mustNewRequest("GET", "http://localhost: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://localhost: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},
{mustNewSignedRequest("GET", "http://localhost:9000", 0, nil, t), ErrNone},
}
// Validates all testcases.
for _, testCase := range testCases {
if testCase.s3Error == ErrBadDigest {
testCase.req.Header.Set("Content-Md5", "garbage")
}
if s3Error := isReqAuthenticated(testCase.req, serverConfig.GetRegion()); s3Error != testCase.s3Error {
t.Fatalf("Unexpected s3error returned wanted %d, got %d", testCase.s3Error, s3Error)
}
+40 -78
View File
@@ -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.
@@ -35,117 +35,79 @@ type authConfig struct {
secureConn bool // Make TLS connection to RPC server or not.
serviceName string // Service name of auth server.
disableReconnect bool // Disable reconnect on failure or not.
/// Retry configurable values.
// Each retry unit multiplicative, measured in time.Duration.
// This is the basic unit used for calculating backoffs.
retryUnit time.Duration
// Maximum retry duration i.e A caller would wait no more than this
// duration to continue their loop.
retryCap time.Duration
// Maximum retries an call authRPC client would do for a failed
// RPC call.
retryAttemptThreshold int
}
// AuthRPCClient is a authenticated RPC client which does authentication before doing Call().
type AuthRPCClient struct {
sync.RWMutex // Mutex to lock this object.
rpcClient *RPCClient // Reconnectable RPC client to make any RPC call.
config authConfig // Authentication configuration information.
authToken string // Authentication token.
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.
func newAuthRPCClient(config authConfig) *AuthRPCClient {
// Check if retry params are set properly if not default them.
emptyDuration := time.Duration(int64(0))
if config.retryUnit == emptyDuration {
config.retryUnit = defaultRetryUnit
}
if config.retryCap == emptyDuration {
config.retryCap = defaultRetryCap
}
if config.retryAttemptThreshold == 0 {
config.retryAttemptThreshold = globalAuthRPCRetryThreshold
}
return &AuthRPCClient{
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 == "" {
// Login to authenticate and acquire a new auth token.
var (
loginMethod = authClient.config.serviceName + loginMethodName
loginArgs = LoginRPCArgs{
Username: authClient.config.accessKey,
Password: authClient.config.secretKey,
Version: Version,
RequestTime: UTCNow(),
}
loginReply = LoginRPCReply{}
)
if err = authClient.rpcClient.Call(loginMethod, &loginArgs, &loginReply); err != nil {
return err
}
authClient.authToken = loginReply.AuthToken
// 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)
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())
authClient.RLock()
// Set token before the rpc call.
args.SetAuthToken(authClient.authToken)
authClient.RUnlock()
// Do an RPC call.
return authClient.rpcClient.Call(serviceMethod, args, reply)
// 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)
SetRequestTime(requestTime time.Time)
}, reply interface{}) (err error) {
// Done channel is used to close any lingering retry routine, as soon
// as this function returns.
doneCh := make(chan struct{})
defer close(doneCh)
for i := range newRetryTimer(authClient.config.retryUnit, authClient.config.retryCap, doneCh) {
for i := range newRetryTimer(time.Second, 30*time.Second, MaxJitter, doneCh) {
if err = authClient.call(serviceMethod, args, reply); err == rpc.ErrShutdown {
// As connection at server side is closed, close the rpc client.
authClient.Close()
@@ -153,7 +115,7 @@ func (authClient *AuthRPCClient) Call(serviceMethod string, args interface {
// Retry if reconnect is not disabled.
if !authClient.config.disableReconnect {
// Retry until threshold reaches.
if i < authClient.config.retryAttemptThreshold {
if i < globalAuthRPCRetryThreshold {
continue
}
}
+1 -1
View File
@@ -77,7 +77,7 @@ func TestLogin(t *testing.T) {
// Invalid password length
{
args: LoginRPCArgs{
Username: "minio",
Username: globalMinioDefaultOwnerID,
Password: "aaa",
Version: Version,
},
+47 -31
View File
@@ -23,13 +23,30 @@ import (
"math/rand"
"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().
@@ -39,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)
}
@@ -49,7 +66,7 @@ func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
// generate md5sum for the generated data.
// md5sum of the data to written is required as input for PutObject.
metadata := make(map[string]string)
metadata["etag"] = getMD5Hash(textData)
metadata["md5Sum"] = getMD5Hash(textData)
sha256sum := ""
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
b.ReportAllocs()
@@ -61,8 +78,8 @@ func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
if err != nil {
b.Fatal(err)
}
if objInfo.ETag != metadata["etag"] {
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, metadata["etag"])
if objInfo.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.
@@ -78,22 +95,22 @@ 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["etag"] = getMD5Hash(textData)
metadata["md5Sum"] = getMD5Hash(textData)
sha256sum := ""
uploadID, err = obj.NewMultipartUpload(bucket, object, metadata)
if err != nil {
@@ -115,14 +132,13 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
textPartData = textData[j*partSize:]
}
metadata := make(map[string]string)
metadata["etag"] = getMD5Hash([]byte(textPartData))
var partInfo PartInfo
partInfo, err = obj.PutObjectPart(bucket, object, uploadID, j, int64(len(textPartData)), bytes.NewBuffer(textPartData), metadata["etag"], sha256sum)
metadata["md5Sum"] = getMD5Hash([]byte(textPartData))
md5Sum, 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 != metadata["etag"] {
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, etag, metadata["etag"])
if md5Sum != metadata["md5Sum"] {
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, md5Sum, metadata["md5Sum"])
}
}
}
@@ -199,7 +215,7 @@ func runGetObjectBenchmark(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)
}
@@ -208,19 +224,19 @@ func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
for i := 0; i < 10; i++ {
// get text data generated for number of bytes equal to object size.
textData := generateBytesData(objSize)
// generate etag for the generated data.
// etag of the data to written is required as input for PutObject.
// generate md5sum for the generated data.
// md5sum of the data to written is required as input for PutObject.
// PutObject is the functions which writes the data onto the FS/XL backend.
metadata := make(map[string]string)
metadata["etag"] = getMD5Hash(textData)
metadata["md5Sum"] = getMD5Hash(textData)
// insert the object.
var objInfo ObjectInfo
objInfo, err = obj.PutObject(bucket, "object"+strconv.Itoa(i), int64(len(textData)), bytes.NewBuffer(textData), metadata, sha256sum)
if err != nil {
b.Fatal(err)
}
if objInfo.ETag != metadata["etag"] {
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, metadata["etag"])
if objInfo.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"])
}
}
@@ -244,7 +260,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))]
@@ -307,7 +323,7 @@ func runPutObjectBenchmarkParallel(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)
}
@@ -317,7 +333,7 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
// generate md5sum for the generated data.
// md5sum of the data to written is required as input for PutObject.
metadata := make(map[string]string)
metadata["etag"] = getMD5Hash([]byte(textData))
metadata["md5Sum"] = getMD5Hash([]byte(textData))
sha256sum := ""
// benchmark utility which helps obtain number of allocations and bytes allocated per ops.
b.ReportAllocs()
@@ -332,8 +348,8 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
if err != nil {
b.Fatal(err)
}
if objInfo.ETag != metadata["etag"] {
b.Fatalf("Write no: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", objInfo.ETag, metadata["etag"])
if objInfo.MD5Sum != metadata["md5Sum"] {
b.Fatalf("Write no: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", objInfo.MD5Sum, metadata["md5Sum"])
}
i++
}
@@ -355,7 +371,7 @@ func runGetObjectBenchmarkParallel(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)
}
@@ -367,7 +383,7 @@ func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
// md5sum of the data to written is required as input for PutObject.
// PutObject is the functions which writes the data onto the FS/XL backend.
metadata := make(map[string]string)
metadata["etag"] = getMD5Hash([]byte(textData))
metadata["md5Sum"] = getMD5Hash([]byte(textData))
sha256sum := ""
// insert the object.
var objInfo ObjectInfo
@@ -375,8 +391,8 @@ func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
if err != nil {
b.Fatal(err)
}
if objInfo.ETag != metadata["etag"] {
b.Fatalf("Write no: %d: Md5Sum mismatch during object write into the bucket: Expected %s, got %s", i+1, objInfo.ETag, metadata["etag"])
if objInfo.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"])
}
}
-68
View File
@@ -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
}
-137
View File
@@ -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)
}
}
}
+2 -7
View File
@@ -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,7 +17,6 @@
package cmd
import (
"fmt"
"path"
"sync"
"time"
@@ -64,10 +63,6 @@ func (br *browserPeerAPIHandlers) SetAuthPeer(args SetAuthPeerArgs, reply *AuthR
return err
}
if !args.Creds.IsValid() {
return fmt.Errorf("Invalid credential passed")
}
// Update credentials in memory
serverConfig.SetCredential(args.Creds)
@@ -112,7 +107,7 @@ func updateCredsOnPeers(creds credential) map[string]error {
secretKey: serverCred.SecretKey,
serverAddr: peers[ix],
secureConn: globalIsSSL,
serviceEndpoint: path.Join(minioReservedBucketPath, browserPeerPath),
serviceEndpoint: path.Join(reservedBucket, browserPeerPath),
serviceName: "BrowserPeer",
})
+9 -8
View File
@@ -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,6 +19,7 @@ package cmd
import (
"path"
"testing"
"time"
)
// API suite container common to both FS and XL.
@@ -35,7 +36,7 @@ func (s *TestRPCBrowserPeerSuite) SetUpSuite(c *testing.T) {
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",
}
}
@@ -62,9 +63,9 @@ func TestBrowserPeerRPC(t *testing.T) {
// Tests for browser peer rpc.
func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
// Construct RPC call arguments.
creds, err := createCredential("abcd1", "abcd1234")
if err != nil {
t.Fatalf("unable to create credential. %v", err)
creds := credential{
AccessKey: "abcd1",
SecretKey: "abcd1234",
}
// Validate for invalid token.
@@ -72,7 +73,7 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
args.AuthToken = "garbage"
rclient := newRPCClient(s.testAuthConf.serverAddr, s.testAuthConf.serviceEndpoint, false)
defer rclient.Close()
err = rclient.Call("BrowserPeer.SetAuthPeer", &args, &AuthRPCReply{})
err := rclient.Call("BrowserPeer.SetAuthPeer", &args, &AuthRPCReply{})
if err != nil {
if err.Error() != errInvalidToken.Error() {
t.Fatal(err)
@@ -95,7 +96,7 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: Version,
RequestTime: UTCNow(),
RequestTime: time.Now().UTC(),
}
rreply := &LoginRPCReply{}
err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply)
@@ -110,7 +111,7 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
Username: creds.AccessKey,
Password: creds.SecretKey,
Version: Version,
RequestTime: UTCNow(),
RequestTime: time.Now().UTC(),
}
rreply = &LoginRPCReply{}
err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply)
+4 -2
View File
@@ -17,6 +17,8 @@
package cmd
import (
"net/rpc"
router "github.com/gorilla/mux"
)
@@ -37,13 +39,13 @@ type browserPeerAPIHandlers struct {
func registerBrowserPeerRPCRouter(mux *router.Router) error {
bpHandlers := &browserPeerAPIHandlers{}
bpRPCServer := newRPCServer()
bpRPCServer := rpc.NewServer()
err := bpRPCServer.RegisterName("BrowserPeer", bpHandlers)
if err != nil {
return traceError(err)
}
bpRouter := mux.NewRoute().PathPrefix(minioReservedBucketPath).Subrouter()
bpRouter := mux.NewRoute().PathPrefix(reservedBucket).Subrouter()
bpRouter.Path(browserPeerPath).Handler(bpRPCServer)
return nil
}
+3 -2
View File
@@ -18,6 +18,7 @@ package cmd
import (
"net/http"
"strings"
"github.com/gorilla/mux"
)
@@ -43,7 +44,7 @@ func validateListObjectsArgs(prefix, marker, delimiter string, maxKeys int) APIE
// Marker is set validate pre-condition.
if marker != "" {
// Marker not common with prefix is not implemented.
if !hasPrefix(marker, prefix) {
if !strings.HasPrefix(marker, prefix) {
return ErrNotImplemented
}
}
@@ -100,7 +101,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
return
}
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsInfo.NextMarker, startAfter, delimiter, fetchOwner, listObjectsInfo.IsTruncated, maxKeys, listObjectsInfo.Objects, listObjectsInfo.Prefixes)
response := generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter, fetchOwner, maxKeys, listObjectsInfo)
// Write success response.
writeSuccessResponseXML(w, encodeResponse(response))
+77 -157
View File
@@ -19,8 +19,8 @@ package cmd
import (
"encoding/base64"
"encoding/xml"
"fmt"
"io"
"net"
"net/http"
"net/url"
"path"
@@ -33,7 +33,7 @@ import (
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
// Enforces bucket policies for a bucket for a given tatusaction.
func enforceBucketPolicy(bucket, action, resource, referer string, queryParams url.Values) (s3Error APIErrorCode) {
func enforceBucketPolicy(bucket string, action string, reqURL *url.URL) (s3Error APIErrorCode) {
// Verify if bucket actually exists
if err := checkBucketExist(bucket, newObjectLayerFn()); err != nil {
err = errorCause(err)
@@ -50,10 +50,6 @@ func enforceBucketPolicy(bucket, action, resource, referer string, queryParams u
return ErrInternalError
}
if globalBucketPolicies == nil {
return ErrAccessDenied
}
// Fetch bucket policy, if policy is not set return access denied.
policy := globalBucketPolicies.GetBucketPolicy(bucket)
if policy == nil {
@@ -61,21 +57,16 @@ func enforceBucketPolicy(bucket, action, resource, referer string, queryParams u
}
// Construct resource in 'arn:aws:s3:::examplebucket/object' format.
arn := bucketARNPrefix + strings.TrimSuffix(strings.TrimPrefix(resource, "/"), "/")
resource := bucketARNPrefix + strings.TrimSuffix(strings.TrimPrefix(reqURL.Path, "/"), "/")
// Get conditions for policy verification.
conditionKeyMap := make(map[string]set.StringSet)
for queryParam := range queryParams {
conditionKeyMap[queryParam] = set.CreateStringSet(queryParams.Get(queryParam))
}
// Add request referer to conditionKeyMap if present.
if referer != "" {
conditionKeyMap["referer"] = set.CreateStringSet(referer)
for queryParam := range reqURL.Query() {
conditionKeyMap[queryParam] = set.CreateStringSet(reqURL.Query().Get(queryParam))
}
// Validate action, resource and conditions with current policy statements.
if !bucketPolicyEvalStatements(action, arn, conditionKeyMap, policy.Statements) {
if !bucketPolicyEvalStatements(action, resource, conditionKeyMap, policy.Statements) {
return ErrAccessDenied
}
return ErrNone
@@ -83,10 +74,6 @@ func enforceBucketPolicy(bucket, action, resource, referer string, queryParams u
// Check if the action is allowed on the bucket/prefix.
func isBucketActionAllowed(action, bucket, prefix string) bool {
if globalBucketPolicies == nil {
return false
}
policy := globalBucketPolicies.GetBucketPolicy(bucket)
if policy == nil {
return false
@@ -94,7 +81,10 @@ func isBucketActionAllowed(action, bucket, prefix string) bool {
resource := bucketARNPrefix + path.Join(bucket, prefix)
var conditionKeyMap map[string]set.StringSet
// Validate action, resource and conditions with current policy statements.
return bucketPolicyEvalStatements(action, resource, conditionKeyMap, policy.Statements)
if !bucketPolicyEvalStatements(action, resource, conditionKeyMap, policy.Statements) {
return false
}
return true
}
// GetBucketLocationHandler - GET Bucket location.
@@ -170,7 +160,7 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter,
}
if keyMarker != "" {
// Marker not common with prefix is not implemented.
if !hasPrefix(keyMarker, prefix) {
if !strings.HasPrefix(keyMarker, prefix) {
writeErrorResponse(w, ErrNotImplemented, r.URL)
return
}
@@ -282,11 +272,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
for index, object := range deleteObjects.Objects {
wg.Add(1)
go func(i int, obj ObjectIdentifier) {
objectLock := globalNSMutex.NewNSLock(bucket, obj.ObjectName)
objectLock.Lock()
defer objectLock.Unlock()
defer wg.Done()
dErr := objectAPI.DeleteObject(bucket, obj.ObjectName)
if dErr != nil {
dErrs[i] = dErr
@@ -327,13 +313,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{
@@ -342,10 +321,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,
},
})
}
}
@@ -374,17 +352,10 @@ 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 {
writeErrorResponse(w, s3Error, r.URL)
return
}
// Validate if location sent by the client is valid, reject
// Validate if incoming location constraint is valid, reject
// requests which do not follow valid region requirements.
if !isValidLocation(location) {
writeErrorResponse(w, ErrInvalidRegion, r.URL)
if s3Error := isValidLocationConstraint(r); s3Error != ErrNone {
writeErrorResponse(w, s3Error, r.URL)
return
}
@@ -393,7 +364,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
defer bucketLock.Unlock()
// Proceed to creating a bucket.
err := objectAPI.MakeBucketWithLocation(bucket, "")
err := objectAPI.MakeBucket(bucket)
if err != nil {
errorIf(err, "Unable to create a bucket.")
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
@@ -417,13 +388,6 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return
}
// Require Content-Length to be set in the request
size := r.ContentLength
if size < 0 {
writeErrorResponse(w, ErrMissingContentLength, 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.
reader, err := r.MultipartReader()
@@ -433,54 +397,21 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return
}
// Read multipart data and save in memory and in the disk if needed
form, err := reader.ReadForm(maxFormMemory)
if err != nil {
errorIf(err, "Unable to initialize multipart reader.")
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL)
return
}
// Remove all tmp files creating during multipart upload
defer form.RemoveAll()
// Extract all form fields
fileBody, fileName, fileSize, formValues, err := extractPostPolicyFormValues(form)
fileBody, fileName, formValues, err := extractPostPolicyFormValues(reader)
if err != nil {
errorIf(err, "Unable to parse form values.")
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL)
return
}
// Check if file is provided, error out otherwise.
if fileBody == nil {
writeErrorResponse(w, ErrPOSTFileRequired, r.URL)
return
}
// Close multipart file
defer fileBody.Close()
bucket := mux.Vars(r)["bucket"]
formValues.Set("Bucket", 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)
@@ -489,7 +420,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
@@ -507,85 +438,83 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return
}
// Ensure that the object size is within expected range, also the file size
// should not exceed the maximum single Put size (5 GiB)
// Use rangeReader to ensure that object size is within expected range.
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 policy restricted the size of the object.
fileBody = &rangeReader{
Reader: fileBody,
Min: lengthRange.Min,
Max: lengthRange.Max,
}
if fileSize > lengthRange.Max || isMaxObjectSize(fileSize) {
errorIf(err, "Unable to create object.")
writeErrorResponse(w, toAPIErrorCode(errDataTooLarge), r.URL)
return
} else {
// Default values of min/max size of the object.
fileBody = &rangeReader{
Reader: fileBody,
Min: 0,
Max: maxObjectSize,
}
}
// 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)
sha256sum := ""
objectLock := globalNSMutex.NewNSLock(bucket, object)
objectLock.Lock()
defer objectLock.Unlock()
objInfo, err := objectAPI.PutObject(bucket, object, fileSize, fileBody, metadata, sha256sum)
objInfo, err := objectAPI.PutObject(bucket, object, -1, fileBody, metadata, sha256sum)
if err != nil {
errorIf(err, "Unable to create object.")
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
return
}
w.Header().Set("ETag", `"`+objInfo.ETag+`"`)
w.Header().Set("ETag", "\""+objInfo.MD5Sum+"\"")
w.Header().Set("Location", getObjectLocation(bucket, object))
// Get host and port from Request.RemoteAddr.
host, port, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
host, port = "", ""
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)
}
}
}
// 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: getObjectLocation(objInfo.Bucket, objInfo.Name),
})
writeResponse(w, http.StatusCreated, resp, "application/xml")
case "200":
writeSuccessResponseHeadersOnly(w)
default:
writeSuccessNoContent(w)
}
}
// HeadBucketHandler - HEAD Bucket
@@ -653,21 +582,12 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
// Delete bucket access policy, if present - ignore any errors.
_ = removeBucketPolicy(bucket, objectAPI)
// Notify all peers (including self) to update in-memory state
S3PeersUpdateBucketPolicy(bucket, policyChange{true, nil})
// Delete notification config, if present - ignore any errors.
_ = removeNotificationConfig(bucket, objectAPI)
// Notify all peers (including self) to update in-memory state
S3PeersUpdateBucketNotification(bucket, nil)
// Delete listener config, if present - ignore any errors.
_ = removeListenerConfig(bucket, objectAPI)
// Notify all peers (including self) to update in-memory state
S3PeersUpdateBucketListener(bucket, []listenerConfig{})
// Write success response.
writeSuccessNoContent(w)
}
+1 -34
View File
@@ -68,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,
@@ -771,36 +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 credential, t *testing.T) {
testCases := []struct {
// input.
action string
bucket string
prefix string
isGlobalPoliciesNil bool
// flag indicating whether the test should pass.
shouldPass bool
}{
{"s3:GetBucketLocation", "mybucket", "abc", true, false},
{"s3:ListObject", "mybucket", "abc", false, false},
}
for i, testCase := range testCases {
if testCase.isGlobalPoliciesNil {
globalBucketPolicies = nil
} else {
initBucketPolicies(obj)
}
isAllowed := isBucketActionAllowed(testCase.action, testCase.bucket, testCase.prefix)
if isAllowed != testCase.shouldPass {
t.Errorf("Case %d: Expected the response status to be `%t`, but instead found `%t`", i+1, testCase.shouldPass, isAllowed)
}
}
}
+5 -24
View File
@@ -94,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.
@@ -113,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"
}
@@ -136,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"`
UserDefined map[string]string `json:"userDefined,omitempty"`
VersionID string `json:"versionId,omitempty"`
Sequencer string `json:"sequencer"`
Key string `json:"key"`
Size int64 `json:"size,omitempty"`
ETag string `json:"eTag,omitempty"`
VersionID string `json:"versionId,omitempty"`
Sequencer string `json:"sequencer"`
}
const (
@@ -178,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"`
@@ -197,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.
+6 -12
View File
@@ -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.
@@ -204,21 +204,15 @@ func writeNotification(w http.ResponseWriter, notification map[string][]Notifica
if err != nil {
return err
}
// 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")
// 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.
@@ -294,7 +288,7 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
return
}
accountID := fmt.Sprintf("%d", UTCNow().UnixNano())
accountID := fmt.Sprintf("%d", time.Now().UTC().UnixNano())
accountARN := fmt.Sprintf(
"%s:%s:%s:%s-%s",
minioTopic,
+1 -2
View File
@@ -185,7 +185,7 @@ func testGetBucketNotificationHandler(obj ObjectLayer, instanceType, bucketName
filterRules := []filterRule{
{
Name: "prefix",
Value: "minio",
Value: globalMinioDefaultOwnerID,
},
{
Name: "suffix",
@@ -268,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)
+38 -68
View File
@@ -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,21 +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
}
if serverConfig.GetRegion() != "" {
region := strs[3]
if region != serverConfig.GetRegion() {
return ErrRegionNotification
account := strings.SplitN(strings.TrimPrefix(arn, arnType+serverConfig.GetRegion()+":"), ":", 2)
switch len(account) {
case 1:
// This means ARN is malformed, account should have min of 2elements.
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
}
@@ -142,34 +133,27 @@ func isValidQueueID(queueARN string) bool {
// Is Queue identifier valid?.
if isAMQPQueue(sqsARN) { // AMQP eueue.
amqpN := serverConfig.Notify.GetAMQPByID(sqsARN.AccountID)
amqpN := serverConfig.GetAMQPNotifyByID(sqsARN.AccountID)
return amqpN.Enable && amqpN.URL != ""
} else if isMQTTQueue(sqsARN) {
mqttN := serverConfig.Notify.GetMQTTByID(sqsARN.AccountID)
return mqttN.Enable && mqttN.Broker != ""
} else if isNATSQueue(sqsARN) {
natsN := serverConfig.Notify.GetNATSByID(sqsARN.AccountID)
natsN := serverConfig.GetNATSNotifyByID(sqsARN.AccountID)
return natsN.Enable && natsN.Address != ""
} else if isElasticQueue(sqsARN) { // Elastic queue.
elasticN := serverConfig.Notify.GetElasticSearchByID(sqsARN.AccountID)
elasticN := serverConfig.GetElasticSearchNotifyByID(sqsARN.AccountID)
return elasticN.Enable && elasticN.URL != ""
} else if isRedisQueue(sqsARN) { // Redis queue.
redisN := serverConfig.Notify.GetRedisByID(sqsARN.AccountID)
redisN := serverConfig.GetRedisNotifyByID(sqsARN.AccountID)
return redisN.Enable && redisN.Addr != ""
} else if isPostgreSQLQueue(sqsARN) {
pgN := serverConfig.Notify.GetPostgreSQLByID(sqsARN.AccountID)
pgN := serverConfig.GetPostgreSQLNotifyByID(sqsARN.AccountID)
// Postgres can work with only default conn. info.
return pgN.Enable
} else if isMySQLQueue(sqsARN) {
msqlN := serverConfig.Notify.GetMySQLByID(sqsARN.AccountID)
// Mysql can work with only default conn. info.
return msqlN.Enable
} else if isKafkaQueue(sqsARN) {
kafkaN := serverConfig.Notify.GetKafkaByID(sqsARN.AccountID)
kafkaN := serverConfig.GetKafkaNotifyByID(sqsARN.AccountID)
return (kafkaN.Enable && len(kafkaN.Brokers) > 0 &&
kafkaN.Topic != "")
} else if isWebhookQueue(sqsARN) {
webhookN := serverConfig.Notify.GetWebhookByID(sqsARN.AccountID)
webhookN := serverConfig.GetWebhookNotifyByID(sqsARN.AccountID)
return webhookN.Enable && webhookN.Endpoint != ""
}
return false
@@ -216,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
}
@@ -254,50 +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
}
if serverConfig.GetRegion() != "" {
region := strs[3]
if region != serverConfig.GetRegion() {
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
}
+10 -65
View File
@@ -259,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:",
@@ -293,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 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.
@@ -358,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:",
@@ -387,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 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)
}
}
}
+14 -65
View File
@@ -21,8 +21,6 @@ import (
"io"
"io/ioutil"
"net/http"
"runtime"
"strings"
humanize "github.com/dustin/go-humanize"
mux "github.com/gorilla/mux"
@@ -65,10 +63,6 @@ func bucketPolicyActionMatch(action string, statement policyStatement) bool {
// Match function matches wild cards in 'pattern' for resource.
func resourceMatch(pattern, resource string) bool {
if runtime.GOOS == "windows" {
// For windows specifically make sure we are case insensitive.
return wildcard.Match(strings.ToLower(pattern), strings.ToLower(resource))
}
return wildcard.Match(pattern, resource)
}
@@ -77,10 +71,6 @@ func actionMatch(pattern, action string) bool {
return wildcard.MatchSimple(pattern, action)
}
func refererMatch(pattern, referer string) bool {
return wildcard.MatchSimple(pattern, referer)
}
// Verify if given resource matches with policy statement.
func bucketPolicyResourceMatch(resource string, statement policyStatement) bool {
// the resource rule for object could contain "*" wild card.
@@ -95,74 +85,33 @@ func bucketPolicyConditionMatch(conditions map[string]set.StringSet, statement p
// Supports following conditions.
// - StringEquals
// - StringNotEquals
// - StringLike
// - StringNotLike
//
// Supported applicable condition keys for each conditions.
// - s3:prefix
// - s3:max-keys
// - s3:aws-Referer
// The following loop evaluates the logical AND of all the
// conditions in the statement. Note: we can break out of the
// loop if and only if a condition evaluates to false.
var conditionMatches = true
for condition, conditionKeyVal := range statement.Conditions {
prefixConditon := conditionKeyVal["s3:prefix"]
maxKeyCondition := conditionKeyVal["s3:max-keys"]
if condition == "StringEquals" {
// If there is no condition with "s3:prefix" or "s3:max-keys" condition key
// then there is nothing to check condition against.
if !prefixConditon.IsEmpty() && !prefixConditon.Equals(conditions["prefix"]) {
return false
if !conditionKeyVal["s3:prefix"].Equals(conditions["prefix"]) {
conditionMatches = false
break
}
if !maxKeyCondition.IsEmpty() && !maxKeyCondition.Equals(conditions["max-keys"]) {
return false
if !conditionKeyVal["s3:max-keys"].Equals(conditions["max-keys"]) {
conditionMatches = false
break
}
} else if condition == "StringNotEquals" {
// If there is no condition with "s3:prefix" or "s3:max-keys" condition key
// then there is nothing to check condition against.
if !prefixConditon.IsEmpty() && prefixConditon.Equals(conditions["prefix"]) {
return false
if !conditionKeyVal["s3:prefix"].Equals(conditions["prefix"]) {
conditionMatches = false
break
}
if !maxKeyCondition.IsEmpty() && maxKeyCondition.Equals(conditions["max-keys"]) {
return false
}
} else if condition == "StringLike" {
awsReferers := conditionKeyVal["aws:Referer"]
// Skip empty condition, it is trivially satisfied.
if awsReferers.IsEmpty() {
continue
}
// wildcard match of referer in statement was not empty.
// StringLike has a match, i.e, condition evaluates to true.
refererFound := false
for referer := range conditions["referer"] {
if !awsReferers.FuncMatch(refererMatch, referer).IsEmpty() {
refererFound = true
break
}
}
// No matching referer found, so the condition is false.
if !refererFound {
return false
}
} else if condition == "StringNotLike" {
awsReferers := conditionKeyVal["aws:Referer"]
// Skip empty condition, it is trivially satisfied.
if awsReferers.IsEmpty() {
continue
}
// wildcard match of referer in statement was not empty.
// StringNotLike has a match, i.e, condition evaluates to false.
for referer := range conditions["referer"] {
if !awsReferers.FuncMatch(refererMatch, referer).IsEmpty() {
return false
}
if !conditionKeyVal["s3:max-keys"].Equals(conditions["max-keys"]) {
conditionMatches = false
break
}
}
}
return true
return conditionMatches
}
// PutBucketPolicyHandler - PUT Bucket policy
+6 -35
View File
@@ -251,7 +251,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
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)
}
@@ -913,7 +913,7 @@ func TestBucketPolicyConditionMatch(t *testing.T) {
statementCondition: getStatementWithCondition("StringNotEquals", "s3:prefix", "Asia/"),
condition: getInnerMap("prefix", "Asia/"),
expectedMatch: false,
expectedMatch: true,
},
// Test case - 6.
// StringNotEquals condition doesn't match.
@@ -922,7 +922,7 @@ func TestBucketPolicyConditionMatch(t *testing.T) {
statementCondition: getStatementWithCondition("StringNotEquals", "s3:prefix", "Asia/"),
condition: getInnerMap("prefix", "Africa/"),
expectedMatch: true,
expectedMatch: false,
},
// Test case - 7.
// StringNotEquals condition matches.
@@ -931,7 +931,7 @@ func TestBucketPolicyConditionMatch(t *testing.T) {
statementCondition: getStatementWithCondition("StringNotEquals", "s3:max-keys", "Asia/"),
condition: getInnerMap("max-keys", "Asia/"),
expectedMatch: false,
expectedMatch: true,
},
// Test case - 8.
// StringNotEquals condition doesn't match.
@@ -940,35 +940,7 @@ func TestBucketPolicyConditionMatch(t *testing.T) {
statementCondition: getStatementWithCondition("StringNotEquals", "s3:max-keys", "Asia/"),
condition: getInnerMap("max-keys", "Africa/"),
expectedMatch: true,
},
// Test case - 9.
// StringLike condition matches.
{
statementCondition: getStatementWithCondition("StringLike", "aws:Referer", "http://www.example.com/"),
condition: getInnerMap("referer", "http://www.example.com/"),
expectedMatch: true,
},
// Test case - 10.
// StringLike condition doesn't match.
{
statementCondition: getStatementWithCondition("StringLike", "aws:Referer", "http://www.example.com/"),
condition: getInnerMap("referer", "www.somethingelse.com"),
expectedMatch: false,
},
// Test case - 11.
// StringNotLike condition evaluates to false.
{
statementCondition: getStatementWithCondition("StringNotLike", "aws:Referer", "http://www.example.com/"),
condition: getInnerMap("referer", "http://www.example.com/"),
expectedMatch: false,
},
// Test case - 12.
// StringNotLike condition evaluates to true.
{
statementCondition: getStatementWithCondition("StringNotLike", "aws:Referer", "http://www.example.com/"),
condition: getInnerMap("referer", "http://somethingelse.com/"),
expectedMatch: true,
expectedMatch: false,
},
}
@@ -977,8 +949,7 @@ func TestBucketPolicyConditionMatch(t *testing.T) {
// call the function under test and assert the result with the expected result.
doesMatch := bucketPolicyConditionMatch(tc.condition, tc.statementCondition)
if tc.expectedMatch != doesMatch {
t.Errorf("Expected the match to be `%v`; got `%v` - %v %v.",
tc.expectedMatch, doesMatch, tc.condition, tc.statementCondition)
t.Errorf("Expected the match to be `%v`; got `%v`.", tc.expectedMatch, doesMatch)
}
})
}
+13 -28
View File
@@ -29,22 +29,17 @@ import (
"github.com/minio/minio-go/pkg/set"
)
var conditionKeyActionMap = map[string]set.StringSet{
"s3:prefix": set.CreateStringSet("s3:ListBucket"),
"s3:max-keys": set.CreateStringSet("s3:ListBucket"),
}
// supportedActionMap - lists all the actions supported by minio.
var supportedActionMap = set.CreateStringSet("*", "s3:*", "s3:GetObject",
"s3:ListBucket", "s3:PutObject", "s3:GetBucketLocation", "s3:DeleteObject",
"s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts")
// supported Conditions type.
var supportedConditionsType = set.CreateStringSet("StringEquals", "StringNotEquals", "StringLike", "StringNotLike")
var supportedConditionsType = set.CreateStringSet("StringEquals", "StringNotEquals")
// Validate s3:prefix, s3:max-keys are present if not
// supported keys for the conditions.
var supportedConditionsKey = set.CreateStringSet("s3:prefix", "s3:max-keys", "aws:Referer")
var supportedConditionsKey = set.CreateStringSet("s3:prefix", "s3:max-keys")
// supportedEffectMap - supported effects.
var supportedEffectMap = set.CreateStringSet("Allow", "Deny")
@@ -111,12 +106,12 @@ func isValidResources(resources set.StringSet) (err error) {
return err
}
for resource := range resources {
if !hasPrefix(resource, bucketARNPrefix) {
if !strings.HasPrefix(resource, bucketARNPrefix) {
err = errors.New("Unsupported resource style found: ‘" + resource + "’, please validate your policy document")
return err
}
resourceSuffix := strings.SplitAfter(resource, bucketARNPrefix)[1]
if len(resourceSuffix) == 0 || hasPrefix(resourceSuffix, "/") {
if len(resourceSuffix) == 0 || strings.HasPrefix(resourceSuffix, "/") {
err = errors.New("Invalid resource style found: ‘" + resource + "’, please validate your policy document")
return err
}
@@ -183,12 +178,11 @@ func isValidPrincipals(principal interface{}) (err error) {
return nil
}
// isValidConditions - returns nil if the given conditions valid and
// corresponding error otherwise.
func isValidConditions(actions set.StringSet, conditions map[string]map[string]set.StringSet) (err error) {
// Verify conditions should be valid. Validate if only
// supported condition keys are present and return error
// otherwise.
// isValidConditions - are valid conditions.
func isValidConditions(conditions map[string]map[string]set.StringSet) (err error) {
// Verify conditions should be valid.
// Validate if stringEquals, stringNotEquals are present
// if not throw an error.
conditionKeyVal := make(map[string]set.StringSet)
for conditionType := range conditions {
if !supportedConditionsType.Contains(conditionType) {
@@ -200,15 +194,6 @@ func isValidConditions(actions set.StringSet, conditions map[string]map[string]s
err = fmt.Errorf("Unsupported condition key '%s', please validate your policy document", conditionType)
return err
}
compatibleActions := conditionKeyActionMap[key]
if !compatibleActions.IsEmpty() &&
compatibleActions.Intersection(actions).IsEmpty() {
err = fmt.Errorf("Unsupported condition key %s for the given actions %s, "+
"please validate your policy document", key, actions)
return err
}
conditionVal, ok := conditionKeyVal[key]
if ok && !value.Intersection(conditionVal).IsEmpty() {
err = fmt.Errorf("Ambigious condition values for key '%s', please validate your policy document", key)
@@ -237,8 +222,8 @@ func resourcePrefix(resource string) string {
}
// checkBucketPolicyResources validates Resources in unmarshalled bucket policy structure.
// - Resources are validated against the given set of Actions.
// -
// First valation of Resources done for given set of Actions.
// Later its validated for recursive Resources.
func checkBucketPolicyResources(bucket string, bucketPolicy *bucketPolicy) APIErrorCode {
// Validate statements for special actions and collect resources
// for others to validate nesting.
@@ -282,7 +267,7 @@ func checkBucketPolicyResources(bucket string, bucketPolicy *bucketPolicy) APIEr
// nesting. Reject such rules.
for _, otherResource := range resources {
// Common prefix reject such rules.
if hasPrefix(otherResource, resource) {
if strings.HasPrefix(otherResource, resource) {
return ErrPolicyNesting
}
}
@@ -332,7 +317,7 @@ func parseBucketPolicy(bucketPolicyReader io.Reader, policy *bucketPolicy) (err
return err
}
// Statement conditions should be valid.
if err := isValidConditions(statement.Actions, statement.Conditions); err != nil {
if err := isValidConditions(statement.Conditions); err != nil {
return err
}
}
+31 -109
View File
@@ -22,7 +22,6 @@ import (
"fmt"
"testing"
"github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio-go/pkg/set"
)
@@ -343,18 +342,6 @@ func TestIsValidPrincipals(t *testing.T) {
}
}
// getEmptyConditionKeyMap - returns a function that generates a
// condition key map for a given key.
func getEmptyConditionKeyMap(conditionKey string) func() map[string]map[string]set.StringSet {
emptyConditonGenerator := func() map[string]map[string]set.StringSet {
emptyMap := make(map[string]set.StringSet)
conditions := make(map[string]map[string]set.StringSet)
conditions[conditionKey] = emptyMap
return conditions
}
return emptyConditonGenerator
}
// Tests validate policyStatement condition validator.
func TestIsValidConditions(t *testing.T) {
// returns empty conditions map.
@@ -363,17 +350,22 @@ func TestIsValidConditions(t *testing.T) {
}
// returns map with the "StringEquals" set to empty map.
setEmptyStringEquals := getEmptyConditionKeyMap("StringEquals")
setEmptyStringEquals := func() map[string]map[string]set.StringSet {
emptyMap := make(map[string]set.StringSet)
conditions := make(map[string]map[string]set.StringSet)
conditions["StringEquals"] = emptyMap
return conditions
}
// returns map with the "StringNotEquals" set to empty map.
setEmptyStringNotEquals := getEmptyConditionKeyMap("StringNotEquals")
// returns map with the "StringLike" set to empty map.
setEmptyStringLike := getEmptyConditionKeyMap("StringLike")
// returns map with the "StringNotLike" set to empty map.
setEmptyStringNotLike := getEmptyConditionKeyMap("StringNotLike")
setEmptyStringNotEquals := func() map[string]map[string]set.StringSet {
emptyMap := make(map[string]set.StringSet)
conditions := make(map[string]map[string]set.StringSet)
conditions["StringNotEquals"] = emptyMap
return conditions
}
// Generate conditions.
generateConditions := func(key1, key2, value string) map[string]map[string]set.StringSet {
innerMap := make(map[string]set.StringSet)
@@ -385,11 +377,11 @@ func TestIsValidConditions(t *testing.T) {
// generate ambigious conditions.
generateAmbigiousConditions := func() map[string]map[string]set.StringSet {
prefixMap := make(map[string]set.StringSet)
prefixMap["s3:prefix"] = set.CreateStringSet("Asia/")
innerMap := make(map[string]set.StringSet)
innerMap["s3:prefix"] = set.CreateStringSet("Asia/")
conditions := make(map[string]map[string]set.StringSet)
conditions["StringEquals"] = prefixMap
conditions["StringNotEquals"] = prefixMap
conditions["StringEquals"] = innerMap
conditions["StringNotEquals"] = innerMap
return conditions
}
@@ -425,20 +417,13 @@ func TestIsValidConditions(t *testing.T) {
setEmptyConditions(),
setEmptyStringEquals(),
setEmptyStringNotEquals(),
setEmptyStringLike(),
setEmptyStringNotLike(),
generateConditions("StringEquals", "s3:prefix", "Asia/"),
generateConditions("StringEquals", "s3:max-keys", "100"),
generateConditions("StringNotEquals", "s3:prefix", "Asia/"),
generateConditions("StringNotEquals", "s3:max-keys", "100"),
}
getObjectActionSet := set.CreateStringSet("s3:GetObject")
roBucketActionSet := set.CreateStringSet(readOnlyBucketActions...)
maxKeysConditionErr := fmt.Errorf("Unsupported condition key %s for the given actions %s, "+
"please validate your policy document", "s3:max-keys", getObjectActionSet)
testCases := []struct {
inputActions set.StringSet
inputCondition map[string]map[string]set.StringSet
// expected result.
expectedErr error
@@ -448,44 +433,42 @@ func TestIsValidConditions(t *testing.T) {
// Malformed conditions.
// Test case - 1.
// "StringValues" is an invalid type.
{roBucketActionSet, testConditions[0], fmt.Errorf("Unsupported condition type 'StringValues', " +
{testConditions[0], fmt.Errorf("Unsupported condition type 'StringValues', " +
"please validate your policy document"), false},
// Test case - 2.
// "s3:Object" is an invalid key.
{roBucketActionSet, testConditions[1], fmt.Errorf("Unsupported condition key " +
{testConditions[1], fmt.Errorf("Unsupported condition key " +
"'StringEquals', please validate your policy document"), false},
// Test case - 3.
// Test case with Ambigious conditions set.
{roBucketActionSet, testConditions[2], fmt.Errorf("Ambigious condition values for key 's3:prefix', " +
{testConditions[2], fmt.Errorf("Ambigious condition values for key 's3:prefix', " +
"please validate your policy document"), false},
// Test case - 4.
// Test case with valid and invalid condition types.
{roBucketActionSet, testConditions[3], fmt.Errorf("Unsupported condition type 'InvalidType', " +
{testConditions[3], fmt.Errorf("Unsupported condition type 'InvalidType', " +
"please validate your policy document"), false},
// Test case - 5.
// Test case with valid and invalid condition keys.
{roBucketActionSet, testConditions[4], fmt.Errorf("Unsupported condition key 'StringEquals', " +
{testConditions[4], fmt.Errorf("Unsupported condition key 'StringEquals', " +
"please validate your policy document"), false},
// Test cases with valid conditions.
// Test case - 6.
{roBucketActionSet, testConditions[5], nil, true},
{testConditions[5], nil, true},
// Test case - 7.
{roBucketActionSet, testConditions[6], nil, true},
{testConditions[6], nil, true},
// Test case - 8.
{roBucketActionSet, testConditions[7], nil, true},
{testConditions[7], nil, true},
// Test case - 9.
{roBucketActionSet, testConditions[8], nil, true},
{testConditions[8], nil, true},
// Test case - 10.
{roBucketActionSet, testConditions[9], nil, true},
{testConditions[9], nil, true},
// Test case - 11.
{roBucketActionSet, testConditions[10], nil, true},
// Test case - 12.
{roBucketActionSet, testConditions[11], nil, true},
// Test case - 13.
{getObjectActionSet, testConditions[11], maxKeysConditionErr, false},
{testConditions[10], nil, true},
// Test case 10.
{testConditions[11], nil, true},
}
for i, testCase := range testCases {
actualErr := isValidConditions(testCase.inputActions, testCase.inputCondition)
actualErr := isValidConditions(testCase.inputCondition)
if actualErr != nil && testCase.shouldPass {
t.Errorf("Test %d: Expected to pass, but failed with: <ERROR> %s", i+1, actualErr.Error())
}
@@ -726,64 +709,3 @@ func TestParseBucketPolicy(t *testing.T) {
}
}
}
func TestAWSRefererCondition(t *testing.T) {
resource := set.CreateStringSet([]string{
fmt.Sprintf("%s%s", bucketARNPrefix, "minio-bucket"+"/"+"Asia"+"*"),
}...)
conditionsKeyMap := make(policy.ConditionKeyMap)
conditionsKeyMap.Add("aws:Referer",
set.CreateStringSet("www.example.com",
"http://www.example.com"))
requestConditionKeyMap := make(map[string]set.StringSet)
requestConditionKeyMap["referer"] = set.CreateStringSet("www.example.com")
testCases := []struct {
effect string
conditionKey string
match bool
}{
{
effect: "Allow",
conditionKey: "StringLike",
match: true,
},
{
effect: "Allow",
conditionKey: "StringNotLike",
match: false,
},
{
effect: "Deny",
conditionKey: "StringLike",
match: true,
},
{
effect: "Deny",
conditionKey: "StringNotLike",
match: false,
},
}
for i, test := range testCases {
conditions := make(map[string]map[string]set.StringSet)
conditions[test.conditionKey] = conditionsKeyMap
allowStatement := policyStatement{
Sid: "Testing AWS referer condition",
Effect: test.effect,
Principal: map[string]interface{}{
"AWS": "*",
},
Resources: resource,
Conditions: conditions,
}
if result := bucketPolicyConditionMatch(requestConditionKeyMap, allowStatement); result != test.match {
t.Errorf("Test %d - Expected conditons to evaluate to %v but got %v",
i+1, test.match, result)
}
}
}
+125 -74
View File
@@ -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.
@@ -17,99 +17,150 @@
package cmd
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"errors"
"io/ioutil"
"os"
"path/filepath"
)
func parsePublicCertFile(certFile string) (x509Certs []*x509.Certificate, err error) {
// Read certificate file.
var data []byte
if data, err = ioutil.ReadFile(certFile); err != nil {
return nil, err
// createCertsPath create certs path.
func createCertsPath() error {
certsPath, err := getCertsPath()
if err != nil {
return err
}
// Parse all certs in the chain.
current := data
for len(current) > 0 {
var pemBlock *pem.Block
if pemBlock, current = pem.Decode(current); pemBlock == nil {
return nil, fmt.Errorf("Could not read PEM block from file %s", certFile)
}
var x509Cert *x509.Certificate
if x509Cert, err = x509.ParseCertificate(pemBlock.Bytes); err != nil {
return nil, err
}
x509Certs = append(x509Certs, x509Cert)
if err := os.MkdirAll(certsPath, 0700); err != nil {
return err
}
if len(x509Certs) == 0 {
return nil, fmt.Errorf("Empty public certificate file %s", certFile)
}
return x509Certs, nil
rootCAsPath := filepath.Join(certsPath, globalMinioCertsCADir)
return os.MkdirAll(rootCAsPath, 0700)
}
func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
// Get all CA file names.
var caFiles []string
fis, err := ioutil.ReadDir(certsCAsDir)
// getCertsPath get certs path.
func getCertsPath() (string, error) {
var certsPath string
configDir, err := getConfigPath()
if err != nil {
return "", err
}
certsPath = filepath.Join(configDir, globalMinioCertsDir)
return certsPath, nil
}
// mustGetCertsPath must get certs path.
func mustGetCertsPath() string {
certsPath, err := getCertsPath()
fatalIf(err, "Failed to get certificate path.")
return certsPath
}
// mustGetCertFile must get cert file.
func mustGetCertFile() string {
return filepath.Join(mustGetCertsPath(), globalMinioCertFile)
}
// mustGetKeyFile must get key file.
func mustGetKeyFile() string {
return filepath.Join(mustGetCertsPath(), globalMinioKeyFile)
}
// mustGetCAFiles must get the list of the CA certificates stored in minio config dir
func mustGetCAFiles() (caCerts []string) {
CAsDir := filepath.Join(mustGetCertsPath(), globalMinioCertsCADir)
caFiles, _ := ioutil.ReadDir(CAsDir)
for _, cert := range caFiles {
caCerts = append(caCerts, filepath.Join(CAsDir, cert.Name()))
}
return
}
// mustGetSystemCertPool returns empty cert pool in case of error (windows)
func mustGetSystemCertPool() *x509.CertPool {
pool, err := x509.SystemCertPool()
if err != nil {
return x509.NewCertPool()
}
return pool
}
// isCertFileExists verifies if cert file exists, returns true if
// found, false otherwise.
func isCertFileExists() bool {
st, e := os.Stat(filepath.Join(mustGetCertsPath(), globalMinioCertFile))
// If file exists and is regular return true.
if e == nil && st.Mode().IsRegular() {
return true
}
return false
}
// isKeyFileExists verifies if key file exists, returns true if found,
// false otherwise.
func isKeyFileExists() bool {
st, e := os.Stat(filepath.Join(mustGetCertsPath(), globalMinioKeyFile))
// If file exists and is regular return true.
if e == nil && st.Mode().IsRegular() {
return true
}
return false
}
// isSSL - returns true with both cert and key exists.
func isSSL() bool {
return isCertFileExists() && isKeyFileExists()
}
// Reads certificated file and returns a list of parsed certificates.
func readCertificateChain() ([]*x509.Certificate, error) {
bytes, err := ioutil.ReadFile(mustGetCertFile())
if err != nil {
return nil, err
}
for _, fi := range fis {
caFiles = append(caFiles, filepath.Join(certsCAsDir, fi.Name()))
}
// Proceed to parse the certificates.
return parseCertificateChain(bytes)
}
// Parses certificate chain, returns a list of parsed certificates.
func parseCertificateChain(bytes []byte) ([]*x509.Certificate, error) {
var certs []*x509.Certificate
var block *pem.Block
current := bytes
// Parse all certs in the chain.
for len(current) > 0 {
block, current = pem.Decode(current)
if block == nil {
return nil, errors.New("Could not PEM block")
}
// Parse the decoded certificate.
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
certs = append(certs, cert)
}
return certs, nil
}
// loadRootCAs fetches CA files provided in minio config and adds them to globalRootCAs
// Currently under Windows, there is no way to load system + user CAs at the same time
func loadRootCAs() {
caFiles := mustGetCAFiles()
if len(caFiles) == 0 {
return nil, nil
return
}
rootCAs, err := x509.SystemCertPool()
if err != nil {
// In some systems like Windows, system cert pool is not supported.
// Hence we create a new cert pool.
rootCAs = x509.NewCertPool()
}
// Get system cert pool, and empty cert pool under Windows because it is not supported
globalRootCAs = mustGetSystemCertPool()
// Load custom root CAs for client requests
for _, caFile := range caFiles {
caCert, err := ioutil.ReadFile(caFile)
if err != nil {
return nil, err
fatalIf(err, "Unable to load a CA file")
}
rootCAs.AppendCertsFromPEM(caCert)
globalRootCAs.AppendCertsFromPEM(caCert)
}
return rootCAs, nil
}
func getSSLConfig() (x509Certs []*x509.Certificate, rootCAs *x509.CertPool, tlsCert *tls.Certificate, secureConn bool, err error) {
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
return nil, nil, nil, false, nil
}
if x509Certs, err = parsePublicCertFile(getPublicCertFile()); err != nil {
return nil, nil, nil, false, err
}
var cert tls.Certificate
if cert, err = tls.LoadX509KeyPair(getPublicCertFile(), getPrivateKeyFile()); err != nil {
return nil, nil, nil, false, err
}
tlsCert = &cert
if rootCAs, err = getRootCAs(getCADir()); err != nil {
return nil, nil, nil, false, err
}
secureConn = true
return x509Certs, rootCAs, tlsCert, secureConn, nil
}
+54 -202
View File
@@ -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.
@@ -17,86 +17,52 @@
package cmd
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func createTempFile(prefix, content string) (tempFile string, err error) {
var tmpfile *os.File
if tmpfile, err = ioutil.TempFile("", prefix); err != nil {
return tempFile, err
// Make sure we have a valid certs path.
func TestGetCertsPath(t *testing.T) {
path, err := getCertsPath()
if err != nil {
t.Error(err)
}
if path == "" {
t.Errorf("expected path to not be an empty string, got: '%s'", path)
}
// Ensure it contains some sort of path separator.
if !strings.ContainsRune(path, os.PathSeparator) {
t.Errorf("expected path to contain file separator")
}
// It should also be an absolute path.
if !filepath.IsAbs(path) {
t.Errorf("expected path to be an absolute path")
}
if _, err = tmpfile.Write([]byte(content)); err != nil {
return tempFile, err
}
if err = tmpfile.Close(); err != nil {
return tempFile, err
}
tempFile = tmpfile.Name()
return tempFile, err
// This will error if something goes wrong, so just call it.
mustGetCertsPath()
}
func TestParsePublicCertFile(t *testing.T) {
tempFile1, err := createTempFile("public-cert-file", "")
if err != nil {
t.Fatalf("Unable to create temporary file. %v", err)
// Ensure that the certificate and key file getters contain their respective
// file name and endings.
func TestGetFiles(t *testing.T) {
file := mustGetCertFile()
if !strings.Contains(file, globalMinioCertFile) {
t.Errorf("CertFile does not contain %s", globalMinioCertFile)
}
defer os.Remove(tempFile1)
tempFile2, err := createTempFile("public-cert-file",
`-----BEGIN CERTIFICATE-----
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
AQkBFg50ZXN0c0BtaW5pby5pbzAeFw0xNjEwMTQxMTM0MjJaFw0xNzEwMTQxMTM0
MjJaMH8xCzAJBgNVBAYTAlpZMQ4wDAYDVQQIEwVNaW5pbzERMA8GA1UEBxMISW50
ZXJuZXQxDjAMBgNVBA-some-junk-Q4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF
TWluaW8xHTAbBgkqhkiG9w0BCQEWDnRlc3RzQG1pbmlvLmlvMIGfMA0GCSqGSIb3
DQEBAQUAA4GNADCBiQKBgQDwNUYB/Sj79WsUE8qnXzzh2glSzWxUE79sCOpQYK83
HWkrl5WxlG8ZxDR1IQV9Ex/lzigJu8G+KXahon6a+3n5GhNrYRe5kIXHQHz0qvv4
aMulqlnYpvSfC83aaO9GVBtwXS/O4Nykd7QBg4nZlazVmsGk7POOjhpjGShRsqpU
JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
M9ofSEt/bdRD
-----END CERTIFICATE-----`)
if err != nil {
t.Fatalf("Unable to create temporary file. %v", err)
file = mustGetKeyFile()
if !strings.Contains(file, globalMinioKeyFile) {
t.Errorf("KeyFile does not contain %s", globalMinioKeyFile)
}
defer os.Remove(tempFile2)
}
tempFile3, err := createTempFile("public-cert-file",
`-----BEGIN CERTIFICATE-----
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
AQkBFg50ZXN0c0BtaW5pby5pbzAeFw0xNjEwMTQxMTM0MjJaFw0xNzEwMTQxMTM0
MjJaMH8xCzAJBgNVBAYTAlpZMQ4wDAYDVQQIEwVNaW5pbzERMA8GA1UEBxMISW50
ZXJuZXQxDjAMBgNVBAabababababaQ4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF
TWluaW8xHTAbBgkqhkiG9w0BCQEWDnRlc3RzQG1pbmlvLmlvMIGfMA0GCSqGSIb3
DQEBAQUAA4GNADCBiQKBgQDwNUYB/Sj79WsUE8qnXzzh2glSzWxUE79sCOpQYK83
HWkrl5WxlG8ZxDR1IQV9Ex/lzigJu8G+KXahon6a+3n5GhNrYRe5kIXHQHz0qvv4
aMulqlnYpvSfC83aaO9GVBtwXS/O4Nykd7QBg4nZlazVmsGk7POOjhpjGShRsqpU
JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
M9ofSEt/bdRD
-----END CERTIFICATE-----`)
if err != nil {
t.Fatalf("Unable to create temporary file. %v", err)
}
defer os.Remove(tempFile3)
tempFile4, err := createTempFile("public-cert-file",
`-----BEGIN CERTIFICATE-----
// Parses .crt file contents
func TestParseCertificateChain(t *testing.T) {
// given
cert := `-----BEGIN CERTIFICATE-----
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
@@ -111,149 +77,35 @@ JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
M9ofSEt/bdRD
-----END CERTIFICATE-----`)
-----END CERTIFICATE-----`
// when
certs, err := parseCertificateChain([]byte(cert))
// then
if err != nil {
t.Fatalf("Unable to create temporary file. %v", err)
}
defer os.Remove(tempFile4)
tempFile5, err := createTempFile("public-cert-file",
`-----BEGIN CERTIFICATE-----
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
AQkBFg50ZXN0c0BtaW5pby5pbzAeFw0xNjEwMTQxMTM0MjJaFw0xNzEwMTQxMTM0
MjJaMH8xCzAJBgNVBAYTAlpZMQ4wDAYDVQQIEwVNaW5pbzERMA8GA1UEBxMISW50
ZXJuZXQxDjAMBgNVBAoTBU1pbmlvMQ4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF
TWluaW8xHTAbBgkqhkiG9w0BCQEWDnRlc3RzQG1pbmlvLmlvMIGfMA0GCSqGSIb3
DQEBAQUAA4GNADCBiQKBgQDwNUYB/Sj79WsUE8qnXzzh2glSzWxUE79sCOpQYK83
HWkrl5WxlG8ZxDR1IQV9Ex/lzigJu8G+KXahon6a+3n5GhNrYRe5kIXHQHz0qvv4
aMulqlnYpvSfC83aaO9GVBtwXS/O4Nykd7QBg4nZlazVmsGk7POOjhpjGShRsqpU
JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
M9ofSEt/bdRD
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICdTCCAd4CCQCO5G/W1xcE9TANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJa
WTEOMAwGA1UECBMFTWluaW8xETAPBgNVBAcTCEludGVybmV0MQ4wDAYDVQQKEwVN
aW5pbzEOMAwGA1UECxMFTWluaW8xDjAMBgNVBAMTBU1pbmlvMR0wGwYJKoZIhvcN
AQkBFg50ZXN0c0BtaW5pby5pbzAeFw0xNjEwMTQxMTM0MjJaFw0xNzEwMTQxMTM0
MjJaMH8xCzAJBgNVBAYTAlpZMQ4wDAYDVQQIEwVNaW5pbzERMA8GA1UEBxMISW50
ZXJuZXQxDjAMBgNVBAoTBU1pbmlvMQ4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF
TWluaW8xHTAbBgkqhkiG9w0BCQEWDnRlc3RzQG1pbmlvLmlvMIGfMA0GCSqGSIb3
DQEBAQUAA4GNADCBiQKBgQDwNUYB/Sj79WsUE8qnXzzh2glSzWxUE79sCOpQYK83
HWkrl5WxlG8ZxDR1IQV9Ex/lzigJu8G+KXahon6a+3n5GhNrYRe5kIXHQHz0qvv4
aMulqlnYpvSfC83aaO9GVBtwXS/O4Nykd7QBg4nZlazVmsGk7POOjhpjGShRsqpU
JwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALqjOA6bD8BEl7hkQ8XwX/owSAL0URDe
nUfCOsXgIIAqgw4uTCLOfCJVZNKmRT+KguvPAQ6Z80vau2UxPX5Q2Q+OHXDRrEnK
FjqSBgLP06Qw7a++bshlWGTt5bHWOneW3EQikedckVuIKPkOCib9yGi4VmBBjdFE
M9ofSEt/bdRD
-----END CERTIFICATE-----`)
if err != nil {
t.Fatalf("Unable to create temporary file. %v", err)
}
defer os.Remove(tempFile5)
nonexistentErr := fmt.Errorf("open nonexistent-file: no such file or directory")
if runtime.GOOS == "windows" {
// Below concatenation is done to get rid of goline error
// "error strings should not be capitalized or end with punctuation or a newline"
nonexistentErr = fmt.Errorf("open nonexistent-file:" + " The system cannot find the file specified.")
t.Fatalf("Could not parse certificate: %s", err)
}
testCases := []struct {
certFile string
expectedResultLen int
expectedErr error
}{
{"nonexistent-file", 0, nonexistentErr},
{tempFile1, 0, fmt.Errorf("Empty public certificate file %s", tempFile1)},
{tempFile2, 0, fmt.Errorf("Could not read PEM block from file %s", tempFile2)},
{tempFile3, 0, fmt.Errorf("asn1: structure error: sequence tag mismatch")},
{tempFile4, 1, nil},
{tempFile5, 2, nil},
if len(certs) != 1 {
t.Fatalf("Expected number of certificates in chain was 1, actual: %d", len(certs))
}
for _, testCase := range testCases {
certs, err := parsePublicCertFile(testCase.certFile)
if testCase.expectedErr == nil {
if err != nil {
t.Fatalf("error: expected = <nil>, got = %v", err)
}
} else if err == nil {
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
} else if testCase.expectedErr.Error() != err.Error() {
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
}
if len(certs) != testCase.expectedResultLen {
t.Fatalf("certs: expected = %v, got = %v", testCase.expectedResultLen, len(certs))
}
if certs[0].Subject.CommonName != "Minio" {
t.Fatalf("Expected Subject.CommonName was Minio, actual: %s", certs[0].Subject.CommonName)
}
}
func TestGetRootCAs(t *testing.T) {
emptydir, err := ioutil.TempDir("", "test-get-root-cas")
if err != nil {
t.Fatalf("Unable create temp directory. %v", emptydir)
}
defer os.RemoveAll(emptydir)
// Parses invalid .crt file contents and returns error
func TestParseInvalidCertificateChain(t *testing.T) {
// given
cert := `This is now valid certificate`
dir1, err := ioutil.TempDir("", "test-get-root-cas")
if err != nil {
t.Fatalf("Unable create temp directory. %v", dir1)
}
defer os.RemoveAll(dir1)
if err = os.Mkdir(filepath.Join(dir1, "empty-dir"), 0755); err != nil {
t.Fatalf("Unable create empty dir. %v", err)
}
// when
_, err := parseCertificateChain([]byte(cert))
dir2, err := ioutil.TempDir("", "test-get-root-cas")
if err != nil {
t.Fatalf("Unable create temp directory. %v", dir2)
}
defer os.RemoveAll(dir2)
if err = ioutil.WriteFile(filepath.Join(dir2, "empty-file"), []byte{}, 0644); err != nil {
t.Fatalf("Unable create test file. %v", err)
}
nonexistentErr := fmt.Errorf("open nonexistent-dir: no such file or directory")
if runtime.GOOS == "windows" {
// Below concatenation is done to get rid of goline error
// "error strings should not be capitalized or end with punctuation or a newline"
nonexistentErr = fmt.Errorf("open nonexistent-dir:" + " The system cannot find the file specified.")
}
err1 := fmt.Errorf("read %s: is a directory", filepath.Join(dir1, "empty-dir"))
if runtime.GOOS == "windows" {
// Below concatenation is done to get rid of goline error
// "error strings should not be capitalized or end with punctuation or a newline"
err1 = fmt.Errorf("read %s:"+" The handle is invalid.", filepath.Join(dir1, "empty-dir"))
}
testCases := []struct {
certCAsDir string
expectedErr error
}{
{"nonexistent-dir", nonexistentErr},
{dir1, err1},
{emptydir, nil},
{dir2, nil},
}
for _, testCase := range testCases {
_, err := getRootCAs(testCase.certCAsDir)
if testCase.expectedErr == nil {
if err != nil {
t.Fatalf("error: expected = <nil>, got = %v", err)
}
} else if err == nil {
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
} else if testCase.expectedErr.Error() != err.Error() {
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
}
// then
if err == nil {
t.Fatalf("Expected error but none occurred")
}
}
+76
View File
@@ -0,0 +1,76 @@
/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"net"
"os"
"syscall"
)
// Make sure that none of the other processes are listening on the
// specified port on any of the interfaces.
//
// On linux if a process is listening on 127.0.0.1:9000 then Listen()
// on ":9000" fails with the error "port already in use".
// However on Mac OSX Listen() on ":9000" falls back to the IPv6 address.
// This causes confusion on Mac OSX that minio server is not reachable
// on 127.0.0.1 even though minio server is running. So before we start
// the minio server we make sure that the port is free on each tcp network.
//
// Port is string on purpose here.
// https://github.com/golang/go/issues/16142#issuecomment-245912773
//
// "Keep in mind that ports in Go are strings: https://play.golang.org/p/zk2WEri_E9"
// - @bradfitz
func checkPortAvailability(portStr string) error {
network := [3]string{"tcp", "tcp4", "tcp6"}
for _, n := range network {
l, err := net.Listen(n, net.JoinHostPort("", portStr))
if err != nil {
if isAddrInUse(err) {
// Return error if another process is listening on the
// same port.
return err
}
// Ignore any other error (ex. EAFNOSUPPORT)
continue
}
// look for error so we don't have dangling connection
if err = l.Close(); err != nil {
return err
}
}
return nil
}
// Return true if err is "address already in use" error.
// syscall.EADDRINUSE is available on all OSes.
func isAddrInUse(err error) bool {
if opErr, ok := err.(*net.OpError); ok {
if sysErr, ok := opErr.Err.(*os.SyscallError); ok {
if errno, ok := sysErr.Err.(syscall.Errno); ok {
if errno == syscall.EADDRINUSE {
return true
}
}
}
}
return false
}
+54
View File
@@ -0,0 +1,54 @@
/*
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"net"
"runtime"
"testing"
)
// Tests for port availability logic written for server startup sequence.
func TestCheckPortAvailability(t *testing.T) {
tests := []struct {
port string
}{
{getFreePort()},
{getFreePort()},
}
for _, test := range tests {
// This test should pass if the ports are available
err := checkPortAvailability(test.port)
if err != nil {
t.Fatalf("checkPortAvailability test failed for port: %s. Error: %v", test.port, err)
}
// Now use the ports and check again
ln, err := net.Listen("tcp", net.JoinHostPort("", test.port))
if err != nil {
t.Fail()
}
defer ln.Close()
err = checkPortAvailability(test.port)
// Skip if the os is windows due to https://github.com/golang/go/issues/7598
if err == nil && runtime.GOOS != globalWindowsOSName {
t.Fatalf("checkPortAvailability should fail for port: %s. Error: %v", test.port, err)
}
}
}
+13 -10
View File
@@ -1,7 +1,5 @@
// +build !windows
/*
* Minio Cloud Storage, (C) 2017 Minio, Inc.
* Minio Cloud Storage, (C) 2015 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,13 +14,18 @@
* limitations under the License.
*/
package os
package cmd
import os1 "os"
import "github.com/minio/cli"
// Stat returns a FileInfo structure describing the
// named file. If there is an error, it will be of type
// *PathError.
func Stat(name string) (os1.FileInfo, error) {
return os1.Stat(name)
// Collection of minio commands currently supported are.
var commands = []cli.Command{}
// Collection of minio commands currently supported in a trie tree.
var commandsTree = newTrie()
// registerCommand registers a cli command.
func registerCommand(command cli.Command) {
commands = append(commands, command)
commandsTree.Insert(command.Name)
}
@@ -15,3 +15,27 @@
*/
package cmd
import (
"testing"
"github.com/minio/cli"
)
// Tests register command function.
func TestRegisterCommand(t *testing.T) {
registerCommand(cli.Command{
Name: "test1",
})
ccount := len(commands)
if ccount != 1 {
t.Fatalf("Unexpected number of commands found %d", ccount)
}
registerCommand(cli.Command{
Name: "test2",
})
ccount = len(commands)
if ccount != 2 {
t.Fatalf("Unexpected number of commands found %d", ccount)
}
}
-117
View File
@@ -1,117 +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"
"os"
"path/filepath"
"strings"
"time"
"github.com/minio/cli"
)
// Check for updates and print a notification message
func checkUpdate(mode string) {
// Its OK to ignore any errors during getUpdateInfo() here.
if older, downloadURL, err := getUpdateInfo(1*time.Second, mode); err == nil {
if updateMsg := computeUpdateMessage(downloadURL, older); updateMsg != "" {
log.Println(updateMsg)
}
}
}
func enableLoggers() {
fileLogTarget := serverConfig.Logger.GetFile()
if fileLogTarget.Enable {
err := InitFileLogger(&fileLogTarget)
fatalIf(err, "Unable to initialize file logger")
log.AddTarget(fileLogTarget)
}
consoleLogTarget := serverConfig.Logger.GetConsole()
if consoleLogTarget.Enable {
InitConsoleLogger(&consoleLogTarget)
}
log.SetConsoleTarget(consoleLogTarget)
}
func initConfig() {
// Config file does not exist, we create it fresh and return upon success.
if isFile(getConfigFile()) {
fatalIf(migrateConfig(), "Config migration failed.")
fatalIf(loadConfig(), "Unable to load config version: '%s'.", v19)
} else {
fatalIf(newConfig(), "Unable to initialize minio config for the first time.")
log.Println("Created minio configuration file successfully at " + getConfigDir())
}
}
func handleCommonCmdArgs(ctx *cli.Context) {
// Set configuration directory.
{
// Get configuration directory from command line argument.
configDir := ctx.String("config-dir")
if !ctx.IsSet("config-dir") && ctx.GlobalIsSet("config-dir") {
configDir = ctx.GlobalString("config-dir")
}
if configDir == "" {
fatalIf(errors.New("empty directory"), "Configuration directory cannot be empty.")
}
// Disallow relative paths, figure out absolute paths.
configDirAbs, err := filepath.Abs(configDir)
fatalIf(err, "Unable to fetch absolute path for config directory %s", configDir)
setConfigDir(configDirAbs)
}
}
func handleCommonEnvVars() {
// Start profiler if env is set.
if profiler := os.Getenv("_MINIO_PROFILER"); profiler != "" {
globalProfiler = startProfiler(profiler)
}
// Check if object cache is disabled.
globalXLObjCacheDisabled = strings.EqualFold(os.Getenv("_MINIO_CACHE"), "off")
accessKey := os.Getenv("MINIO_ACCESS_KEY")
secretKey := os.Getenv("MINIO_SECRET_KEY")
if accessKey != "" && secretKey != "" {
cred, err := createCredential(accessKey, secretKey)
fatalIf(err, "Invalid access/secret Key set in environment.")
// credential Envs are set globally.
globalIsEnvCreds = true
globalActiveCred = cred
}
if browser := os.Getenv("MINIO_BROWSER"); browser != "" {
browserFlag, err := ParseBrowserFlag(browser)
if err != nil {
fatalIf(errors.New("invalid value"), "Unknown value ‘%s’ in MINIO_BROWSER environment variable.", browser)
}
// browser Envs are set globally, this does not represent
// if browser is turned off or on.
globalIsEnvBrowser = true
globalIsBrowserEnabled = bool(browserFlag)
}
}

Some files were not shown because too many files have changed in this diff Show More