mirror of
https://github.com/pgsty/minio.git
synced 2026-07-20 20:50:22 +03:00
Merge branch 'master' into release
This commit is contained in:
+8
-5
@@ -1,23 +1,26 @@
|
|||||||
FROM alpine:3.5
|
FROM alpine:3.5
|
||||||
|
|
||||||
MAINTAINER Minio Inc <dev@minio.io>
|
MAINTAINER Minio Inc <dev@minio.io>
|
||||||
|
|
||||||
|
COPY buildscripts/docker-entrypoint.sh buildscripts/healthcheck.sh /usr/bin/
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
apk add --no-cache ca-certificates && \
|
apk add --no-cache ca-certificates && \
|
||||||
apk add --no-cache --virtual .build-deps curl && \
|
apk add --no-cache --virtual .build-deps curl && \
|
||||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
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 && \
|
curl https://dl.minio.io/server/minio/release/linux-amd64/minio > /usr/bin/minio && \
|
||||||
chmod +x /usr/bin/minio && apk del .build-deps
|
chmod +x /usr/bin/minio && \
|
||||||
|
chmod +x /usr/bin/docker-entrypoint.sh && \
|
||||||
|
chmod +x /usr/bin/healthcheck.sh
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
|
||||||
COPY buildscripts/docker-entrypoint.sh /usr/bin/
|
|
||||||
|
|
||||||
RUN chmod +x /usr/bin/docker-entrypoint.sh
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||||
|
|
||||||
VOLUME ["/export"]
|
VOLUME ["/export"]
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s \
|
||||||
|
CMD /usr/bin/healthcheck.sh
|
||||||
|
|
||||||
CMD ["minio"]
|
CMD ["minio"]
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,24 @@ FROM resin/aarch64-alpine:3.5
|
|||||||
|
|
||||||
MAINTAINER Minio Inc <dev@minio.io>
|
MAINTAINER Minio Inc <dev@minio.io>
|
||||||
|
|
||||||
|
COPY buildscripts/docker-entrypoint.sh buildscripts/healthcheck.sh /usr/bin/
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
apk add --no-cache ca-certificates && \
|
apk add --no-cache ca-certificates && \
|
||||||
apk add --no-cache --virtual .build-deps curl && \
|
apk add --no-cache --virtual .build-deps curl && \
|
||||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
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 && \
|
curl https://dl.minio.io/server/minio/release/linux-arm64/minio > /usr/bin/minio && \
|
||||||
chmod +x /usr/bin/minio && apk del .build-deps
|
chmod +x /usr/bin/minio && \
|
||||||
|
chmod +x /usr/bin/docker-entrypoint.sh && \
|
||||||
|
chmod +x /usr/bin/healthcheck.sh
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
|
||||||
COPY buildscripts/docker-entrypoint.sh /usr/bin/
|
|
||||||
|
|
||||||
RUN chmod +x /usr/bin/docker-entrypoint.sh
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||||
|
|
||||||
VOLUME ["/export"]
|
VOLUME ["/export"]
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s \
|
||||||
|
CMD /usr/bin/healthcheck.sh
|
||||||
|
|
||||||
CMD ["minio"]
|
CMD ["minio"]
|
||||||
|
|||||||
@@ -2,21 +2,24 @@ FROM resin/armhf-alpine:3.5
|
|||||||
|
|
||||||
MAINTAINER Minio Inc <dev@minio.io>
|
MAINTAINER Minio Inc <dev@minio.io>
|
||||||
|
|
||||||
|
COPY buildscripts/docker-entrypoint.sh buildscripts/healthcheck.sh /usr/bin/
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
apk add --no-cache ca-certificates && \
|
apk add --no-cache ca-certificates && \
|
||||||
apk add --no-cache --virtual .build-deps curl && \
|
apk add --no-cache --virtual .build-deps curl && \
|
||||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
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 && \
|
curl https://dl.minio.io/server/minio/release/linux-arm/minio > /usr/bin/minio && \
|
||||||
chmod +x /usr/bin/minio && apk del .build-deps
|
chmod +x /usr/bin/minio && \
|
||||||
|
chmod +x /usr/bin/docker-entrypoint.sh && \
|
||||||
|
chmod +x /usr/bin/healthcheck.sh
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
|
||||||
COPY buildscripts/docker-entrypoint.sh /usr/bin/
|
|
||||||
|
|
||||||
RUN chmod +x /usr/bin/docker-entrypoint.sh
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||||
|
|
||||||
VOLUME ["/export"]
|
VOLUME ["/export"]
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s \
|
||||||
|
CMD /usr/bin/healthcheck.sh
|
||||||
|
|
||||||
CMD ["minio"]
|
CMD ["minio"]
|
||||||
|
|||||||
@@ -1,59 +1,10 @@
|
|||||||
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
|
||||||
PWD := $(shell pwd)
|
PWD := $(shell pwd)
|
||||||
GOPATH := $(shell go env GOPATH)
|
GOPATH := $(shell go env GOPATH)
|
||||||
|
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
||||||
|
|
||||||
BUILD_LDFLAGS := '$(LDFLAGS)'
|
BUILD_LDFLAGS := '$(LDFLAGS)'
|
||||||
TAG := latest
|
|
||||||
|
|
||||||
HOST ?= $(shell uname)
|
all: build
|
||||||
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:
|
checks:
|
||||||
@echo "Check deps"
|
@echo "Check deps"
|
||||||
@@ -68,7 +19,7 @@ getdeps: checks
|
|||||||
@echo "Installing misspell" && go get -u github.com/client9/misspell/cmd/misspell
|
@echo "Installing misspell" && go get -u github.com/client9/misspell/cmd/misspell
|
||||||
@echo "Installing ineffassign" && go get -u github.com/gordonklaus/ineffassign
|
@echo "Installing ineffassign" && go get -u github.com/gordonklaus/ineffassign
|
||||||
|
|
||||||
verifiers: vet fmt lint cyclo spelling
|
verifiers: getdeps vet fmt lint cyclo spelling
|
||||||
|
|
||||||
vet:
|
vet:
|
||||||
@echo "Running $@"
|
@echo "Running $@"
|
||||||
@@ -94,8 +45,6 @@ cyclo:
|
|||||||
@${GOPATH}/bin/gocyclo -over 100 cmd
|
@${GOPATH}/bin/gocyclo -over 100 cmd
|
||||||
@${GOPATH}/bin/gocyclo -over 100 pkg
|
@${GOPATH}/bin/gocyclo -over 100 pkg
|
||||||
|
|
||||||
build: getdeps verifiers $(UI_ASSETS)
|
|
||||||
|
|
||||||
deadcode:
|
deadcode:
|
||||||
@${GOPATH}/bin/deadcode
|
@${GOPATH}/bin/deadcode
|
||||||
|
|
||||||
@@ -104,7 +53,9 @@ spelling:
|
|||||||
@${GOPATH}/bin/misspell -error `find pkg/`
|
@${GOPATH}/bin/misspell -error `find pkg/`
|
||||||
@${GOPATH}/bin/misspell -error `find docs/`
|
@${GOPATH}/bin/misspell -error `find docs/`
|
||||||
|
|
||||||
test: build
|
# Builds minio, runs the verifiers then runs the tests.
|
||||||
|
check: test
|
||||||
|
test: verifiers build
|
||||||
@echo "Running all minio testing"
|
@echo "Running all minio testing"
|
||||||
@go test $(GOFLAGS) .
|
@go test $(GOFLAGS) .
|
||||||
@go test $(GOFLAGS) github.com/minio/minio/cmd...
|
@go test $(GOFLAGS) github.com/minio/minio/cmd...
|
||||||
@@ -114,9 +65,10 @@ coverage: build
|
|||||||
@echo "Running all coverage for minio"
|
@echo "Running all coverage for minio"
|
||||||
@./buildscripts/go-coverage.sh
|
@./buildscripts/go-coverage.sh
|
||||||
|
|
||||||
gomake-all: build
|
# Builds minio locally.
|
||||||
@echo "Installing minio at $(GOPATH)/bin/minio"
|
build:
|
||||||
@go build --ldflags $(BUILD_LDFLAGS) -o $(GOPATH)/bin/minio
|
@echo "Building minio to $(PWD)/minio ..."
|
||||||
|
@CGO_ENABLED=0 go build --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
|
||||||
|
|
||||||
pkg-add:
|
pkg-add:
|
||||||
@echo "Adding new package $(PKG)"
|
@echo "Adding new package $(PKG)"
|
||||||
@@ -133,7 +85,11 @@ pkg-remove:
|
|||||||
pkg-list:
|
pkg-list:
|
||||||
@$(GOPATH)/bin/govendor list
|
@$(GOPATH)/bin/govendor list
|
||||||
|
|
||||||
install: gomake-all
|
# 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."
|
||||||
|
|
||||||
release: verifiers
|
release: verifiers
|
||||||
@MINIO_RELEASE=RELEASE ./buildscripts/build.sh
|
@MINIO_RELEASE=RELEASE ./buildscripts/build.sh
|
||||||
|
|||||||
@@ -27,20 +27,13 @@ brew install minio/stable/minio
|
|||||||
minio server ~/Photos
|
minio server ~/Photos
|
||||||
```
|
```
|
||||||
#### Note
|
#### Note
|
||||||
If you previously installed minio using `brew install minio` then uninstall minio as shown below
|
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 uninstall minio
|
||||||
```
|
|
||||||
|
|
||||||
Then re-install the latest minio using:
|
|
||||||
|
|
||||||
```
|
|
||||||
brew install minio/stable/minio
|
brew install minio/stable/minio
|
||||||
```
|
```
|
||||||
|
|
||||||
>`brew install minio` and `brew upgrade minio` will no longer install/upgrade the latest minio binaries on macOS. Upstream bugs in golang 1.8 broke Minio brew installer. Use the updated `minio/stable/minio` in your brew paths.
|
|
||||||
|
|
||||||
### Binary Download
|
### Binary Download
|
||||||
| Platform| Architecture | URL|
|
| Platform| Architecture | URL|
|
||||||
| ----------| -------- | ------|
|
| ----------| -------- | ------|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ install:
|
|||||||
# To run your custom scripts instead of automatic MSBuild
|
# To run your custom scripts instead of automatic MSBuild
|
||||||
build_script:
|
build_script:
|
||||||
# Compile
|
# 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"
|
- appveyor AddCompilationMessage "Starting Compile"
|
||||||
- cd c:\gopath\src\github.com\minio\minio
|
- cd c:\gopath\src\github.com\minio\minio
|
||||||
- go run buildscripts/gen-ldflags.go > temp.txt
|
- go run buildscripts/gen-ldflags.go > temp.txt
|
||||||
|
|||||||
@@ -370,8 +370,24 @@ export default class Browse extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleExpireValue(targetInput, inc, object) {
|
handleExpireValue(targetInput, inc, object) {
|
||||||
inc === -1 ? this.refs[targetInput].stepDown(1) : this.refs[targetInput].stepUp(1)
|
let value = this.refs[targetInput].value
|
||||||
|
let maxValue = (targetInput == 'expireHours') ? 23 : (targetInput == 'expireMins') ? 59 : (targetInput == 'expireDays') ? 7 : 0
|
||||||
|
value = isNaN(value) ? 0 : value
|
||||||
|
|
||||||
|
// Use custom step count to support browser Edge
|
||||||
|
if((inc === -1)) {
|
||||||
|
if(value != 0) {
|
||||||
|
value--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(value != maxValue) {
|
||||||
|
value++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.refs[targetInput].value = value
|
||||||
|
|
||||||
|
// Reset hours and mins when days reaches it's max value
|
||||||
if (this.refs.expireDays.value == 7) {
|
if (this.refs.expireDays.value == 7) {
|
||||||
this.refs.expireHours.value = 0
|
this.refs.expireHours.value = 0
|
||||||
this.refs.expireMins.value = 0
|
this.refs.expireMins.value = 0
|
||||||
@@ -379,6 +395,7 @@ export default class Browse extends React.Component {
|
|||||||
if (this.refs.expireDays.value + this.refs.expireHours.value + this.refs.expireMins.value == 0) {
|
if (this.refs.expireDays.value + this.refs.expireHours.value + this.refs.expireMins.value == 0) {
|
||||||
this.refs.expireDays.value = 7
|
this.refs.expireDays.value = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
const {dispatch} = this.props
|
const {dispatch} = this.props
|
||||||
dispatch(actions.shareObject(object, this.refs.expireDays.value, this.refs.expireHours.value, this.refs.expireMins.value))
|
dispatch(actions.shareObject(object, this.refs.expireDays.value, this.refs.expireHours.value, this.refs.expireMins.value))
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-54
File diff suppressed because one or more lines are too long
+1273
-953
File diff suppressed because it is too large
Load Diff
+11
-2
@@ -90,9 +90,18 @@ main() {
|
|||||||
go_build ${each_osarch}
|
go_build ${each_osarch}
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
for each_osarch in $(echo $chosen_osarch | sed 's/,/ /g'); do
|
local found=0
|
||||||
go_build ${each_osarch}
|
for each_osarch in ${SUPPORTED_OSARCH}; do
|
||||||
|
if [ "$chosen_osarch" = "$each_osarch" ]; then
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
if [ ${found} -eq 1 ]; then
|
||||||
|
go_build ${chosen_osarch}
|
||||||
|
else
|
||||||
|
echo "Unknown architecture \"${chosen_osarch}\""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/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 () {
|
||||||
|
address="http://127.0.0.1:9000"
|
||||||
|
resource="/minio/index.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
HealthCheckMain () {
|
||||||
|
# Get the http response code
|
||||||
|
http_response=$(curl -s -o /dev/null -I -w "%{http_code}" ${address}${resource})
|
||||||
|
|
||||||
|
# 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
|
||||||
@@ -113,15 +113,15 @@ func (rc remoteAdminClient) ReInitDisks() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServerInfoData - Returns the server info of this server.
|
// ServerInfoData - Returns the server info of this server.
|
||||||
func (lc localAdminClient) ServerInfoData() (ServerInfoData, error) {
|
func (lc localAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
|
||||||
if globalBootTime.IsZero() {
|
if globalBootTime.IsZero() {
|
||||||
return ServerInfoData{}, errServerNotInitialized
|
return sid, errServerNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build storage info
|
// Build storage info
|
||||||
objLayer := newObjectLayerFn()
|
objLayer := newObjectLayerFn()
|
||||||
if objLayer == nil {
|
if objLayer == nil {
|
||||||
return ServerInfoData{}, errServerNotInitialized
|
return sid, errServerNotInitialized
|
||||||
}
|
}
|
||||||
storage := objLayer.StorageInfo()
|
storage := objLayer.StorageInfo()
|
||||||
|
|
||||||
@@ -145,12 +145,12 @@ func (lc localAdminClient) ServerInfoData() (ServerInfoData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServerInfo - returns the server info of the server to which the RPC call is made.
|
// ServerInfo - returns the server info of the server to which the RPC call is made.
|
||||||
func (rc remoteAdminClient) ServerInfoData() (ServerInfoData, error) {
|
func (rc remoteAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
|
||||||
args := AuthRPCArgs{}
|
args := AuthRPCArgs{}
|
||||||
reply := ServerInfoDataReply{}
|
reply := ServerInfoDataReply{}
|
||||||
err := rc.Call(serverInfoDataRPC, &args, &reply)
|
err := rc.Call(serverInfoDataRPC, &args, &reply)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ServerInfoData{}, err
|
return sid, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return reply.ServerInfoData, nil
|
return reply.ServerInfoData, nil
|
||||||
@@ -493,7 +493,7 @@ func getPeerConfig(peers adminPeers) ([]byte, error) {
|
|||||||
|
|
||||||
// getValidServerConfig - finds the server config that is present in
|
// getValidServerConfig - finds the server config that is present in
|
||||||
// quorum or more number of servers.
|
// quorum or more number of servers.
|
||||||
func getValidServerConfig(serverConfigs []serverConfigV13, errs []error) (serverConfigV13, error) {
|
func getValidServerConfig(serverConfigs []serverConfigV13, errs []error) (scv serverConfigV13, e error) {
|
||||||
// majority-based quorum
|
// majority-based quorum
|
||||||
quorum := len(serverConfigs)/2 + 1
|
quorum := len(serverConfigs)/2 + 1
|
||||||
|
|
||||||
@@ -566,7 +566,7 @@ func getValidServerConfig(serverConfigs []serverConfigV13, errs []error) (server
|
|||||||
|
|
||||||
// If quorum nodes don't agree.
|
// If quorum nodes don't agree.
|
||||||
if maxOccurrence < quorum {
|
if maxOccurrence < quorum {
|
||||||
return serverConfigV13{}, errXLWriteQuorum
|
return scv, errXLWriteQuorum
|
||||||
}
|
}
|
||||||
|
|
||||||
return configJSON, nil
|
return configJSON, nil
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/rpc"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@@ -236,7 +235,7 @@ func (s *adminCmd) CommitConfig(cArgs *CommitConfigArgs, cReply *CommitConfigRep
|
|||||||
// stop and restart commands.
|
// stop and restart commands.
|
||||||
func registerAdminRPCRouter(mux *router.Router) error {
|
func registerAdminRPCRouter(mux *router.Router) error {
|
||||||
adminRPCHandler := &adminCmd{}
|
adminRPCHandler := &adminCmd{}
|
||||||
adminRPCServer := rpc.NewServer()
|
adminRPCServer := newRPCServer()
|
||||||
err := adminRPCServer.RegisterName("Admin", adminRPCHandler)
|
err := adminRPCServer.RegisterName("Admin", adminRPCHandler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return traceError(err)
|
return traceError(err)
|
||||||
|
|||||||
+15
-1
@@ -115,6 +115,7 @@ const (
|
|||||||
ErrBucketAlreadyOwnedByYou
|
ErrBucketAlreadyOwnedByYou
|
||||||
ErrInvalidDuration
|
ErrInvalidDuration
|
||||||
ErrNotSupported
|
ErrNotSupported
|
||||||
|
ErrBucketAlreadyExists
|
||||||
// Add new error codes here.
|
// Add new error codes here.
|
||||||
|
|
||||||
// Bucket notification related errors.
|
// Bucket notification related errors.
|
||||||
@@ -317,7 +318,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
},
|
},
|
||||||
ErrInvalidPart: {
|
ErrInvalidPart: {
|
||||||
Code: "InvalidPart",
|
Code: "InvalidPart",
|
||||||
Description: "One or more of the specified parts could not be found.",
|
Description: "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrInvalidPartOrder: {
|
ErrInvalidPartOrder: {
|
||||||
@@ -355,6 +356,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "The bucket you tried to delete is not empty",
|
Description: "The bucket you tried to delete is not empty",
|
||||||
HTTPStatusCode: http.StatusConflict,
|
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: {
|
ErrAllAccessDisabled: {
|
||||||
Code: "AllAccessDisabled",
|
Code: "AllAccessDisabled",
|
||||||
Description: "All access to this bucket has been disabled.",
|
Description: "All access to this bucket has been disabled.",
|
||||||
@@ -663,6 +669,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrStorageFull
|
apiErr = ErrStorageFull
|
||||||
case BadDigest:
|
case BadDigest:
|
||||||
apiErr = ErrBadDigest
|
apiErr = ErrBadDigest
|
||||||
|
case AllAccessDisabled:
|
||||||
|
apiErr = ErrAllAccessDisabled
|
||||||
case IncompleteBody:
|
case IncompleteBody:
|
||||||
apiErr = ErrIncompleteBody
|
apiErr = ErrIncompleteBody
|
||||||
case ObjectExistsAsDirectory:
|
case ObjectExistsAsDirectory:
|
||||||
@@ -677,6 +685,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrBucketAlreadyOwnedByYou
|
apiErr = ErrBucketAlreadyOwnedByYou
|
||||||
case BucketNotEmpty:
|
case BucketNotEmpty:
|
||||||
apiErr = ErrBucketNotEmpty
|
apiErr = ErrBucketNotEmpty
|
||||||
|
case BucketAlreadyExists:
|
||||||
|
apiErr = ErrBucketAlreadyExists
|
||||||
case BucketExists:
|
case BucketExists:
|
||||||
apiErr = ErrBucketAlreadyOwnedByYou
|
apiErr = ErrBucketAlreadyOwnedByYou
|
||||||
case ObjectNotFound:
|
case ObjectNotFound:
|
||||||
@@ -701,6 +711,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrNoSuchUpload
|
apiErr = ErrNoSuchUpload
|
||||||
case PartTooSmall:
|
case PartTooSmall:
|
||||||
apiErr = ErrEntityTooSmall
|
apiErr = ErrEntityTooSmall
|
||||||
|
case SignatureDoesNotMatch:
|
||||||
|
apiErr = ErrSignatureDoesNotMatch
|
||||||
case SHA256Mismatch:
|
case SHA256Mismatch:
|
||||||
apiErr = ErrContentSHA256Mismatch
|
apiErr = ErrContentSHA256Mismatch
|
||||||
case ObjectTooLarge:
|
case ObjectTooLarge:
|
||||||
@@ -713,6 +725,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrNotImplemented
|
apiErr = ErrNotImplemented
|
||||||
case PolicyNotFound:
|
case PolicyNotFound:
|
||||||
apiErr = ErrNoSuchBucketPolicy
|
apiErr = ErrNoSuchBucketPolicy
|
||||||
|
case PartTooBig:
|
||||||
|
apiErr = ErrEntityTooLarge
|
||||||
default:
|
default:
|
||||||
apiErr = ErrInternalError
|
apiErr = ErrInternalError
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -36,7 +36,11 @@ func setCommonHeaders(w http.ResponseWriter) {
|
|||||||
// Set unique request ID for each reply.
|
// Set unique request ID for each reply.
|
||||||
w.Header().Set(responseRequestIDKey, mustGetRequestID(UTCNow()))
|
w.Header().Set(responseRequestIDKey, mustGetRequestID(UTCNow()))
|
||||||
w.Header().Set("Server", globalServerUserAgent)
|
w.Header().Set("Server", globalServerUserAgent)
|
||||||
w.Header().Set("X-Amz-Bucket-Region", serverConfig.GetRegion())
|
// Set `x-amz-bucket-region` only if region is set on the server
|
||||||
|
// by default minio uses an empty region.
|
||||||
|
if region := serverConfig.GetRegion(); region != "" {
|
||||||
|
w.Header().Set("X-Amz-Bucket-Region", region)
|
||||||
|
}
|
||||||
w.Header().Set("Accept-Ranges", "bytes")
|
w.Header().Set("Accept-Ranges", "bytes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-15
@@ -288,8 +288,6 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
|
|||||||
var owner = Owner{}
|
var owner = Owner{}
|
||||||
|
|
||||||
owner.ID = globalMinioDefaultOwnerID
|
owner.ID = globalMinioDefaultOwnerID
|
||||||
owner.DisplayName = globalMinioDefaultOwnerID
|
|
||||||
|
|
||||||
for _, bucket := range buckets {
|
for _, bucket := range buckets {
|
||||||
var listbucket = Bucket{}
|
var listbucket = Bucket{}
|
||||||
listbucket.Name = bucket.Name
|
listbucket.Name = bucket.Name
|
||||||
@@ -312,8 +310,6 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max
|
|||||||
var data = ListObjectsResponse{}
|
var data = ListObjectsResponse{}
|
||||||
|
|
||||||
owner.ID = globalMinioDefaultOwnerID
|
owner.ID = globalMinioDefaultOwnerID
|
||||||
owner.DisplayName = globalMinioDefaultOwnerID
|
|
||||||
|
|
||||||
for _, object := range resp.Objects {
|
for _, object := range resp.Objects {
|
||||||
var content = Object{}
|
var content = Object{}
|
||||||
if object.Name == "" {
|
if object.Name == "" {
|
||||||
@@ -352,18 +348,17 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generates an ListObjectsV2 response for the said bucket with other enumerated options.
|
// generates an ListObjectsV2 response for the said bucket with other enumerated options.
|
||||||
func generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter string, fetchOwner bool, maxKeys int, resp ListObjectsInfo) ListObjectsV2Response {
|
func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, delimiter string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string) ListObjectsV2Response {
|
||||||
var contents []Object
|
var contents []Object
|
||||||
var prefixes []CommonPrefix
|
var commonPrefixes []CommonPrefix
|
||||||
var owner = Owner{}
|
var owner = Owner{}
|
||||||
var data = ListObjectsV2Response{}
|
var data = ListObjectsV2Response{}
|
||||||
|
|
||||||
if fetchOwner {
|
if fetchOwner {
|
||||||
owner.ID = globalMinioDefaultOwnerID
|
owner.ID = globalMinioDefaultOwnerID
|
||||||
owner.DisplayName = globalMinioDefaultOwnerID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, object := range resp.Objects {
|
for _, object := range objects {
|
||||||
var content = Object{}
|
var content = Object{}
|
||||||
if object.Name == "" {
|
if object.Name == "" {
|
||||||
continue
|
continue
|
||||||
@@ -387,14 +382,14 @@ func generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter
|
|||||||
data.Prefix = prefix
|
data.Prefix = prefix
|
||||||
data.MaxKeys = maxKeys
|
data.MaxKeys = maxKeys
|
||||||
data.ContinuationToken = token
|
data.ContinuationToken = token
|
||||||
data.NextContinuationToken = resp.NextMarker
|
data.NextContinuationToken = nextToken
|
||||||
data.IsTruncated = resp.IsTruncated
|
data.IsTruncated = isTruncated
|
||||||
for _, prefix := range resp.Prefixes {
|
for _, prefix := range prefixes {
|
||||||
var prefixItem = CommonPrefix{}
|
var prefixItem = CommonPrefix{}
|
||||||
prefixItem.Prefix = prefix
|
prefixItem.Prefix = prefix
|
||||||
prefixes = append(prefixes, prefixItem)
|
commonPrefixes = append(commonPrefixes, prefixItem)
|
||||||
}
|
}
|
||||||
data.CommonPrefixes = prefixes
|
data.CommonPrefixes = commonPrefixes
|
||||||
data.KeyCount = len(data.Contents) + len(data.CommonPrefixes)
|
data.KeyCount = len(data.Contents) + len(data.CommonPrefixes)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
@@ -443,9 +438,7 @@ func generateListPartsResponse(partsInfo ListPartsInfo) ListPartsResponse {
|
|||||||
listPartsResponse.UploadID = partsInfo.UploadID
|
listPartsResponse.UploadID = partsInfo.UploadID
|
||||||
listPartsResponse.StorageClass = globalMinioDefaultStorageClass
|
listPartsResponse.StorageClass = globalMinioDefaultStorageClass
|
||||||
listPartsResponse.Initiator.ID = globalMinioDefaultOwnerID
|
listPartsResponse.Initiator.ID = globalMinioDefaultOwnerID
|
||||||
listPartsResponse.Initiator.DisplayName = globalMinioDefaultOwnerID
|
|
||||||
listPartsResponse.Owner.ID = globalMinioDefaultOwnerID
|
listPartsResponse.Owner.ID = globalMinioDefaultOwnerID
|
||||||
listPartsResponse.Owner.DisplayName = globalMinioDefaultOwnerID
|
|
||||||
|
|
||||||
listPartsResponse.MaxParts = partsInfo.MaxParts
|
listPartsResponse.MaxParts = partsInfo.MaxParts
|
||||||
listPartsResponse.PartNumberMarker = partsInfo.PartNumberMarker
|
listPartsResponse.PartNumberMarker = partsInfo.PartNumberMarker
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func TestLogin(t *testing.T) {
|
|||||||
// Invalid password length
|
// Invalid password length
|
||||||
{
|
{
|
||||||
args: LoginRPCArgs{
|
args: LoginRPCArgs{
|
||||||
Username: globalMinioDefaultOwnerID,
|
Username: "minio",
|
||||||
Password: "aaa",
|
Password: "aaa",
|
||||||
Version: Version,
|
Version: Version,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,8 +17,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/rpc"
|
|
||||||
|
|
||||||
router "github.com/gorilla/mux"
|
router "github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,7 +37,7 @@ type browserPeerAPIHandlers struct {
|
|||||||
func registerBrowserPeerRPCRouter(mux *router.Router) error {
|
func registerBrowserPeerRPCRouter(mux *router.Router) error {
|
||||||
bpHandlers := &browserPeerAPIHandlers{}
|
bpHandlers := &browserPeerAPIHandlers{}
|
||||||
|
|
||||||
bpRPCServer := rpc.NewServer()
|
bpRPCServer := newRPCServer()
|
||||||
err := bpRPCServer.RegisterName("BrowserPeer", bpHandlers)
|
err := bpRPCServer.RegisterName("BrowserPeer", bpHandlers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return traceError(err)
|
return traceError(err)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter, fetchOwner, maxKeys, listObjectsInfo)
|
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsInfo.NextMarker, startAfter, delimiter, fetchOwner, listObjectsInfo.IsTruncated, maxKeys, listObjectsInfo.Objects, listObjectsInfo.Prefixes)
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessResponseXML(w, encodeResponse(response))
|
writeSuccessResponseXML(w, encodeResponse(response))
|
||||||
|
|||||||
+15
-1
@@ -525,7 +525,12 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract metadata to be saved from received Form.
|
// Extract metadata to be saved from received Form.
|
||||||
metadata := extractMetadataFromForm(formValues)
|
metadata, err := extractMetadataFromHeader(formValues)
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "found invalid http request header")
|
||||||
|
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
sha256sum := ""
|
sha256sum := ""
|
||||||
|
|
||||||
objectLock := globalNSMutex.NewNSLock(bucket, object)
|
objectLock := globalNSMutex.NewNSLock(bucket, object)
|
||||||
@@ -648,12 +653,21 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
// Delete bucket access policy, if present - ignore any errors.
|
// Delete bucket access policy, if present - ignore any errors.
|
||||||
_ = removeBucketPolicy(bucket, objectAPI)
|
_ = 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.
|
// Delete notification config, if present - ignore any errors.
|
||||||
_ = removeNotificationConfig(bucket, objectAPI)
|
_ = removeNotificationConfig(bucket, objectAPI)
|
||||||
|
|
||||||
|
// Notify all peers (including self) to update in-memory state
|
||||||
|
S3PeersUpdateBucketNotification(bucket, nil)
|
||||||
|
|
||||||
// Delete listener config, if present - ignore any errors.
|
// Delete listener config, if present - ignore any errors.
|
||||||
_ = removeListenerConfig(bucket, objectAPI)
|
_ = removeListenerConfig(bucket, objectAPI)
|
||||||
|
|
||||||
|
// Notify all peers (including self) to update in-memory state
|
||||||
|
S3PeersUpdateBucketListener(bucket, []listenerConfig{})
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,6 +204,15 @@ func writeNotification(w http.ResponseWriter, notification map[string][]Notifica
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// Add additional CRLF characters for client to
|
||||||
// differentiate the individual events properly.
|
// differentiate the individual events properly.
|
||||||
_, err = w.Write(append(notificationBytes, crlf...))
|
_, err = w.Write(append(notificationBytes, crlf...))
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ func testGetBucketNotificationHandler(obj ObjectLayer, instanceType, bucketName
|
|||||||
filterRules := []filterRule{
|
filterRules := []filterRule{
|
||||||
{
|
{
|
||||||
Name: "prefix",
|
Name: "prefix",
|
||||||
Value: globalMinioDefaultOwnerID,
|
Value: "minio",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "suffix",
|
Name: "suffix",
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ func isValidQueueID(queueARN string) bool {
|
|||||||
if isAMQPQueue(sqsARN) { // AMQP eueue.
|
if isAMQPQueue(sqsARN) { // AMQP eueue.
|
||||||
amqpN := serverConfig.Notify.GetAMQPByID(sqsARN.AccountID)
|
amqpN := serverConfig.Notify.GetAMQPByID(sqsARN.AccountID)
|
||||||
return amqpN.Enable && amqpN.URL != ""
|
return amqpN.Enable && amqpN.URL != ""
|
||||||
|
} else if isMQTTQueue(sqsARN) {
|
||||||
|
mqttN := serverConfig.Notify.GetMQTTByID(sqsARN.AccountID)
|
||||||
|
return mqttN.Enable && mqttN.Broker != ""
|
||||||
} else if isNATSQueue(sqsARN) {
|
} else if isNATSQueue(sqsARN) {
|
||||||
natsN := serverConfig.Notify.GetNATSByID(sqsARN.AccountID)
|
natsN := serverConfig.Notify.GetNATSByID(sqsARN.AccountID)
|
||||||
return natsN.Enable && natsN.Address != ""
|
return natsN.Enable && natsN.Address != ""
|
||||||
@@ -251,6 +254,7 @@ func validateNotificationConfig(nConfig notificationConfig) APIErrorCode {
|
|||||||
// Unmarshals input value of AWS ARN format into minioSqs object.
|
// Unmarshals input value of AWS ARN format into minioSqs object.
|
||||||
// Returned value represents minio sqs types, currently supported are
|
// Returned value represents minio sqs types, currently supported are
|
||||||
// - amqp
|
// - amqp
|
||||||
|
// - mqtt
|
||||||
// - nats
|
// - nats
|
||||||
// - elasticsearch
|
// - elasticsearch
|
||||||
// - redis
|
// - redis
|
||||||
@@ -273,6 +277,8 @@ func unmarshalSqsARN(queueARN string) (mSqs arnSQS) {
|
|||||||
switch sqsType {
|
switch sqsType {
|
||||||
case queueTypeAMQP:
|
case queueTypeAMQP:
|
||||||
mSqs.Type = queueTypeAMQP
|
mSqs.Type = queueTypeAMQP
|
||||||
|
case queueTypeMQTT:
|
||||||
|
mSqs.Type = queueTypeMQTT
|
||||||
case queueTypeNATS:
|
case queueTypeNATS:
|
||||||
mSqs.Type = queueTypeNATS
|
mSqs.Type = queueTypeNATS
|
||||||
case queueTypeElastic:
|
case queueTypeElastic:
|
||||||
|
|||||||
@@ -358,6 +358,11 @@ func TestUnmarshalSQSARN(t *testing.T) {
|
|||||||
queueARN: "arn:minio:sqs:us-east-1:1:amqp",
|
queueARN: "arn:minio:sqs:us-east-1:1:amqp",
|
||||||
Type: "amqp",
|
Type: "amqp",
|
||||||
},
|
},
|
||||||
|
// Valid mqtt queue arn.
|
||||||
|
{
|
||||||
|
queueARN: "arn:minio:sqs:us-east-1:1:mqtt",
|
||||||
|
Type: "mqtt",
|
||||||
|
},
|
||||||
// Invalid empty queue arn.
|
// Invalid empty queue arn.
|
||||||
{
|
{
|
||||||
queueARN: "",
|
queueARN: "",
|
||||||
|
|||||||
+31
-24
@@ -17,6 +17,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -24,35 +25,34 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parsePublicCertFile(certFile string) (certs []*x509.Certificate, err error) {
|
func parsePublicCertFile(certFile string) (x509Certs []*x509.Certificate, err error) {
|
||||||
var bytes []byte
|
// Read certificate file.
|
||||||
|
var data []byte
|
||||||
if bytes, err = ioutil.ReadFile(certFile); err != nil {
|
if data, err = ioutil.ReadFile(certFile); err != nil {
|
||||||
return certs, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse all certs in the chain.
|
// Parse all certs in the chain.
|
||||||
var block *pem.Block
|
current := data
|
||||||
var cert *x509.Certificate
|
|
||||||
current := bytes
|
|
||||||
for len(current) > 0 {
|
for len(current) > 0 {
|
||||||
if block, current = pem.Decode(current); block == nil {
|
var pemBlock *pem.Block
|
||||||
err = fmt.Errorf("Could not read PEM block from file %s", certFile)
|
if pemBlock, current = pem.Decode(current); pemBlock == nil {
|
||||||
return certs, err
|
return nil, fmt.Errorf("Could not read PEM block from file %s", certFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cert, err = x509.ParseCertificate(block.Bytes); err != nil {
|
var x509Cert *x509.Certificate
|
||||||
return certs, err
|
if x509Cert, err = x509.ParseCertificate(pemBlock.Bytes); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
certs = append(certs, cert)
|
x509Certs = append(x509Certs, x509Cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(certs) == 0 {
|
if len(x509Certs) == 0 {
|
||||||
err = fmt.Errorf("Empty public certificate file %s", certFile)
|
return nil, fmt.Errorf("Empty public certificate file %s", certFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
return certs, err
|
return x509Certs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
|
func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
|
||||||
@@ -81,7 +81,7 @@ func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
|
|||||||
for _, caFile := range caFiles {
|
for _, caFile := range caFiles {
|
||||||
caCert, err := ioutil.ReadFile(caFile)
|
caCert, err := ioutil.ReadFile(caFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rootCAs, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rootCAs.AppendCertsFromPEM(caCert)
|
rootCAs.AppendCertsFromPEM(caCert)
|
||||||
@@ -90,19 +90,26 @@ func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
|
|||||||
return rootCAs, nil
|
return rootCAs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSSLConfig() (publicCerts []*x509.Certificate, rootCAs *x509.CertPool, secureConn bool, err error) {
|
func getSSLConfig() (x509Certs []*x509.Certificate, rootCAs *x509.CertPool, tlsCert *tls.Certificate, secureConn bool, err error) {
|
||||||
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
|
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
|
||||||
return publicCerts, rootCAs, secureConn, err
|
return nil, nil, nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if publicCerts, err = parsePublicCertFile(getPublicCertFile()); err != nil {
|
if x509Certs, err = parsePublicCertFile(getPublicCertFile()); err != nil {
|
||||||
return publicCerts, rootCAs, secureConn, err
|
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 {
|
if rootCAs, err = getRootCAs(getCADir()); err != nil {
|
||||||
return publicCerts, rootCAs, secureConn, err
|
return nil, nil, nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
secureConn = true
|
secureConn = true
|
||||||
return publicCerts, rootCAs, secureConn, err
|
return x509Certs, rootCAs, tlsCert, secureConn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+113
-1
@@ -141,7 +141,13 @@ func migrateConfig() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
case v18:
|
case "18":
|
||||||
|
// Migrate version '17' to '18'.
|
||||||
|
if err = migrateV18ToV19(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case v19:
|
||||||
// No migration needed. this always points to current version.
|
// No migration needed. this always points to current version.
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
@@ -1366,3 +1372,109 @@ func migrateV17ToV18() error {
|
|||||||
log.Printf(configMigrateMSGTemplate, configFile, cv17.Version, srvConfig.Version)
|
log.Printf(configMigrateMSGTemplate, configFile, cv17.Version, srvConfig.Version)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migrateV18ToV19() error {
|
||||||
|
configFile := getConfigFile()
|
||||||
|
|
||||||
|
cv18 := &serverConfigV18{}
|
||||||
|
_, err := quick.Load(configFile, cv18)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("Unable to load config version ‘18’. %v", err)
|
||||||
|
}
|
||||||
|
if cv18.Version != "18" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy over fields from V18 into V19 config struct
|
||||||
|
srvConfig := &serverConfigV18{
|
||||||
|
Logger: &loggers{},
|
||||||
|
Notify: ¬ifier{},
|
||||||
|
}
|
||||||
|
srvConfig.Version = "19"
|
||||||
|
srvConfig.Credential = cv18.Credential
|
||||||
|
srvConfig.Region = cv18.Region
|
||||||
|
if srvConfig.Region == "" {
|
||||||
|
// Region needs to be set for AWS Signature Version 4.
|
||||||
|
srvConfig.Region = globalMinioDefaultRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
srvConfig.Logger.Console = cv18.Logger.Console
|
||||||
|
srvConfig.Logger.File = cv18.Logger.File
|
||||||
|
|
||||||
|
// check and set notifiers config
|
||||||
|
if len(cv18.Notify.AMQP) == 0 {
|
||||||
|
srvConfig.Notify.AMQP = make(map[string]amqpNotify)
|
||||||
|
srvConfig.Notify.AMQP["1"] = amqpNotify{}
|
||||||
|
} else {
|
||||||
|
// New deliveryMode parameter is added for AMQP,
|
||||||
|
// default value is already 0, so nothing to
|
||||||
|
// explicitly migrate here.
|
||||||
|
srvConfig.Notify.AMQP = cv18.Notify.AMQP
|
||||||
|
}
|
||||||
|
if len(cv18.Notify.ElasticSearch) == 0 {
|
||||||
|
srvConfig.Notify.ElasticSearch = make(map[string]elasticSearchNotify)
|
||||||
|
srvConfig.Notify.ElasticSearch["1"] = elasticSearchNotify{
|
||||||
|
Format: formatNamespace,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
srvConfig.Notify.ElasticSearch = cv18.Notify.ElasticSearch
|
||||||
|
}
|
||||||
|
if len(cv18.Notify.Redis) == 0 {
|
||||||
|
srvConfig.Notify.Redis = make(map[string]redisNotify)
|
||||||
|
srvConfig.Notify.Redis["1"] = redisNotify{
|
||||||
|
Format: formatNamespace,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
srvConfig.Notify.Redis = cv18.Notify.Redis
|
||||||
|
}
|
||||||
|
if len(cv18.Notify.PostgreSQL) == 0 {
|
||||||
|
srvConfig.Notify.PostgreSQL = make(map[string]postgreSQLNotify)
|
||||||
|
srvConfig.Notify.PostgreSQL["1"] = postgreSQLNotify{
|
||||||
|
Format: formatNamespace,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
srvConfig.Notify.PostgreSQL = cv18.Notify.PostgreSQL
|
||||||
|
}
|
||||||
|
if len(cv18.Notify.Kafka) == 0 {
|
||||||
|
srvConfig.Notify.Kafka = make(map[string]kafkaNotify)
|
||||||
|
srvConfig.Notify.Kafka["1"] = kafkaNotify{}
|
||||||
|
} else {
|
||||||
|
srvConfig.Notify.Kafka = cv18.Notify.Kafka
|
||||||
|
}
|
||||||
|
if len(cv18.Notify.NATS) == 0 {
|
||||||
|
srvConfig.Notify.NATS = make(map[string]natsNotify)
|
||||||
|
srvConfig.Notify.NATS["1"] = natsNotify{}
|
||||||
|
} else {
|
||||||
|
srvConfig.Notify.NATS = cv18.Notify.NATS
|
||||||
|
}
|
||||||
|
if len(cv18.Notify.Webhook) == 0 {
|
||||||
|
srvConfig.Notify.Webhook = make(map[string]webhookNotify)
|
||||||
|
srvConfig.Notify.Webhook["1"] = webhookNotify{}
|
||||||
|
} else {
|
||||||
|
srvConfig.Notify.Webhook = cv18.Notify.Webhook
|
||||||
|
}
|
||||||
|
if len(cv18.Notify.MySQL) == 0 {
|
||||||
|
srvConfig.Notify.MySQL = make(map[string]mySQLNotify)
|
||||||
|
srvConfig.Notify.MySQL["1"] = mySQLNotify{
|
||||||
|
Format: formatNamespace,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
srvConfig.Notify.MySQL = cv18.Notify.MySQL
|
||||||
|
}
|
||||||
|
|
||||||
|
// V18 will not have mqtt support, so we add that here.
|
||||||
|
srvConfig.Notify.MQTT = make(map[string]mqttNotify)
|
||||||
|
srvConfig.Notify.MQTT["1"] = mqttNotify{}
|
||||||
|
|
||||||
|
// Load browser config from existing config in the file.
|
||||||
|
srvConfig.Browser = cv18.Browser
|
||||||
|
|
||||||
|
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||||
|
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv18.Version, srvConfig.Version, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(configMigrateMSGTemplate, configFile, cv18.Version, srvConfig.Version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -122,11 +122,14 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
|||||||
if err := migrateV17ToV18(); err != nil {
|
if err := migrateV17ToV18(); err != nil {
|
||||||
t.Fatal("migrate v17 to v18 should succeed when no config file is found")
|
t.Fatal("migrate v17 to v18 should succeed when no config file is found")
|
||||||
}
|
}
|
||||||
|
if err := migrateV18ToV19(); err != nil {
|
||||||
|
t.Fatal("migrate v18 to v19 should succeed when no config file is found")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test if a config migration from v2 to v18 is successfully done
|
// Test if a config migration from v2 to v19 is successfully done
|
||||||
func TestServerConfigMigrateV2toV18(t *testing.T) {
|
func TestServerConfigMigrateV2toV19(t *testing.T) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Init Test config failed")
|
t.Fatalf("Init Test config failed")
|
||||||
@@ -166,7 +169,7 @@ func TestServerConfigMigrateV2toV18(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check the version number in the upgraded config file
|
// Check the version number in the upgraded config file
|
||||||
expectedVersion := v18
|
expectedVersion := v19
|
||||||
if serverConfig.Version != expectedVersion {
|
if serverConfig.Version != expectedVersion {
|
||||||
t.Fatalf("Expect version "+expectedVersion+", found: %v", serverConfig.Version)
|
t.Fatalf("Expect version "+expectedVersion+", found: %v", serverConfig.Version)
|
||||||
}
|
}
|
||||||
@@ -246,6 +249,9 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
|||||||
if err := migrateV17ToV18(); err == nil {
|
if err := migrateV17ToV18(); err == nil {
|
||||||
t.Fatal("migrateConfigV17ToV18() should fail with a corrupted json")
|
t.Fatal("migrateConfigV17ToV18() should fail with a corrupted json")
|
||||||
}
|
}
|
||||||
|
if err := migrateV18ToV19(); err == nil {
|
||||||
|
t.Fatal("migrateConfigV18ToV19() should fail with a corrupted json")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test if all migrate code returns error with corrupted config files
|
// Test if all migrate code returns error with corrupted config files
|
||||||
|
|||||||
@@ -449,3 +449,22 @@ type serverConfigV17 struct {
|
|||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify *notifier `json:"notify"`
|
Notify *notifier `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serverConfigV18 server configuration version '18' which is like
|
||||||
|
// version '17' except it adds support for "deliveryMode" parameter in
|
||||||
|
// the AMQP notification target.
|
||||||
|
type serverConfigV18 struct {
|
||||||
|
sync.RWMutex
|
||||||
|
Version string `json:"version"`
|
||||||
|
|
||||||
|
// S3 API configuration.
|
||||||
|
Credential credential `json:"credential"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Browser BrowserFlag `json:"browser"`
|
||||||
|
|
||||||
|
// Additional error logging configuration.
|
||||||
|
Logger *loggers `json:"logger"`
|
||||||
|
|
||||||
|
// Notification queue configuration.
|
||||||
|
Notify *notifier `json:"notify"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,18 +27,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Config version
|
// Config version
|
||||||
const v18 = "18"
|
const v19 = "19"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// serverConfig server config.
|
// serverConfig server config.
|
||||||
serverConfig *serverConfigV18
|
serverConfig *serverConfigV19
|
||||||
serverConfigMu sync.RWMutex
|
serverConfigMu sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
// serverConfigV18 server configuration version '18' which is like
|
// serverConfigV19 server configuration version '19' which is like
|
||||||
// version '17' except it adds support for "deliveryMode" parameter in
|
// version '18' except it adds support for MQTT notifications.
|
||||||
// the AMQP notification target.
|
type serverConfigV19 struct {
|
||||||
type serverConfigV18 struct {
|
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
|
|
||||||
@@ -55,7 +54,7 @@ type serverConfigV18 struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetVersion get current config version.
|
// GetVersion get current config version.
|
||||||
func (s *serverConfigV18) GetVersion() string {
|
func (s *serverConfigV19) GetVersion() string {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
|
|
||||||
@@ -63,7 +62,7 @@ func (s *serverConfigV18) GetVersion() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetRegion set new region.
|
// SetRegion set new region.
|
||||||
func (s *serverConfigV18) SetRegion(region string) {
|
func (s *serverConfigV19) SetRegion(region string) {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
defer s.Unlock()
|
defer s.Unlock()
|
||||||
|
|
||||||
@@ -71,7 +70,7 @@ func (s *serverConfigV18) SetRegion(region string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetRegion get current region.
|
// GetRegion get current region.
|
||||||
func (s *serverConfigV18) GetRegion() string {
|
func (s *serverConfigV19) GetRegion() string {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
|
|
||||||
@@ -79,7 +78,7 @@ func (s *serverConfigV18) GetRegion() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetCredentials set new credentials.
|
// SetCredentials set new credentials.
|
||||||
func (s *serverConfigV18) SetCredential(creds credential) {
|
func (s *serverConfigV19) SetCredential(creds credential) {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
defer s.Unlock()
|
defer s.Unlock()
|
||||||
|
|
||||||
@@ -88,7 +87,7 @@ func (s *serverConfigV18) SetCredential(creds credential) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCredentials get current credentials.
|
// GetCredentials get current credentials.
|
||||||
func (s *serverConfigV18) GetCredential() credential {
|
func (s *serverConfigV19) GetCredential() credential {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
|
|
||||||
@@ -96,7 +95,7 @@ func (s *serverConfigV18) GetCredential() credential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetBrowser set if browser is enabled.
|
// SetBrowser set if browser is enabled.
|
||||||
func (s *serverConfigV18) SetBrowser(b bool) {
|
func (s *serverConfigV19) SetBrowser(b bool) {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
defer s.Unlock()
|
defer s.Unlock()
|
||||||
|
|
||||||
@@ -105,7 +104,7 @@ func (s *serverConfigV18) SetBrowser(b bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCredentials get current credentials.
|
// GetCredentials get current credentials.
|
||||||
func (s *serverConfigV18) GetBrowser() bool {
|
func (s *serverConfigV19) GetBrowser() bool {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
|
|
||||||
@@ -113,7 +112,7 @@ func (s *serverConfigV18) GetBrowser() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save config.
|
// Save config.
|
||||||
func (s *serverConfigV18) Save() error {
|
func (s *serverConfigV19) Save() error {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
|
|
||||||
@@ -121,9 +120,9 @@ func (s *serverConfigV18) Save() error {
|
|||||||
return quick.Save(getConfigFile(), s)
|
return quick.Save(getConfigFile(), s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServerConfigV18() *serverConfigV18 {
|
func newServerConfigV19() *serverConfigV19 {
|
||||||
srvCfg := &serverConfigV18{
|
srvCfg := &serverConfigV19{
|
||||||
Version: v18,
|
Version: v19,
|
||||||
Credential: mustGetNewCredential(),
|
Credential: mustGetNewCredential(),
|
||||||
Region: globalMinioDefaultRegion,
|
Region: globalMinioDefaultRegion,
|
||||||
Browser: true,
|
Browser: true,
|
||||||
@@ -137,6 +136,8 @@ func newServerConfigV18() *serverConfigV18 {
|
|||||||
// Make sure to initialize notification configs.
|
// Make sure to initialize notification configs.
|
||||||
srvCfg.Notify.AMQP = make(map[string]amqpNotify)
|
srvCfg.Notify.AMQP = make(map[string]amqpNotify)
|
||||||
srvCfg.Notify.AMQP["1"] = amqpNotify{}
|
srvCfg.Notify.AMQP["1"] = amqpNotify{}
|
||||||
|
srvCfg.Notify.MQTT = make(map[string]mqttNotify)
|
||||||
|
srvCfg.Notify.MQTT["1"] = mqttNotify{}
|
||||||
srvCfg.Notify.ElasticSearch = make(map[string]elasticSearchNotify)
|
srvCfg.Notify.ElasticSearch = make(map[string]elasticSearchNotify)
|
||||||
srvCfg.Notify.ElasticSearch["1"] = elasticSearchNotify{}
|
srvCfg.Notify.ElasticSearch["1"] = elasticSearchNotify{}
|
||||||
srvCfg.Notify.Redis = make(map[string]redisNotify)
|
srvCfg.Notify.Redis = make(map[string]redisNotify)
|
||||||
@@ -159,7 +160,7 @@ func newServerConfigV18() *serverConfigV18 {
|
|||||||
// found, otherwise use default parameters
|
// found, otherwise use default parameters
|
||||||
func newConfig() error {
|
func newConfig() error {
|
||||||
// Initialize server config.
|
// Initialize server config.
|
||||||
srvCfg := newServerConfigV18()
|
srvCfg := newServerConfigV19()
|
||||||
|
|
||||||
// If env is set override the credentials from config file.
|
// If env is set override the credentials from config file.
|
||||||
if globalIsEnvCreds {
|
if globalIsEnvCreds {
|
||||||
@@ -237,8 +238,8 @@ func checkDupJSONKeys(json string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getValidConfig - returns valid server configuration
|
// getValidConfig - returns valid server configuration
|
||||||
func getValidConfig() (*serverConfigV18, error) {
|
func getValidConfig() (*serverConfigV19, error) {
|
||||||
srvCfg := &serverConfigV18{
|
srvCfg := &serverConfigV19{
|
||||||
Region: globalMinioDefaultRegion,
|
Region: globalMinioDefaultRegion,
|
||||||
Browser: true,
|
Browser: true,
|
||||||
}
|
}
|
||||||
@@ -248,8 +249,8 @@ func getValidConfig() (*serverConfigV18, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if srvCfg.Version != v18 {
|
if srvCfg.Version != v19 {
|
||||||
return nil, fmt.Errorf("configuration version mismatch. Expected: ‘%s’, Got: ‘%s’", v18, srvCfg.Version)
|
return nil, fmt.Errorf("configuration version mismatch. Expected: ‘%s’, Got: ‘%s’", v19, srvCfg.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config file json and check for duplication json keys
|
// Load config file json and check for duplication json keys
|
||||||
@@ -80,13 +80,21 @@ func TestServerConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set new console logger.
|
// Set new console logger.
|
||||||
// Set new Webhook notification id.
|
// Set new MySQL notification id.
|
||||||
serverConfig.Notify.SetMySQLByID("2", mySQLNotify{})
|
serverConfig.Notify.SetMySQLByID("2", mySQLNotify{})
|
||||||
savedNotifyCfg6 := serverConfig.Notify.GetMySQLByID("2")
|
savedNotifyCfg6 := serverConfig.Notify.GetMySQLByID("2")
|
||||||
if !reflect.DeepEqual(savedNotifyCfg6, mySQLNotify{}) {
|
if !reflect.DeepEqual(savedNotifyCfg6, mySQLNotify{}) {
|
||||||
t.Errorf("Expecting Webhook config %#v found %#v", mySQLNotify{}, savedNotifyCfg6)
|
t.Errorf("Expecting Webhook config %#v found %#v", mySQLNotify{}, savedNotifyCfg6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set new console logger.
|
||||||
|
// Set new MQTT notification id.
|
||||||
|
serverConfig.Notify.SetMQTTByID("2", mqttNotify{})
|
||||||
|
savedNotifyCfg7 := serverConfig.Notify.GetMQTTByID("2")
|
||||||
|
if !reflect.DeepEqual(savedNotifyCfg7, mqttNotify{}) {
|
||||||
|
t.Errorf("Expecting Webhook config %#v found %#v", mqttNotify{}, savedNotifyCfg7)
|
||||||
|
}
|
||||||
|
|
||||||
consoleLogger := NewConsoleLogger()
|
consoleLogger := NewConsoleLogger()
|
||||||
serverConfig.Logger.SetConsole(consoleLogger)
|
serverConfig.Logger.SetConsole(consoleLogger)
|
||||||
consoleCfg := serverConfig.Logger.GetConsole()
|
consoleCfg := serverConfig.Logger.GetConsole()
|
||||||
@@ -109,8 +117,8 @@ func TestServerConfig(t *testing.T) {
|
|||||||
serverConfig.Logger.SetFile(fileLogger)
|
serverConfig.Logger.SetFile(fileLogger)
|
||||||
|
|
||||||
// Match version.
|
// Match version.
|
||||||
if serverConfig.GetVersion() != v18 {
|
if serverConfig.GetVersion() != v19 {
|
||||||
t.Errorf("Expecting version %s found %s", serverConfig.GetVersion(), v18)
|
t.Errorf("Expecting version %s found %s", serverConfig.GetVersion(), v19)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to save.
|
// Attempt to save.
|
||||||
@@ -223,7 +231,7 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
|
|
||||||
configPath := filepath.Join(rootPath, minioConfigFile)
|
configPath := filepath.Join(rootPath, minioConfigFile)
|
||||||
|
|
||||||
v := v18
|
v := v19
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
configData string
|
configData string
|
||||||
@@ -309,6 +317,9 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
|
|
||||||
// Test 28 - Test valid Format for Redis
|
// Test 28 - Test valid Format for Redis
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
|
||||||
|
|
||||||
|
// Test 29 - Test MQTT
|
||||||
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "clientId": "", "username": "", "password": ""}}}}`, false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
+9
-9
@@ -77,14 +77,14 @@ func (endpoint Endpoint) SetHTTP() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewEndpoint - returns new endpoint based on given arguments.
|
// NewEndpoint - returns new endpoint based on given arguments.
|
||||||
func NewEndpoint(arg string) (Endpoint, error) {
|
func NewEndpoint(arg string) (ep Endpoint, e error) {
|
||||||
// isEmptyPath - check whether given path is not empty.
|
// isEmptyPath - check whether given path is not empty.
|
||||||
isEmptyPath := func(path string) bool {
|
isEmptyPath := func(path string) bool {
|
||||||
return path == "" || path == "/" || path == `\`
|
return path == "" || path == "/" || path == `\`
|
||||||
}
|
}
|
||||||
|
|
||||||
if isEmptyPath(arg) {
|
if isEmptyPath(arg) {
|
||||||
return Endpoint{}, fmt.Errorf("empty or root endpoint is not supported")
|
return ep, fmt.Errorf("empty or root endpoint is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
var isLocal bool
|
var isLocal bool
|
||||||
@@ -96,13 +96,13 @@ func NewEndpoint(arg string) (Endpoint, error) {
|
|||||||
// - All field should be empty except Host and Path.
|
// - All field should be empty except Host and Path.
|
||||||
if !((u.Scheme == "http" || u.Scheme == "https") &&
|
if !((u.Scheme == "http" || u.Scheme == "https") &&
|
||||||
u.User == nil && u.Opaque == "" && u.ForceQuery == false && u.RawQuery == "" && u.Fragment == "") {
|
u.User == nil && u.Opaque == "" && u.ForceQuery == false && u.RawQuery == "" && u.Fragment == "") {
|
||||||
return Endpoint{}, fmt.Errorf("invalid URL endpoint format")
|
return ep, fmt.Errorf("invalid URL endpoint format")
|
||||||
}
|
}
|
||||||
|
|
||||||
host, port, err := net.SplitHostPort(u.Host)
|
host, port, err := net.SplitHostPort(u.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !strings.Contains(err.Error(), "missing port in address") {
|
if !strings.Contains(err.Error(), "missing port in address") {
|
||||||
return Endpoint{}, fmt.Errorf("invalid URL endpoint format: %s", err)
|
return ep, fmt.Errorf("invalid URL endpoint format: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
host = u.Host
|
host = u.Host
|
||||||
@@ -110,26 +110,26 @@ func NewEndpoint(arg string) (Endpoint, error) {
|
|||||||
var p int
|
var p int
|
||||||
p, err = strconv.Atoi(port)
|
p, err = strconv.Atoi(port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Endpoint{}, fmt.Errorf("invalid URL endpoint format: invalid port number")
|
return ep, fmt.Errorf("invalid URL endpoint format: invalid port number")
|
||||||
} else if p < 1 || p > 65535 {
|
} else if p < 1 || p > 65535 {
|
||||||
return Endpoint{}, fmt.Errorf("invalid URL endpoint format: port number must be between 1 to 65535")
|
return ep, fmt.Errorf("invalid URL endpoint format: port number must be between 1 to 65535")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if host == "" {
|
if host == "" {
|
||||||
return Endpoint{}, fmt.Errorf("invalid URL endpoint format: empty host name")
|
return ep, fmt.Errorf("invalid URL endpoint format: empty host name")
|
||||||
}
|
}
|
||||||
|
|
||||||
// As this is path in the URL, we should use path package, not filepath package.
|
// As this is path in the URL, we should use path package, not filepath package.
|
||||||
// On MS Windows, filepath.Clean() converts into Windows path style ie `/foo` becomes `\foo`
|
// On MS Windows, filepath.Clean() converts into Windows path style ie `/foo` becomes `\foo`
|
||||||
u.Path = path.Clean(u.Path)
|
u.Path = path.Clean(u.Path)
|
||||||
if isEmptyPath(u.Path) {
|
if isEmptyPath(u.Path) {
|
||||||
return Endpoint{}, fmt.Errorf("empty or root path is not supported in URL endpoint")
|
return ep, fmt.Errorf("empty or root path is not supported in URL endpoint")
|
||||||
}
|
}
|
||||||
|
|
||||||
isLocal, err = isLocalHost(host)
|
isLocal, err = isLocalHost(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Endpoint{}, err
|
return ep, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
u = &url.URL{Path: path.Clean(arg)}
|
u = &url.URL{Path: path.Clean(arg)}
|
||||||
|
|||||||
+11
-13
@@ -29,7 +29,7 @@ import (
|
|||||||
// all the disks, writes also calculate individual block's checksum
|
// all the disks, writes also calculate individual block's checksum
|
||||||
// for future bit-rot protection.
|
// for future bit-rot protection.
|
||||||
func erasureCreateFile(disks []StorageAPI, volume, path string, reader io.Reader, allowEmpty bool, blockSize int64,
|
func erasureCreateFile(disks []StorageAPI, volume, path string, reader io.Reader, allowEmpty bool, blockSize int64,
|
||||||
dataBlocks, parityBlocks int, algo HashAlgo, writeQuorum int) (bytesWritten int64, checkSums []string, err error) {
|
dataBlocks, parityBlocks int, algo HashAlgo, writeQuorum int) (newDisks []StorageAPI, bytesWritten int64, checkSums []string, err error) {
|
||||||
|
|
||||||
// Allocated blockSized buffer for reading from incoming stream.
|
// Allocated blockSized buffer for reading from incoming stream.
|
||||||
buf := make([]byte, blockSize)
|
buf := make([]byte, blockSize)
|
||||||
@@ -43,7 +43,7 @@ func erasureCreateFile(disks []StorageAPI, volume, path string, reader io.Reader
|
|||||||
// FIXME: this is a bug in Golang, n == 0 and err ==
|
// FIXME: this is a bug in Golang, n == 0 and err ==
|
||||||
// io.ErrUnexpectedEOF for io.ReadFull function.
|
// io.ErrUnexpectedEOF for io.ReadFull function.
|
||||||
if n == 0 && rErr == io.ErrUnexpectedEOF {
|
if n == 0 && rErr == io.ErrUnexpectedEOF {
|
||||||
return 0, nil, traceError(rErr)
|
return nil, 0, nil, traceError(rErr)
|
||||||
}
|
}
|
||||||
if rErr == io.EOF {
|
if rErr == io.EOF {
|
||||||
// We have reached EOF on the first byte read, io.Reader
|
// We have reached EOF on the first byte read, io.Reader
|
||||||
@@ -51,28 +51,28 @@ func erasureCreateFile(disks []StorageAPI, volume, path string, reader io.Reader
|
|||||||
// data. Will create a 0byte file instead.
|
// data. Will create a 0byte file instead.
|
||||||
if bytesWritten == 0 && allowEmpty {
|
if bytesWritten == 0 && allowEmpty {
|
||||||
blocks = make([][]byte, len(disks))
|
blocks = make([][]byte, len(disks))
|
||||||
rErr = appendFile(disks, volume, path, blocks, hashWriters, writeQuorum)
|
newDisks, rErr = appendFile(disks, volume, path, blocks, hashWriters, writeQuorum)
|
||||||
if rErr != nil {
|
if rErr != nil {
|
||||||
return 0, nil, rErr
|
return nil, 0, nil, rErr
|
||||||
}
|
}
|
||||||
} // else we have reached EOF after few reads, no need to
|
} // else we have reached EOF after few reads, no need to
|
||||||
// add an additional 0bytes at the end.
|
// add an additional 0bytes at the end.
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if rErr != nil && rErr != io.ErrUnexpectedEOF {
|
if rErr != nil && rErr != io.ErrUnexpectedEOF {
|
||||||
return 0, nil, traceError(rErr)
|
return nil, 0, nil, traceError(rErr)
|
||||||
}
|
}
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
// Returns encoded blocks.
|
// Returns encoded blocks.
|
||||||
var enErr error
|
var enErr error
|
||||||
blocks, enErr = encodeData(buf[0:n], dataBlocks, parityBlocks)
|
blocks, enErr = encodeData(buf[0:n], dataBlocks, parityBlocks)
|
||||||
if enErr != nil {
|
if enErr != nil {
|
||||||
return 0, nil, enErr
|
return nil, 0, nil, enErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to all disks.
|
// Write to all disks.
|
||||||
if err = appendFile(disks, volume, path, blocks, hashWriters, writeQuorum); err != nil {
|
if newDisks, err = appendFile(disks, volume, path, blocks, hashWriters, writeQuorum); err != nil {
|
||||||
return 0, nil, err
|
return nil, 0, nil, err
|
||||||
}
|
}
|
||||||
bytesWritten += int64(n)
|
bytesWritten += int64(n)
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ func erasureCreateFile(disks []StorageAPI, volume, path string, reader io.Reader
|
|||||||
for i := range checkSums {
|
for i := range checkSums {
|
||||||
checkSums[i] = hex.EncodeToString(hashWriters[i].Sum(nil))
|
checkSums[i] = hex.EncodeToString(hashWriters[i].Sum(nil))
|
||||||
}
|
}
|
||||||
return bytesWritten, checkSums, nil
|
return newDisks, bytesWritten, checkSums, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// encodeData - encodes incoming data buffer into
|
// encodeData - encodes incoming data buffer into
|
||||||
@@ -110,7 +110,7 @@ func encodeData(dataBuffer []byte, dataBlocks, parityBlocks int) ([][]byte, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// appendFile - append data buffer at path.
|
// appendFile - append data buffer at path.
|
||||||
func appendFile(disks []StorageAPI, volume, path string, enBlocks [][]byte, hashWriters []hash.Hash, writeQuorum int) (err error) {
|
func appendFile(disks []StorageAPI, volume, path string, enBlocks [][]byte, hashWriters []hash.Hash, writeQuorum int) ([]StorageAPI, error) {
|
||||||
var wg = &sync.WaitGroup{}
|
var wg = &sync.WaitGroup{}
|
||||||
var wErrs = make([]error, len(disks))
|
var wErrs = make([]error, len(disks))
|
||||||
// Write encoded data to quorum disks in parallel.
|
// Write encoded data to quorum disks in parallel.
|
||||||
@@ -126,8 +126,6 @@ func appendFile(disks []StorageAPI, volume, path string, enBlocks [][]byte, hash
|
|||||||
wErr := disk.AppendFile(volume, path, enBlocks[index])
|
wErr := disk.AppendFile(volume, path, enBlocks[index])
|
||||||
if wErr != nil {
|
if wErr != nil {
|
||||||
wErrs[index] = traceError(wErr)
|
wErrs[index] = traceError(wErr)
|
||||||
// Ignore disk which returned an error.
|
|
||||||
disks[index] = nil
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,5 +140,5 @@ func appendFile(disks []StorageAPI, volume, path string, enBlocks [][]byte, hash
|
|||||||
// Wait for all the appends to finish.
|
// Wait for all the appends to finish.
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
return reduceWriteQuorumErrs(wErrs, objectOpIgnoredErrs, writeQuorum)
|
return evalDisks(disks, wErrs), reduceWriteQuorumErrs(wErrs, objectOpIgnoredErrs, writeQuorum)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func TestErasureCreateFile(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Test when all disks are up.
|
// Test when all disks are up.
|
||||||
size, _, err := erasureCreateFile(disks, "testbucket", "testobject1", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
_, size, _, err := erasureCreateFile(disks, "testbucket", "testobject1", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ func TestErasureCreateFile(t *testing.T) {
|
|||||||
disks[5] = AppendDiskDown{disks[5].(*posix)}
|
disks[5] = AppendDiskDown{disks[5].(*posix)}
|
||||||
|
|
||||||
// Test when two disks are down.
|
// Test when two disks are down.
|
||||||
size, _, err = erasureCreateFile(disks, "testbucket", "testobject2", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
_, size, _, err = erasureCreateFile(disks, "testbucket", "testobject2", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ func TestErasureCreateFile(t *testing.T) {
|
|||||||
disks[8] = AppendDiskDown{disks[8].(*posix)}
|
disks[8] = AppendDiskDown{disks[8].(*posix)}
|
||||||
disks[9] = AppendDiskDown{disks[9].(*posix)}
|
disks[9] = AppendDiskDown{disks[9].(*posix)}
|
||||||
|
|
||||||
size, _, err = erasureCreateFile(disks, "testbucket", "testobject3", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
_, size, _, err = erasureCreateFile(disks, "testbucket", "testobject3", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ func TestErasureCreateFile(t *testing.T) {
|
|||||||
|
|
||||||
// 1 more disk down. 7 disk down in total. Should return quorum error.
|
// 1 more disk down. 7 disk down in total. Should return quorum error.
|
||||||
disks[10] = AppendDiskDown{disks[10].(*posix)}
|
disks[10] = AppendDiskDown{disks[10].(*posix)}
|
||||||
_, _, err = erasureCreateFile(disks, "testbucket", "testobject4", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
_, _, _, err = erasureCreateFile(disks, "testbucket", "testobject4", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||||
if errorCause(err) != errXLWriteQuorum {
|
if errorCause(err) != errXLWriteQuorum {
|
||||||
t.Errorf("erasureCreateFile return value: expected errXLWriteQuorum, got %s", err)
|
t.Errorf("erasureCreateFile return value: expected errXLWriteQuorum, got %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func TestErasureHealFile(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Create a test file.
|
// Create a test file.
|
||||||
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject1", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
_, size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject1", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ func TestErasureReadFileDiskFail(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a test file to read from.
|
// Create a test file to read from.
|
||||||
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
_, size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -325,7 +325,7 @@ func TestErasureReadFileOffsetLength(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a test file to read from.
|
// Create a test file to read from.
|
||||||
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
_, size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -404,7 +404,7 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
|||||||
iterations := 10000
|
iterations := 10000
|
||||||
|
|
||||||
// Create a test file to read from.
|
// Create a test file to read from.
|
||||||
size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
_, size, checkSums, err := erasureCreateFile(disks, "testbucket", "testobject", bytes.NewReader(data), true, blockSize, dataBlocks, parityBlocks, bitRotAlgo, dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-6
@@ -503,9 +503,8 @@ func removeNotificationConfig(bucket string, objAPI ObjectLayer) error {
|
|||||||
// Acquire a write lock on notification config before modifying.
|
// Acquire a write lock on notification config before modifying.
|
||||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, ncPath)
|
objLock := globalNSMutex.NewNSLock(minioMetaBucket, ncPath)
|
||||||
objLock.Lock()
|
objLock.Lock()
|
||||||
err := objAPI.DeleteObject(minioMetaBucket, ncPath)
|
defer objLock.Unlock()
|
||||||
objLock.Unlock()
|
return objAPI.DeleteObject(minioMetaBucket, ncPath)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove listener configuration from storage layer. Used when a bucket is deleted.
|
// Remove listener configuration from storage layer. Used when a bucket is deleted.
|
||||||
@@ -516,9 +515,8 @@ func removeListenerConfig(bucket string, objAPI ObjectLayer) error {
|
|||||||
// Acquire a write lock on notification config before modifying.
|
// Acquire a write lock on notification config before modifying.
|
||||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, lcPath)
|
objLock := globalNSMutex.NewNSLock(minioMetaBucket, lcPath)
|
||||||
objLock.Lock()
|
objLock.Lock()
|
||||||
err := objAPI.DeleteObject(minioMetaBucket, lcPath)
|
defer objLock.Unlock()
|
||||||
objLock.Unlock()
|
return objAPI.DeleteObject(minioMetaBucket, lcPath)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads both notification and listener config.
|
// Loads both notification and listener config.
|
||||||
@@ -609,6 +607,25 @@ func loadAllQueueTargets() (map[string]*logrus.Logger, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load all mqtt targets, initialize their respective loggers.
|
||||||
|
for accountID, mqttN := range serverConfig.Notify.GetMQTT() {
|
||||||
|
if !mqttN.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if queueARN, err := addQueueTarget(queueTargets, accountID, queueTypeMQTT, newMQTTNotify); err != nil {
|
||||||
|
if _, ok := err.(net.Error); ok {
|
||||||
|
err = &net.OpError{
|
||||||
|
Op: "Connecting to " + queueARN,
|
||||||
|
Net: "tcp",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load all nats targets, initialize their respective loggers.
|
// Load all nats targets, initialize their respective loggers.
|
||||||
for accountID, natsN := range serverConfig.Notify.GetNATS() {
|
for accountID, natsN := range serverConfig.Notify.GetNATS() {
|
||||||
if !natsN.Enable {
|
if !natsN.Enable {
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ func TestInitEventNotifier(t *testing.T) {
|
|||||||
filterRules := []filterRule{
|
filterRules := []filterRule{
|
||||||
{
|
{
|
||||||
Name: "prefix",
|
Name: "prefix",
|
||||||
Value: globalMinioDefaultOwnerID,
|
Value: "minio",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "suffix",
|
Name: "suffix",
|
||||||
@@ -535,7 +535,7 @@ func TestAddRemoveBucketListenerConfig(t *testing.T) {
|
|||||||
filterRules := []filterRule{
|
filterRules := []filterRule{
|
||||||
{
|
{
|
||||||
Name: "prefix",
|
Name: "prefix",
|
||||||
Value: globalMinioDefaultOwnerID,
|
Value: "minio",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "suffix",
|
Name: "suffix",
|
||||||
|
|||||||
+80
-78
@@ -159,7 +159,7 @@ func (fs fsObjects) listMultipartUploadIDs(bucketName, objectName, uploadIDMarke
|
|||||||
}
|
}
|
||||||
|
|
||||||
// listMultipartUploads - lists all multipart uploads.
|
// listMultipartUploads - lists all multipart uploads.
|
||||||
func (fs fsObjects) listMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (ListMultipartsInfo, error) {
|
func (fs fsObjects) listMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (lmi ListMultipartsInfo, e error) {
|
||||||
result := ListMultipartsInfo{}
|
result := ListMultipartsInfo{}
|
||||||
recursive := true
|
recursive := true
|
||||||
if delimiter == slashSeparator {
|
if delimiter == slashSeparator {
|
||||||
@@ -191,7 +191,7 @@ func (fs fsObjects) listMultipartUploads(bucket, prefix, keyMarker, uploadIDMark
|
|||||||
if uploadIDMarker != "" {
|
if uploadIDMarker != "" {
|
||||||
uploads, _, err = fs.listMultipartUploadIDs(bucket, keyMarker, uploadIDMarker, maxUploads)
|
uploads, _, err = fs.listMultipartUploadIDs(bucket, keyMarker, uploadIDMarker, maxUploads)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListMultipartsInfo{}, err
|
return lmi, err
|
||||||
}
|
}
|
||||||
maxUploads = maxUploads - len(uploads)
|
maxUploads = maxUploads - len(uploads)
|
||||||
}
|
}
|
||||||
@@ -232,7 +232,7 @@ func (fs fsObjects) listMultipartUploads(bucket, prefix, keyMarker, uploadIDMark
|
|||||||
eof = true
|
eof = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return ListMultipartsInfo{}, walkResult.err
|
return lmi, walkResult.err
|
||||||
}
|
}
|
||||||
|
|
||||||
entry := strings.TrimPrefix(walkResult.entry, retainSlash(bucket))
|
entry := strings.TrimPrefix(walkResult.entry, retainSlash(bucket))
|
||||||
@@ -256,7 +256,7 @@ func (fs fsObjects) listMultipartUploads(bucket, prefix, keyMarker, uploadIDMark
|
|||||||
|
|
||||||
tmpUploads, end, err = fs.listMultipartUploadIDs(bucket, entry, uploadIDMarker, maxUploads)
|
tmpUploads, end, err = fs.listMultipartUploadIDs(bucket, entry, uploadIDMarker, maxUploads)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListMultipartsInfo{}, err
|
return lmi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
uploads = append(uploads, tmpUploads...)
|
uploads = append(uploads, tmpUploads...)
|
||||||
@@ -311,13 +311,13 @@ func (fs fsObjects) listMultipartUploads(bucket, prefix, keyMarker, uploadIDMark
|
|||||||
// Implements S3 compatible ListMultipartUploads API. The resulting
|
// Implements S3 compatible ListMultipartUploads API. The resulting
|
||||||
// ListMultipartsInfo structure is unmarshalled directly into XML and
|
// ListMultipartsInfo structure is unmarshalled directly into XML and
|
||||||
// replied back to the client.
|
// replied back to the client.
|
||||||
func (fs fsObjects) ListMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (ListMultipartsInfo, error) {
|
func (fs fsObjects) ListMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (lmi ListMultipartsInfo, e error) {
|
||||||
if err := checkListMultipartArgs(bucket, prefix, keyMarker, uploadIDMarker, delimiter, fs); err != nil {
|
if err := checkListMultipartArgs(bucket, prefix, keyMarker, uploadIDMarker, delimiter, fs); err != nil {
|
||||||
return ListMultipartsInfo{}, err
|
return lmi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fs.statBucketDir(bucket); err != nil {
|
if _, err := fs.statBucketDir(bucket); err != nil {
|
||||||
return ListMultipartsInfo{}, toObjectErr(err, bucket)
|
return lmi, toObjectErr(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fs.listMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
return fs.listMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||||
@@ -412,9 +412,9 @@ func partToAppend(fsMeta fsMetaV1, fsAppendMeta fsMetaV1) (part objectPartInfo,
|
|||||||
// CopyObjectPart - similar to PutObjectPart but reads data from an existing
|
// CopyObjectPart - similar to PutObjectPart but reads data from an existing
|
||||||
// object. Internally incoming data is written to '.minio.sys/tmp' location
|
// object. Internally incoming data is written to '.minio.sys/tmp' location
|
||||||
// and safely renamed to '.minio.sys/multipart' for reach parts.
|
// and safely renamed to '.minio.sys/multipart' for reach parts.
|
||||||
func (fs fsObjects) CopyObjectPart(srcBucket, srcObject, dstBucket, dstObject, uploadID string, partID int, startOffset int64, length int64) (PartInfo, error) {
|
func (fs fsObjects) CopyObjectPart(srcBucket, srcObject, dstBucket, dstObject, uploadID string, partID int, startOffset int64, length int64) (pi PartInfo, e error) {
|
||||||
if err := checkNewMultipartArgs(srcBucket, srcObject, fs); err != nil {
|
if err := checkNewMultipartArgs(srcBucket, srcObject, fs); err != nil {
|
||||||
return PartInfo{}, err
|
return pi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize pipe.
|
// Initialize pipe.
|
||||||
@@ -431,7 +431,7 @@ func (fs fsObjects) CopyObjectPart(srcBucket, srcObject, dstBucket, dstObject, u
|
|||||||
|
|
||||||
partInfo, err := fs.PutObjectPart(dstBucket, dstObject, uploadID, partID, length, pipeReader, "", "")
|
partInfo, err := fs.PutObjectPart(dstBucket, dstObject, uploadID, partID, length, pipeReader, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PartInfo{}, toObjectErr(err, dstBucket, dstObject)
|
return pi, toObjectErr(err, dstBucket, dstObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicitly close the reader.
|
// Explicitly close the reader.
|
||||||
@@ -444,13 +444,13 @@ func (fs fsObjects) CopyObjectPart(srcBucket, srcObject, dstBucket, dstObject, u
|
|||||||
// an ongoing multipart transaction. Internally incoming data is
|
// an ongoing multipart transaction. Internally incoming data is
|
||||||
// written to '.minio.sys/tmp' location and safely renamed to
|
// written to '.minio.sys/tmp' location and safely renamed to
|
||||||
// '.minio.sys/multipart' for reach parts.
|
// '.minio.sys/multipart' for reach parts.
|
||||||
func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, size int64, data io.Reader, md5Hex string, sha256sum string) (PartInfo, error) {
|
func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, size int64, data io.Reader, md5Hex string, sha256sum string) (pi PartInfo, e error) {
|
||||||
if err := checkPutObjectPartArgs(bucket, object, fs); err != nil {
|
if err := checkPutObjectPartArgs(bucket, object, fs); err != nil {
|
||||||
return PartInfo{}, err
|
return pi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fs.statBucketDir(bucket); err != nil {
|
if _, err := fs.statBucketDir(bucket); err != nil {
|
||||||
return PartInfo{}, toObjectErr(err, bucket)
|
return pi, toObjectErr(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hold the lock so that two parallel complete-multipart-uploads
|
// Hold the lock so that two parallel complete-multipart-uploads
|
||||||
@@ -463,9 +463,9 @@ func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, s
|
|||||||
uploadsPath := pathJoin(fs.fsPath, minioMetaMultipartBucket, bucket, object, uploadsJSONFile)
|
uploadsPath := pathJoin(fs.fsPath, minioMetaMultipartBucket, bucket, object, uploadsJSONFile)
|
||||||
if _, err := fs.rwPool.Open(uploadsPath); err != nil {
|
if _, err := fs.rwPool.Open(uploadsPath); err != nil {
|
||||||
if err == errFileNotFound || err == errFileAccessDenied {
|
if err == errFileNotFound || err == errFileAccessDenied {
|
||||||
return PartInfo{}, traceError(InvalidUploadID{UploadID: uploadID})
|
return pi, traceError(InvalidUploadID{UploadID: uploadID})
|
||||||
}
|
}
|
||||||
return PartInfo{}, toObjectErr(traceError(err), bucket, object)
|
return pi, toObjectErr(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
defer fs.rwPool.Close(uploadsPath)
|
defer fs.rwPool.Close(uploadsPath)
|
||||||
|
|
||||||
@@ -476,16 +476,16 @@ func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, s
|
|||||||
rwlk, err := fs.rwPool.Write(fsMetaPath)
|
rwlk, err := fs.rwPool.Write(fsMetaPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == errFileNotFound || err == errFileAccessDenied {
|
if err == errFileNotFound || err == errFileAccessDenied {
|
||||||
return PartInfo{}, traceError(InvalidUploadID{UploadID: uploadID})
|
return pi, traceError(InvalidUploadID{UploadID: uploadID})
|
||||||
}
|
}
|
||||||
return PartInfo{}, toObjectErr(traceError(err), bucket, object)
|
return pi, toObjectErr(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
defer rwlk.Close()
|
defer rwlk.Close()
|
||||||
|
|
||||||
fsMeta := fsMetaV1{}
|
fsMeta := fsMetaV1{}
|
||||||
_, err = fsMeta.ReadFrom(rwlk)
|
_, err = fsMeta.ReadFrom(rwlk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PartInfo{}, toObjectErr(err, minioMetaMultipartBucket, fsMetaPath)
|
return pi, toObjectErr(err, minioMetaMultipartBucket, fsMetaPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
partSuffix := fmt.Sprintf("object%d", partID)
|
partSuffix := fmt.Sprintf("object%d", partID)
|
||||||
@@ -523,14 +523,14 @@ func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, s
|
|||||||
bytesWritten, cErr := fsCreateFile(fsPartPath, teeReader, buf, size)
|
bytesWritten, cErr := fsCreateFile(fsPartPath, teeReader, buf, size)
|
||||||
if cErr != nil {
|
if cErr != nil {
|
||||||
fsRemoveFile(fsPartPath)
|
fsRemoveFile(fsPartPath)
|
||||||
return PartInfo{}, toObjectErr(cErr, minioMetaTmpBucket, tmpPartPath)
|
return pi, toObjectErr(cErr, minioMetaTmpBucket, tmpPartPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should return IncompleteBody{} error when reader has fewer
|
// Should return IncompleteBody{} error when reader has fewer
|
||||||
// bytes than specified in request header.
|
// bytes than specified in request header.
|
||||||
if bytesWritten < size {
|
if bytesWritten < size {
|
||||||
fsRemoveFile(fsPartPath)
|
fsRemoveFile(fsPartPath)
|
||||||
return PartInfo{}, traceError(IncompleteBody{})
|
return pi, traceError(IncompleteBody{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete temporary part in case of failure. If
|
// Delete temporary part in case of failure. If
|
||||||
@@ -541,14 +541,14 @@ func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, s
|
|||||||
newMD5Hex := hex.EncodeToString(md5Writer.Sum(nil))
|
newMD5Hex := hex.EncodeToString(md5Writer.Sum(nil))
|
||||||
if md5Hex != "" {
|
if md5Hex != "" {
|
||||||
if newMD5Hex != md5Hex {
|
if newMD5Hex != md5Hex {
|
||||||
return PartInfo{}, traceError(BadDigest{md5Hex, newMD5Hex})
|
return pi, traceError(BadDigest{md5Hex, newMD5Hex})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sha256sum != "" {
|
if sha256sum != "" {
|
||||||
newSHA256sum := hex.EncodeToString(sha256Writer.Sum(nil))
|
newSHA256sum := hex.EncodeToString(sha256Writer.Sum(nil))
|
||||||
if newSHA256sum != sha256sum {
|
if newSHA256sum != sha256sum {
|
||||||
return PartInfo{}, traceError(SHA256Mismatch{})
|
return pi, traceError(SHA256Mismatch{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,20 +561,20 @@ func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, s
|
|||||||
fsNSPartPath := pathJoin(fs.fsPath, minioMetaMultipartBucket, partPath)
|
fsNSPartPath := pathJoin(fs.fsPath, minioMetaMultipartBucket, partPath)
|
||||||
if err = fsRenameFile(fsPartPath, fsNSPartPath); err != nil {
|
if err = fsRenameFile(fsPartPath, fsNSPartPath); err != nil {
|
||||||
partLock.Unlock()
|
partLock.Unlock()
|
||||||
return PartInfo{}, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the object part info in `fs.json`.
|
// Save the object part info in `fs.json`.
|
||||||
fsMeta.AddObjectPart(partID, partSuffix, newMD5Hex, size)
|
fsMeta.AddObjectPart(partID, partSuffix, newMD5Hex, size)
|
||||||
if _, err = fsMeta.WriteTo(rwlk); err != nil {
|
if _, err = fsMeta.WriteTo(rwlk); err != nil {
|
||||||
partLock.Unlock()
|
partLock.Unlock()
|
||||||
return PartInfo{}, toObjectErr(err, minioMetaMultipartBucket, uploadIDPath)
|
return pi, toObjectErr(err, minioMetaMultipartBucket, uploadIDPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
partNamePath := pathJoin(fs.fsPath, minioMetaMultipartBucket, uploadIDPath, partSuffix)
|
partNamePath := pathJoin(fs.fsPath, minioMetaMultipartBucket, uploadIDPath, partSuffix)
|
||||||
fi, err := fsStatFile(partNamePath)
|
fi, err := fsStatFile(partNamePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PartInfo{}, toObjectErr(err, minioMetaMultipartBucket, partSuffix)
|
return pi, toObjectErr(err, minioMetaMultipartBucket, partSuffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append the part in background.
|
// Append the part in background.
|
||||||
@@ -599,7 +599,7 @@ func (fs fsObjects) PutObjectPart(bucket, object, uploadID string, partID int, s
|
|||||||
// listObjectParts - wrapper scanning through
|
// listObjectParts - wrapper scanning through
|
||||||
// '.minio.sys/multipart/bucket/object/UPLOADID'. Lists all the parts
|
// '.minio.sys/multipart/bucket/object/UPLOADID'. Lists all the parts
|
||||||
// saved inside '.minio.sys/multipart/bucket/object/UPLOADID'.
|
// saved inside '.minio.sys/multipart/bucket/object/UPLOADID'.
|
||||||
func (fs fsObjects) listObjectParts(bucket, object, uploadID string, partNumberMarker, maxParts int) (ListPartsInfo, error) {
|
func (fs fsObjects) listObjectParts(bucket, object, uploadID string, partNumberMarker, maxParts int) (lpi ListPartsInfo, e error) {
|
||||||
result := ListPartsInfo{}
|
result := ListPartsInfo{}
|
||||||
|
|
||||||
uploadIDPath := pathJoin(bucket, object, uploadID)
|
uploadIDPath := pathJoin(bucket, object, uploadID)
|
||||||
@@ -608,16 +608,16 @@ func (fs fsObjects) listObjectParts(bucket, object, uploadID string, partNumberM
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if err == errFileNotFound || err == errFileAccessDenied {
|
if err == errFileNotFound || err == errFileAccessDenied {
|
||||||
// On windows oddly this is returned.
|
// On windows oddly this is returned.
|
||||||
return ListPartsInfo{}, traceError(InvalidUploadID{UploadID: uploadID})
|
return lpi, traceError(InvalidUploadID{UploadID: uploadID})
|
||||||
}
|
}
|
||||||
return ListPartsInfo{}, toObjectErr(traceError(err), bucket, object)
|
return lpi, toObjectErr(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
defer fs.rwPool.Close(fsMetaPath)
|
defer fs.rwPool.Close(fsMetaPath)
|
||||||
|
|
||||||
fsMeta := fsMetaV1{}
|
fsMeta := fsMetaV1{}
|
||||||
_, err = fsMeta.ReadFrom(metaFile.LockedFile)
|
_, err = fsMeta.ReadFrom(metaFile.LockedFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListPartsInfo{}, toObjectErr(err, minioMetaBucket, fsMetaPath)
|
return lpi, toObjectErr(err, minioMetaBucket, fsMetaPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only parts with higher part numbers will be listed.
|
// Only parts with higher part numbers will be listed.
|
||||||
@@ -633,7 +633,7 @@ func (fs fsObjects) listObjectParts(bucket, object, uploadID string, partNumberM
|
|||||||
partNamePath := pathJoin(fs.fsPath, minioMetaMultipartBucket, uploadIDPath, part.Name)
|
partNamePath := pathJoin(fs.fsPath, minioMetaMultipartBucket, uploadIDPath, part.Name)
|
||||||
fi, err = fsStatFile(partNamePath)
|
fi, err = fsStatFile(partNamePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListPartsInfo{}, toObjectErr(err, minioMetaMultipartBucket, partNamePath)
|
return lpi, toObjectErr(err, minioMetaMultipartBucket, partNamePath)
|
||||||
}
|
}
|
||||||
result.Parts = append(result.Parts, PartInfo{
|
result.Parts = append(result.Parts, PartInfo{
|
||||||
PartNumber: part.Number,
|
PartNumber: part.Number,
|
||||||
@@ -671,13 +671,13 @@ func (fs fsObjects) listObjectParts(bucket, object, uploadID string, partNumberM
|
|||||||
// Implements S3 compatible ListObjectParts API. The resulting
|
// Implements S3 compatible ListObjectParts API. The resulting
|
||||||
// ListPartsInfo structure is unmarshalled directly into XML and
|
// ListPartsInfo structure is unmarshalled directly into XML and
|
||||||
// replied back to the client.
|
// replied back to the client.
|
||||||
func (fs fsObjects) ListObjectParts(bucket, object, uploadID string, partNumberMarker, maxParts int) (ListPartsInfo, error) {
|
func (fs fsObjects) ListObjectParts(bucket, object, uploadID string, partNumberMarker, maxParts int) (lpi ListPartsInfo, e error) {
|
||||||
if err := checkListPartsArgs(bucket, object, fs); err != nil {
|
if err := checkListPartsArgs(bucket, object, fs); err != nil {
|
||||||
return ListPartsInfo{}, err
|
return lpi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fs.statBucketDir(bucket); err != nil {
|
if _, err := fs.statBucketDir(bucket); err != nil {
|
||||||
return ListPartsInfo{}, toObjectErr(err, bucket)
|
return lpi, toObjectErr(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hold the lock so that two parallel complete-multipart-uploads
|
// Hold the lock so that two parallel complete-multipart-uploads
|
||||||
@@ -688,7 +688,7 @@ func (fs fsObjects) ListObjectParts(bucket, object, uploadID string, partNumberM
|
|||||||
|
|
||||||
listPartsInfo, err := fs.listObjectParts(bucket, object, uploadID, partNumberMarker, maxParts)
|
listPartsInfo, err := fs.listObjectParts(bucket, object, uploadID, partNumberMarker, maxParts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListPartsInfo{}, toObjectErr(err, bucket, object)
|
return lpi, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
@@ -701,24 +701,24 @@ func (fs fsObjects) ListObjectParts(bucket, object, uploadID string, partNumberM
|
|||||||
// md5sums of all the parts.
|
// md5sums of all the parts.
|
||||||
//
|
//
|
||||||
// Implements S3 compatible Complete multipart API.
|
// Implements S3 compatible Complete multipart API.
|
||||||
func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, uploadID string, parts []completePart) (ObjectInfo, error) {
|
func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, uploadID string, parts []completePart) (oi ObjectInfo, e error) {
|
||||||
if err := checkCompleteMultipartArgs(bucket, object, fs); err != nil {
|
if err := checkCompleteMultipartArgs(bucket, object, fs); err != nil {
|
||||||
return ObjectInfo{}, err
|
return oi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if an object is present as one of the parent dir.
|
// Check if an object is present as one of the parent dir.
|
||||||
if fs.parentDirIsObject(bucket, pathutil.Dir(object)) {
|
if fs.parentDirIsObject(bucket, pathutil.Dir(object)) {
|
||||||
return ObjectInfo{}, toObjectErr(traceError(errFileAccessDenied), bucket, object)
|
return oi, toObjectErr(traceError(errFileAccessDenied), bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fs.statBucketDir(bucket); err != nil {
|
if _, err := fs.statBucketDir(bucket); err != nil {
|
||||||
return ObjectInfo{}, toObjectErr(err, bucket)
|
return oi, toObjectErr(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate s3 compatible md5sum for complete multipart.
|
// Calculate s3 compatible md5sum for complete multipart.
|
||||||
s3MD5, err := getCompleteMultipartMD5(parts)
|
s3MD5, err := getCompleteMultipartMD5(parts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, err
|
return oi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadIDPath := pathJoin(bucket, object, uploadID)
|
uploadIDPath := pathJoin(bucket, object, uploadID)
|
||||||
@@ -733,9 +733,9 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
|||||||
rlk, err := fs.rwPool.Open(fsMetaPathMultipart)
|
rlk, err := fs.rwPool.Open(fsMetaPathMultipart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == errFileNotFound || err == errFileAccessDenied {
|
if err == errFileNotFound || err == errFileAccessDenied {
|
||||||
return ObjectInfo{}, traceError(InvalidUploadID{UploadID: uploadID})
|
return oi, traceError(InvalidUploadID{UploadID: uploadID})
|
||||||
}
|
}
|
||||||
return ObjectInfo{}, toObjectErr(traceError(err), bucket, object)
|
return oi, toObjectErr(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disallow any parallel abort or complete multipart operations.
|
// Disallow any parallel abort or complete multipart operations.
|
||||||
@@ -743,9 +743,9 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
fs.rwPool.Close(fsMetaPathMultipart)
|
fs.rwPool.Close(fsMetaPathMultipart)
|
||||||
if err == errFileNotFound || err == errFileAccessDenied {
|
if err == errFileNotFound || err == errFileAccessDenied {
|
||||||
return ObjectInfo{}, traceError(InvalidUploadID{UploadID: uploadID})
|
return oi, traceError(InvalidUploadID{UploadID: uploadID})
|
||||||
}
|
}
|
||||||
return ObjectInfo{}, toObjectErr(traceError(err), bucket, object)
|
return oi, toObjectErr(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
defer rwlk.Close()
|
defer rwlk.Close()
|
||||||
|
|
||||||
@@ -754,7 +754,31 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
|||||||
_, err = fsMeta.ReadFrom(rlk.LockedFile)
|
_, err = fsMeta.ReadFrom(rlk.LockedFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.rwPool.Close(fsMetaPathMultipart)
|
fs.rwPool.Close(fsMetaPathMultipart)
|
||||||
return ObjectInfo{}, toObjectErr(err, minioMetaMultipartBucket, fsMetaPathMultipart)
|
return oi, toObjectErr(err, minioMetaMultipartBucket, fsMetaPathMultipart)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all parts and then commit to disk.
|
||||||
|
for i, part := range parts {
|
||||||
|
partIdx := fsMeta.ObjectPartIndex(part.PartNumber)
|
||||||
|
if partIdx == -1 {
|
||||||
|
fs.rwPool.Close(fsMetaPathMultipart)
|
||||||
|
return oi, traceError(InvalidPart{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if fsMeta.Parts[partIdx].ETag != part.ETag {
|
||||||
|
fs.rwPool.Close(fsMetaPathMultipart)
|
||||||
|
return oi, traceError(InvalidPart{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// All parts except the last part has to be atleast 5MB.
|
||||||
|
if (i < len(parts)-1) && !isMinAllowedPartSize(fsMeta.Parts[partIdx].Size) {
|
||||||
|
fs.rwPool.Close(fsMetaPathMultipart)
|
||||||
|
return oi, traceError(PartTooSmall{
|
||||||
|
PartNumber: part.PartNumber,
|
||||||
|
PartSize: fsMeta.Parts[partIdx].Size,
|
||||||
|
PartETag: part.ETag,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for any competing PutObject() operation on bucket/object, since same namespace
|
// Wait for any competing PutObject() operation on bucket/object, since same namespace
|
||||||
@@ -763,7 +787,7 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
|||||||
metaFile, err := fs.rwPool.Create(fsMetaPath)
|
metaFile, err := fs.rwPool.Create(fsMetaPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.rwPool.Close(fsMetaPathMultipart)
|
fs.rwPool.Close(fsMetaPathMultipart)
|
||||||
return ObjectInfo{}, toObjectErr(traceError(err), bucket, object)
|
return oi, toObjectErr(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
defer metaFile.Close()
|
defer metaFile.Close()
|
||||||
|
|
||||||
@@ -780,7 +804,7 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
|||||||
fsTmpObjPath := pathJoin(fs.fsPath, minioMetaTmpBucket, fs.fsUUID, uploadID)
|
fsTmpObjPath := pathJoin(fs.fsPath, minioMetaTmpBucket, fs.fsUUID, uploadID)
|
||||||
if err = fsRenameFile(fsTmpObjPath, fsNSObjPath); err != nil {
|
if err = fsRenameFile(fsTmpObjPath, fsNSObjPath); err != nil {
|
||||||
fs.rwPool.Close(fsMetaPathMultipart)
|
fs.rwPool.Close(fsMetaPathMultipart)
|
||||||
return ObjectInfo{}, toObjectErr(err, minioMetaTmpBucket, uploadID)
|
return oi, toObjectErr(err, minioMetaTmpBucket, uploadID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -798,29 +822,7 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
|||||||
// Allocate staging buffer.
|
// Allocate staging buffer.
|
||||||
var buf = make([]byte, readSizeV1)
|
var buf = make([]byte, readSizeV1)
|
||||||
|
|
||||||
// Validate all parts and then commit to disk.
|
for _, part := range parts {
|
||||||
for i, part := range parts {
|
|
||||||
partIdx := fsMeta.ObjectPartIndex(part.PartNumber)
|
|
||||||
if partIdx == -1 {
|
|
||||||
fs.rwPool.Close(fsMetaPathMultipart)
|
|
||||||
return ObjectInfo{}, traceError(InvalidPart{})
|
|
||||||
}
|
|
||||||
|
|
||||||
if fsMeta.Parts[partIdx].ETag != part.ETag {
|
|
||||||
fs.rwPool.Close(fsMetaPathMultipart)
|
|
||||||
return ObjectInfo{}, traceError(BadDigest{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// All parts except the last part has to be atleast 5MB.
|
|
||||||
if (i < len(parts)-1) && !isMinAllowedPartSize(fsMeta.Parts[partIdx].Size) {
|
|
||||||
fs.rwPool.Close(fsMetaPathMultipart)
|
|
||||||
return ObjectInfo{}, traceError(PartTooSmall{
|
|
||||||
PartNumber: part.PartNumber,
|
|
||||||
PartSize: fsMeta.Parts[partIdx].Size,
|
|
||||||
PartETag: part.ETag,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct part suffix.
|
// Construct part suffix.
|
||||||
partSuffix := fmt.Sprintf("object%d", part.PartNumber)
|
partSuffix := fmt.Sprintf("object%d", part.PartNumber)
|
||||||
multipartPartFile := pathJoin(fs.fsPath, minioMetaMultipartBucket, uploadIDPath, partSuffix)
|
multipartPartFile := pathJoin(fs.fsPath, minioMetaMultipartBucket, uploadIDPath, partSuffix)
|
||||||
@@ -831,9 +833,9 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
fs.rwPool.Close(fsMetaPathMultipart)
|
fs.rwPool.Close(fsMetaPathMultipart)
|
||||||
if err == errFileNotFound {
|
if err == errFileNotFound {
|
||||||
return ObjectInfo{}, traceError(InvalidPart{})
|
return oi, traceError(InvalidPart{})
|
||||||
}
|
}
|
||||||
return ObjectInfo{}, toObjectErr(traceError(err), minioMetaMultipartBucket, partSuffix)
|
return oi, toObjectErr(traceError(err), minioMetaMultipartBucket, partSuffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to hold a lock, this is a unique file and will be only written
|
// No need to hold a lock, this is a unique file and will be only written
|
||||||
@@ -843,7 +845,7 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
reader.Close()
|
reader.Close()
|
||||||
fs.rwPool.Close(fsMetaPathMultipart)
|
fs.rwPool.Close(fsMetaPathMultipart)
|
||||||
return ObjectInfo{}, toObjectErr(traceError(err), bucket, object)
|
return oi, toObjectErr(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.CopyBuffer(wfile, reader, buf)
|
_, err = io.CopyBuffer(wfile, reader, buf)
|
||||||
@@ -851,7 +853,7 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
|||||||
wfile.Close()
|
wfile.Close()
|
||||||
reader.Close()
|
reader.Close()
|
||||||
fs.rwPool.Close(fsMetaPathMultipart)
|
fs.rwPool.Close(fsMetaPathMultipart)
|
||||||
return ObjectInfo{}, toObjectErr(traceError(err), bucket, object)
|
return oi, toObjectErr(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
wfile.Close()
|
wfile.Close()
|
||||||
@@ -860,7 +862,7 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
|||||||
|
|
||||||
if err = fsRenameFile(fsTmpObjPath, fsNSObjPath); err != nil {
|
if err = fsRenameFile(fsTmpObjPath, fsNSObjPath); err != nil {
|
||||||
fs.rwPool.Close(fsMetaPathMultipart)
|
fs.rwPool.Close(fsMetaPathMultipart)
|
||||||
return ObjectInfo{}, toObjectErr(err, minioMetaTmpBucket, uploadID)
|
return oi, toObjectErr(err, minioMetaTmpBucket, uploadID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -876,7 +878,7 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
|||||||
// Write all the set metadata.
|
// Write all the set metadata.
|
||||||
if _, err = fsMeta.WriteTo(metaFile); err != nil {
|
if _, err = fsMeta.WriteTo(metaFile); err != nil {
|
||||||
fs.rwPool.Close(fsMetaPathMultipart)
|
fs.rwPool.Close(fsMetaPathMultipart)
|
||||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
return oi, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close lock held on bucket/object/uploadid/fs.json,
|
// Close lock held on bucket/object/uploadid/fs.json,
|
||||||
@@ -888,17 +890,17 @@ func (fs fsObjects) CompleteMultipartUpload(bucket string, object string, upload
|
|||||||
multipartObjectDir := pathJoin(fs.fsPath, minioMetaMultipartBucket, bucket, object)
|
multipartObjectDir := pathJoin(fs.fsPath, minioMetaMultipartBucket, bucket, object)
|
||||||
multipartUploadIDDir := pathJoin(multipartObjectDir, uploadID)
|
multipartUploadIDDir := pathJoin(multipartObjectDir, uploadID)
|
||||||
if err = fsRemoveUploadIDPath(multipartObjectDir, multipartUploadIDDir); err != nil {
|
if err = fsRemoveUploadIDPath(multipartObjectDir, multipartUploadIDDir); err != nil {
|
||||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
return oi, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove entry from `uploads.json`.
|
// Remove entry from `uploads.json`.
|
||||||
if err = fs.removeUploadID(bucket, object, uploadID, rwlk); err != nil {
|
if err = fs.removeUploadID(bucket, object, uploadID, rwlk); err != nil {
|
||||||
return ObjectInfo{}, toObjectErr(err, minioMetaMultipartBucket, pathutil.Join(bucket, object))
|
return oi, toObjectErr(err, minioMetaMultipartBucket, pathutil.Join(bucket, object))
|
||||||
}
|
}
|
||||||
|
|
||||||
fi, err := fsStatFile(fsNSObjPath)
|
fi, err := fsStatFile(fsNSObjPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
return oi, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return object info.
|
// Return object info.
|
||||||
|
|||||||
+39
-31
@@ -103,6 +103,16 @@ func newFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
di, err := getDiskInfo(preparePath(fsPath))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if disk has minimum required total space.
|
||||||
|
if err = checkDiskMinTotal(di); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Assign a new UUID for FS minio mode. Each server instance
|
// Assign a new UUID for FS minio mode. Each server instance
|
||||||
// gets its own UUID for temporary file transaction.
|
// gets its own UUID for temporary file transaction.
|
||||||
fsUUID := mustGetUUID()
|
fsUUID := mustGetUUID()
|
||||||
@@ -138,14 +148,12 @@ func newFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
|||||||
fs.fsFormatRlk = rlk
|
fs.fsFormatRlk = rlk
|
||||||
|
|
||||||
// Initialize and load bucket policies.
|
// Initialize and load bucket policies.
|
||||||
err = initBucketPolicies(fs)
|
if err = initBucketPolicies(fs); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Unable to load all bucket policies. %s", err)
|
return nil, fmt.Errorf("Unable to load all bucket policies. %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a new event notifier.
|
// Initialize a new event notifier.
|
||||||
err = initEventNotifier(fs)
|
if err = initEventNotifier(fs); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Unable to initialize event notification. %s", err)
|
return nil, fmt.Errorf("Unable to initialize event notification. %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,10 +222,10 @@ func (fs fsObjects) MakeBucketWithLocation(bucket, location string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketInfo - fetch bucket metadata info.
|
// GetBucketInfo - fetch bucket metadata info.
|
||||||
func (fs fsObjects) GetBucketInfo(bucket string) (BucketInfo, error) {
|
func (fs fsObjects) GetBucketInfo(bucket string) (bi BucketInfo, e error) {
|
||||||
st, err := fs.statBucketDir(bucket)
|
st, err := fs.statBucketDir(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return BucketInfo{}, toObjectErr(err, bucket)
|
return bi, toObjectErr(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// As osStat() doesn't carry other than ModTime(), use ModTime() as CreatedTime.
|
// As osStat() doesn't carry other than ModTime(), use ModTime() as CreatedTime.
|
||||||
@@ -304,15 +312,15 @@ func (fs fsObjects) DeleteBucket(bucket string) error {
|
|||||||
// CopyObject - copy object source object to destination object.
|
// CopyObject - copy object source object to destination object.
|
||||||
// if source object and destination object are same we only
|
// if source object and destination object are same we only
|
||||||
// update metadata.
|
// update metadata.
|
||||||
func (fs fsObjects) CopyObject(srcBucket, srcObject, dstBucket, dstObject string, metadata map[string]string) (ObjectInfo, error) {
|
func (fs fsObjects) CopyObject(srcBucket, srcObject, dstBucket, dstObject string, metadata map[string]string) (oi ObjectInfo, e error) {
|
||||||
if _, err := fs.statBucketDir(srcBucket); err != nil {
|
if _, err := fs.statBucketDir(srcBucket); err != nil {
|
||||||
return ObjectInfo{}, toObjectErr(err, srcBucket)
|
return oi, toObjectErr(err, srcBucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stat the file to get file size.
|
// Stat the file to get file size.
|
||||||
fi, err := fsStatFile(pathJoin(fs.fsPath, srcBucket, srcObject))
|
fi, err := fsStatFile(pathJoin(fs.fsPath, srcBucket, srcObject))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, toObjectErr(err, srcBucket, srcObject)
|
return oi, toObjectErr(err, srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this request is only metadata update.
|
// Check if this request is only metadata update.
|
||||||
@@ -322,7 +330,7 @@ func (fs fsObjects) CopyObject(srcBucket, srcObject, dstBucket, dstObject string
|
|||||||
var wlk *lock.LockedFile
|
var wlk *lock.LockedFile
|
||||||
wlk, err = fs.rwPool.Write(fsMetaPath)
|
wlk, err = fs.rwPool.Write(fsMetaPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, toObjectErr(traceError(err), srcBucket, srcObject)
|
return oi, toObjectErr(traceError(err), srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
// This close will allow for locks to be synchronized on `fs.json`.
|
// This close will allow for locks to be synchronized on `fs.json`.
|
||||||
defer wlk.Close()
|
defer wlk.Close()
|
||||||
@@ -331,7 +339,7 @@ func (fs fsObjects) CopyObject(srcBucket, srcObject, dstBucket, dstObject string
|
|||||||
fsMeta := newFSMetaV1()
|
fsMeta := newFSMetaV1()
|
||||||
fsMeta.Meta = metadata
|
fsMeta.Meta = metadata
|
||||||
if _, err = fsMeta.WriteTo(wlk); err != nil {
|
if _, err = fsMeta.WriteTo(wlk); err != nil {
|
||||||
return ObjectInfo{}, toObjectErr(err, srcBucket, srcObject)
|
return oi, toObjectErr(err, srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the new object info.
|
// Return the new object info.
|
||||||
@@ -356,7 +364,7 @@ func (fs fsObjects) CopyObject(srcBucket, srcObject, dstBucket, dstObject string
|
|||||||
|
|
||||||
objInfo, err := fs.PutObject(dstBucket, dstObject, length, pipeReader, metadata, "")
|
objInfo, err := fs.PutObject(dstBucket, dstObject, length, pipeReader, metadata, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, toObjectErr(err, dstBucket, dstObject)
|
return oi, toObjectErr(err, dstBucket, dstObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicitly close the reader.
|
// Explicitly close the reader.
|
||||||
@@ -431,7 +439,7 @@ func (fs fsObjects) GetObject(bucket, object string, offset int64, length int64,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getObjectInfo - wrapper for reading object metadata and constructs ObjectInfo.
|
// getObjectInfo - wrapper for reading object metadata and constructs ObjectInfo.
|
||||||
func (fs fsObjects) getObjectInfo(bucket, object string) (ObjectInfo, error) {
|
func (fs fsObjects) getObjectInfo(bucket, object string) (oi ObjectInfo, e error) {
|
||||||
fsMeta := fsMetaV1{}
|
fsMeta := fsMetaV1{}
|
||||||
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, bucket, object, fsMetaJSONFile)
|
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, bucket, object, fsMetaJSONFile)
|
||||||
|
|
||||||
@@ -446,33 +454,33 @@ func (fs fsObjects) getObjectInfo(bucket, object string) (ObjectInfo, error) {
|
|||||||
// PutObject() transaction, if we arrive at such
|
// PutObject() transaction, if we arrive at such
|
||||||
// a situation we just ignore and continue.
|
// a situation we just ignore and continue.
|
||||||
if errorCause(rerr) != io.EOF {
|
if errorCause(rerr) != io.EOF {
|
||||||
return ObjectInfo{}, toObjectErr(rerr, bucket, object)
|
return oi, toObjectErr(rerr, bucket, object)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore if `fs.json` is not available, this is true for pre-existing data.
|
// Ignore if `fs.json` is not available, this is true for pre-existing data.
|
||||||
if err != nil && err != errFileNotFound {
|
if err != nil && err != errFileNotFound {
|
||||||
return ObjectInfo{}, toObjectErr(traceError(err), bucket, object)
|
return oi, toObjectErr(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stat the file to get file size.
|
// Stat the file to get file size.
|
||||||
fi, err := fsStatFile(pathJoin(fs.fsPath, bucket, object))
|
fi, err := fsStatFile(pathJoin(fs.fsPath, bucket, object))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
return oi, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fsMeta.ToObjectInfo(bucket, object, fi), nil
|
return fsMeta.ToObjectInfo(bucket, object, fi), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObjectInfo - reads object metadata and replies back ObjectInfo.
|
// GetObjectInfo - reads object metadata and replies back ObjectInfo.
|
||||||
func (fs fsObjects) GetObjectInfo(bucket, object string) (ObjectInfo, error) {
|
func (fs fsObjects) GetObjectInfo(bucket, object string) (oi ObjectInfo, e error) {
|
||||||
if err := checkGetObjArgs(bucket, object); err != nil {
|
if err := checkGetObjArgs(bucket, object); err != nil {
|
||||||
return ObjectInfo{}, err
|
return oi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fs.statBucketDir(bucket); err != nil {
|
if _, err := fs.statBucketDir(bucket); err != nil {
|
||||||
return ObjectInfo{}, toObjectErr(err, bucket)
|
return oi, toObjectErr(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fs.getObjectInfo(bucket, object)
|
return fs.getObjectInfo(bucket, object)
|
||||||
@@ -759,18 +767,18 @@ func (fs fsObjects) getObjectETag(bucket, entry string) (string, error) {
|
|||||||
|
|
||||||
// ListObjects - list all objects at prefix upto maxKeys., optionally delimited by '/'. Maintains the list pool
|
// ListObjects - list all objects at prefix upto maxKeys., optionally delimited by '/'. Maintains the list pool
|
||||||
// state for future re-entrant list requests.
|
// state for future re-entrant list requests.
|
||||||
func (fs fsObjects) ListObjects(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
func (fs fsObjects) ListObjects(bucket, prefix, marker, delimiter string, maxKeys int) (loi ListObjectsInfo, e error) {
|
||||||
if err := checkListObjsArgs(bucket, prefix, marker, delimiter, fs); err != nil {
|
if err := checkListObjsArgs(bucket, prefix, marker, delimiter, fs); err != nil {
|
||||||
return ListObjectsInfo{}, err
|
return loi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fs.statBucketDir(bucket); err != nil {
|
if _, err := fs.statBucketDir(bucket); err != nil {
|
||||||
return ListObjectsInfo{}, err
|
return loi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// With max keys of zero we have reached eof, return right here.
|
// With max keys of zero we have reached eof, return right here.
|
||||||
if maxKeys == 0 {
|
if maxKeys == 0 {
|
||||||
return ListObjectsInfo{}, nil
|
return loi, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// For delimiter and prefix as '/' we do not list anything at all
|
// For delimiter and prefix as '/' we do not list anything at all
|
||||||
@@ -779,7 +787,7 @@ func (fs fsObjects) ListObjects(bucket, prefix, marker, delimiter string, maxKey
|
|||||||
// as '/' we don't have any entries, since all the keys are
|
// as '/' we don't have any entries, since all the keys are
|
||||||
// of form 'keyName/...'
|
// of form 'keyName/...'
|
||||||
if delimiter == slashSeparator && prefix == slashSeparator {
|
if delimiter == slashSeparator && prefix == slashSeparator {
|
||||||
return ListObjectsInfo{}, nil
|
return loi, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Over flowing count - reset to maxObjectList.
|
// Over flowing count - reset to maxObjectList.
|
||||||
@@ -860,13 +868,13 @@ func (fs fsObjects) ListObjects(bucket, prefix, marker, delimiter string, maxKey
|
|||||||
if walkResult.err != nil {
|
if walkResult.err != nil {
|
||||||
// File not found is a valid case.
|
// File not found is a valid case.
|
||||||
if errorCause(walkResult.err) == errFileNotFound {
|
if errorCause(walkResult.err) == errFileNotFound {
|
||||||
return ListObjectsInfo{}, nil
|
return loi, nil
|
||||||
}
|
}
|
||||||
return ListObjectsInfo{}, toObjectErr(walkResult.err, bucket, prefix)
|
return loi, toObjectErr(walkResult.err, bucket, prefix)
|
||||||
}
|
}
|
||||||
objInfo, err := entryToObjectInfo(walkResult.entry)
|
objInfo, err := entryToObjectInfo(walkResult.entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListObjectsInfo{}, nil
|
return loi, nil
|
||||||
}
|
}
|
||||||
nextMarker = objInfo.Name
|
nextMarker = objInfo.Name
|
||||||
objInfos = append(objInfos, objInfo)
|
objInfos = append(objInfos, objInfo)
|
||||||
@@ -908,8 +916,8 @@ func (fs fsObjects) HealBucket(bucket string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListObjectsHeal - list all objects to be healed. Valid only for XL
|
// ListObjectsHeal - list all objects to be healed. Valid only for XL
|
||||||
func (fs fsObjects) ListObjectsHeal(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
func (fs fsObjects) ListObjectsHeal(bucket, prefix, marker, delimiter string, maxKeys int) (loi ListObjectsInfo, e error) {
|
||||||
return ListObjectsInfo{}, traceError(NotImplemented{})
|
return loi, traceError(NotImplemented{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListBucketsHeal - list all buckets to be healed. Valid only for XL
|
// ListBucketsHeal - list all buckets to be healed. Valid only for XL
|
||||||
@@ -918,6 +926,6 @@ func (fs fsObjects) ListBucketsHeal() ([]BucketInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs fsObjects) ListUploadsHeal(bucket, prefix, marker, uploadIDMarker,
|
func (fs fsObjects) ListUploadsHeal(bucket, prefix, marker, uploadIDMarker,
|
||||||
delimiter string, maxUploads int) (ListMultipartsInfo, error) {
|
delimiter string, maxUploads int) (lmi ListMultipartsInfo, e error) {
|
||||||
return ListMultipartsInfo{}, traceError(NotImplemented{})
|
return lmi, traceError(NotImplemented{})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2017 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
func anonErrToObjectErr(statusCode int, params ...string) error {
|
||||||
|
bucket := ""
|
||||||
|
object := ""
|
||||||
|
if len(params) >= 1 {
|
||||||
|
bucket = params[0]
|
||||||
|
}
|
||||||
|
if len(params) == 2 {
|
||||||
|
object = params[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch statusCode {
|
||||||
|
case http.StatusNotFound:
|
||||||
|
if object != "" {
|
||||||
|
return ObjectNotFound{bucket, object}
|
||||||
|
}
|
||||||
|
return BucketNotFound{Bucket: bucket}
|
||||||
|
case http.StatusBadRequest:
|
||||||
|
if object != "" {
|
||||||
|
return ObjectNameInvalid{bucket, object}
|
||||||
|
}
|
||||||
|
return BucketNameInvalid{Bucket: bucket}
|
||||||
|
case http.StatusForbidden:
|
||||||
|
fallthrough
|
||||||
|
case http.StatusUnauthorized:
|
||||||
|
return AllAccessDisabled{bucket, object}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errUnexpected
|
||||||
|
}
|
||||||
+111
-14
@@ -29,6 +29,48 @@ import (
|
|||||||
"github.com/Azure/azure-sdk-for-go/storage"
|
"github.com/Azure/azure-sdk-for-go/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Make anonymous HTTP request to azure endpoint.
|
||||||
|
func azureAnonRequest(verb, urlStr string, header http.Header) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(verb, urlStr, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if header != nil {
|
||||||
|
req.Header = header
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4XX and 5XX are error HTTP codes.
|
||||||
|
if resp.StatusCode >= 400 && resp.StatusCode <= 511 {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
respBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(respBody) == 0 {
|
||||||
|
// no error in response body, might happen in HEAD requests
|
||||||
|
return nil, storage.AzureStorageServiceError{
|
||||||
|
StatusCode: resp.StatusCode,
|
||||||
|
Code: resp.Status,
|
||||||
|
Message: "no response body was available for error status code",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Response contains Azure storage service error object.
|
||||||
|
var storageErr storage.AzureStorageServiceError
|
||||||
|
if err := xml.Unmarshal(respBody, &storageErr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
storageErr.StatusCode = resp.StatusCode
|
||||||
|
return nil, storageErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
// AnonGetBucketInfo - Get bucket metadata from azure anonymously.
|
// AnonGetBucketInfo - Get bucket metadata from azure anonymously.
|
||||||
func (a *azureObjects) AnonGetBucketInfo(bucket string) (bucketInfo BucketInfo, err error) {
|
func (a *azureObjects) AnonGetBucketInfo(bucket string) (bucketInfo BucketInfo, err error) {
|
||||||
url, err := url.Parse(a.client.GetBlobURL(bucket, ""))
|
url, err := url.Parse(a.client.GetBlobURL(bucket, ""))
|
||||||
@@ -36,11 +78,11 @@ func (a *azureObjects) AnonGetBucketInfo(bucket string) (bucketInfo BucketInfo,
|
|||||||
return bucketInfo, azureToObjectError(traceError(err))
|
return bucketInfo, azureToObjectError(traceError(err))
|
||||||
}
|
}
|
||||||
url.RawQuery = "restype=container"
|
url.RawQuery = "restype=container"
|
||||||
resp, err := http.Head(url.String())
|
resp, err := azureAnonRequest(httpHEAD, url.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return bucketInfo, azureToObjectError(traceError(err), bucket)
|
return bucketInfo, azureToObjectError(traceError(err), bucket)
|
||||||
}
|
}
|
||||||
resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return bucketInfo, azureToObjectError(traceError(anonErrToObjectErr(resp.StatusCode, bucket)), bucket)
|
return bucketInfo, azureToObjectError(traceError(anonErrToObjectErr(resp.StatusCode, bucket)), bucket)
|
||||||
@@ -67,19 +109,14 @@ func (a *azureObjects) AnonPutObject(bucket, object string, size int64, data io.
|
|||||||
// AnonGetObject - SendGET request without authentication.
|
// AnonGetObject - SendGET request without authentication.
|
||||||
// This is needed when clients send GET requests on objects that can be downloaded without auth.
|
// This is needed when clients send GET requests on objects that can be downloaded without auth.
|
||||||
func (a *azureObjects) AnonGetObject(bucket, object string, startOffset int64, length int64, writer io.Writer) (err error) {
|
func (a *azureObjects) AnonGetObject(bucket, object string, startOffset int64, length int64, writer io.Writer) (err error) {
|
||||||
u := a.client.GetBlobURL(bucket, object)
|
h := make(http.Header)
|
||||||
req, err := http.NewRequest("GET", u, nil)
|
|
||||||
if err != nil {
|
|
||||||
return azureToObjectError(traceError(err), bucket, object)
|
|
||||||
}
|
|
||||||
|
|
||||||
if length > 0 && startOffset > 0 {
|
if length > 0 && startOffset > 0 {
|
||||||
req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", startOffset, startOffset+length-1))
|
h.Add("Range", fmt.Sprintf("bytes=%d-%d", startOffset, startOffset+length-1))
|
||||||
} else if startOffset > 0 {
|
} else if startOffset > 0 {
|
||||||
req.Header.Add("Range", fmt.Sprintf("bytes=%d-", startOffset))
|
h.Add("Range", fmt.Sprintf("bytes=%d-", startOffset))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := azureAnonRequest(httpGET, a.client.GetBlobURL(bucket, object), h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return azureToObjectError(traceError(err), bucket, object)
|
return azureToObjectError(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
@@ -96,11 +133,11 @@ func (a *azureObjects) AnonGetObject(bucket, object string, startOffset int64, l
|
|||||||
// AnonGetObjectInfo - Send HEAD request without authentication and convert the
|
// AnonGetObjectInfo - Send HEAD request without authentication and convert the
|
||||||
// result to ObjectInfo.
|
// result to ObjectInfo.
|
||||||
func (a *azureObjects) AnonGetObjectInfo(bucket, object string) (objInfo ObjectInfo, err error) {
|
func (a *azureObjects) AnonGetObjectInfo(bucket, object string) (objInfo ObjectInfo, err error) {
|
||||||
resp, err := http.Head(a.client.GetBlobURL(bucket, object))
|
resp, err := azureAnonRequest(httpHEAD, a.client.GetBlobURL(bucket, object), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return objInfo, azureToObjectError(traceError(err), bucket, object)
|
return objInfo, azureToObjectError(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return objInfo, azureToObjectError(traceError(anonErrToObjectErr(resp.StatusCode, bucket, object)), bucket, object)
|
return objInfo, azureToObjectError(traceError(anonErrToObjectErr(resp.StatusCode, bucket, object)), bucket, object)
|
||||||
@@ -153,7 +190,7 @@ func (a *azureObjects) AnonListObjects(bucket, prefix, marker, delimiter string,
|
|||||||
}
|
}
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
resp, err := http.Get(url.String())
|
resp, err := azureAnonRequest(httpGET, url.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, azureToObjectError(traceError(err))
|
return result, azureToObjectError(traceError(err))
|
||||||
}
|
}
|
||||||
@@ -190,3 +227,63 @@ func (a *azureObjects) AnonListObjects(bucket, prefix, marker, delimiter string,
|
|||||||
result.Prefixes = listResp.BlobPrefixes
|
result.Prefixes = listResp.BlobPrefixes
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AnonListObjectsV2 - List objects in V2 mode, anonymously
|
||||||
|
func (a *azureObjects) AnonListObjectsV2(bucket, prefix, continuationToken string, fetchOwner bool, delimiter string, maxKeys int) (result ListObjectsV2Info, err error) {
|
||||||
|
params := storage.ListBlobsParameters{
|
||||||
|
Prefix: prefix,
|
||||||
|
Marker: continuationToken,
|
||||||
|
Delimiter: delimiter,
|
||||||
|
MaxResults: uint(maxKeys),
|
||||||
|
}
|
||||||
|
|
||||||
|
q := azureListBlobsGetParameters(params)
|
||||||
|
q.Set("restype", "container")
|
||||||
|
q.Set("comp", "list")
|
||||||
|
|
||||||
|
url, err := url.Parse(a.client.GetBlobURL(bucket, ""))
|
||||||
|
if err != nil {
|
||||||
|
return result, azureToObjectError(traceError(err))
|
||||||
|
}
|
||||||
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
resp, err := http.Get(url.String())
|
||||||
|
if err != nil {
|
||||||
|
return result, azureToObjectError(traceError(err))
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var listResp storage.BlobListResponse
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return result, azureToObjectError(traceError(err))
|
||||||
|
}
|
||||||
|
err = xml.Unmarshal(data, &listResp)
|
||||||
|
if err != nil {
|
||||||
|
return result, azureToObjectError(traceError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If NextMarker is not empty, this means response is truncated and NextContinuationToken should be set
|
||||||
|
if listResp.NextMarker != "" {
|
||||||
|
result.IsTruncated = true
|
||||||
|
result.NextContinuationToken = listResp.NextMarker
|
||||||
|
}
|
||||||
|
for _, object := range listResp.Blobs {
|
||||||
|
t, e := time.Parse(time.RFC1123, object.Properties.LastModified)
|
||||||
|
if e != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.Objects = append(result.Objects, ObjectInfo{
|
||||||
|
Bucket: bucket,
|
||||||
|
Name: object.Name,
|
||||||
|
ModTime: t,
|
||||||
|
Size: object.Properties.ContentLength,
|
||||||
|
ETag: canonicalizeETag(object.Properties.Etag),
|
||||||
|
ContentType: object.Properties.ContentType,
|
||||||
|
ContentEncoding: object.Properties.ContentEncoding,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
result.Prefixes = listResp.BlobPrefixes
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ func (a *azureObjects) HealObject(bucket, object string) (int, int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListObjectsHeal - Not relevant.
|
// ListObjectsHeal - Not relevant.
|
||||||
func (a *azureObjects) ListObjectsHeal(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
func (a *azureObjects) ListObjectsHeal(bucket, prefix, marker, delimiter string, maxKeys int) (loi ListObjectsInfo, e error) {
|
||||||
return ListObjectsInfo{}, traceError(NotImplemented{})
|
return loi, traceError(NotImplemented{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUploadsHeal - Not relevant.
|
// ListUploadsHeal - Not relevant.
|
||||||
func (a *azureObjects) ListUploadsHeal(bucket, prefix, marker, uploadIDMarker,
|
func (a *azureObjects) ListUploadsHeal(bucket, prefix, marker, uploadIDMarker,
|
||||||
delimiter string, maxUploads int) (ListMultipartsInfo, error) {
|
delimiter string, maxUploads int) (lmi ListMultipartsInfo, e error) {
|
||||||
return ListMultipartsInfo{}, traceError(NotImplemented{})
|
return lmi, traceError(NotImplemented{})
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-38
@@ -67,6 +67,11 @@ func azureToS3Metadata(meta map[string]string) (newMeta map[string]string) {
|
|||||||
return newMeta
|
return newMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append "-1" to etag so that clients do not interpret it as MD5.
|
||||||
|
func azureToS3ETag(etag string) string {
|
||||||
|
return canonicalizeETag(etag) + "-1"
|
||||||
|
}
|
||||||
|
|
||||||
// To store metadata during NewMultipartUpload which will be used after
|
// To store metadata during NewMultipartUpload which will be used after
|
||||||
// CompleteMultipartUpload to call SetBlobMetadata.
|
// CompleteMultipartUpload to call SetBlobMetadata.
|
||||||
type azureMultipartMetaInfo struct {
|
type azureMultipartMetaInfo struct {
|
||||||
@@ -137,6 +142,8 @@ func azureToObjectError(err error, params ...string) error {
|
|||||||
err = BucketExists{Bucket: bucket}
|
err = BucketExists{Bucket: bucket}
|
||||||
case "InvalidResourceName":
|
case "InvalidResourceName":
|
||||||
err = BucketNameInvalid{Bucket: bucket}
|
err = BucketNameInvalid{Bucket: bucket}
|
||||||
|
case "RequestBodyTooLarge":
|
||||||
|
err = PartTooBig{}
|
||||||
default:
|
default:
|
||||||
switch azureErr.StatusCode {
|
switch azureErr.StatusCode {
|
||||||
case http.StatusNotFound:
|
case http.StatusNotFound:
|
||||||
@@ -153,15 +160,26 @@ func azureToObjectError(err error, params ...string) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inits azure blob storage client and returns azureObjects.
|
// Inits azure blob storage client and returns AzureObjects.
|
||||||
func newAzureLayer(endPoint string, account, key string, secure bool) (GatewayLayer, error) {
|
func newAzureLayer(host string) (GatewayLayer, error) {
|
||||||
if endPoint == "" {
|
var err error
|
||||||
endPoint = storage.DefaultBaseURL
|
var endpoint = storage.DefaultBaseURL
|
||||||
|
var secure = true
|
||||||
|
|
||||||
|
// If user provided some parameters
|
||||||
|
if host != "" {
|
||||||
|
endpoint, secure, err = parseGatewayEndpoint(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
c, err := storage.NewClient(account, key, endPoint, globalAzureAPIVersion, secure)
|
|
||||||
|
creds := serverConfig.GetCredential()
|
||||||
|
c, err := storage.NewClient(creds.AccessKey, creds.SecretKey, endpoint, globalAzureAPIVersion, secure)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &azureObjects{}, err
|
return &azureObjects{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &azureObjects{
|
return &azureObjects{
|
||||||
client: c.GetBlobService(),
|
client: c.GetBlobService(),
|
||||||
metaInfo: azureMultipartMetaInfo{
|
metaInfo: azureMultipartMetaInfo{
|
||||||
@@ -174,13 +192,12 @@ func newAzureLayer(endPoint string, account, key string, secure bool) (GatewayLa
|
|||||||
// Shutdown - save any gateway metadata to disk
|
// Shutdown - save any gateway metadata to disk
|
||||||
// if necessary and reload upon next restart.
|
// if necessary and reload upon next restart.
|
||||||
func (a *azureObjects) Shutdown() error {
|
func (a *azureObjects) Shutdown() error {
|
||||||
// TODO
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StorageInfo - Not relevant to Azure backend.
|
// StorageInfo - Not relevant to Azure backend.
|
||||||
func (a *azureObjects) StorageInfo() StorageInfo {
|
func (a *azureObjects) StorageInfo() (si StorageInfo) {
|
||||||
return StorageInfo{}
|
return si
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeBucketWithLocation - Create a new container on azure backend.
|
// MakeBucketWithLocation - Create a new container on azure backend.
|
||||||
@@ -190,13 +207,13 @@ func (a *azureObjects) MakeBucketWithLocation(bucket, location string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketInfo - Get bucket metadata..
|
// GetBucketInfo - Get bucket metadata..
|
||||||
func (a *azureObjects) GetBucketInfo(bucket string) (BucketInfo, error) {
|
func (a *azureObjects) GetBucketInfo(bucket string) (bi BucketInfo, e error) {
|
||||||
// Azure does not have an equivalent call, hence use ListContainers.
|
// Azure does not have an equivalent call, hence use ListContainers.
|
||||||
resp, err := a.client.ListContainers(storage.ListContainersParameters{
|
resp, err := a.client.ListContainers(storage.ListContainersParameters{
|
||||||
Prefix: bucket,
|
Prefix: bucket,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return BucketInfo{}, azureToObjectError(traceError(err), bucket)
|
return bi, azureToObjectError(traceError(err), bucket)
|
||||||
}
|
}
|
||||||
for _, container := range resp.Containers {
|
for _, container := range resp.Containers {
|
||||||
if container.Name == bucket {
|
if container.Name == bucket {
|
||||||
@@ -209,7 +226,7 @@ func (a *azureObjects) GetBucketInfo(bucket string) (BucketInfo, error) {
|
|||||||
} // else continue
|
} // else continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return BucketInfo{}, traceError(BucketNotFound{Bucket: bucket})
|
return bi, traceError(BucketNotFound{Bucket: bucket})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListBuckets - Lists all azure containers, uses Azure equivalent ListContainers.
|
// ListBuckets - Lists all azure containers, uses Azure equivalent ListContainers.
|
||||||
@@ -260,7 +277,42 @@ func (a *azureObjects) ListObjects(bucket, prefix, marker, delimiter string, max
|
|||||||
Name: object.Name,
|
Name: object.Name,
|
||||||
ModTime: t,
|
ModTime: t,
|
||||||
Size: object.Properties.ContentLength,
|
Size: object.Properties.ContentLength,
|
||||||
ETag: canonicalizeETag(object.Properties.Etag),
|
ETag: azureToS3ETag(object.Properties.Etag),
|
||||||
|
ContentType: object.Properties.ContentType,
|
||||||
|
ContentEncoding: object.Properties.ContentEncoding,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
result.Prefixes = resp.BlobPrefixes
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListObjectsV2 - list all blobs in Azure bucket filtered by prefix
|
||||||
|
func (a *azureObjects) ListObjectsV2(bucket, prefix, continuationToken string, fetchOwner bool, delimiter string, maxKeys int) (result ListObjectsV2Info, err error) {
|
||||||
|
resp, err := a.client.ListBlobs(bucket, storage.ListBlobsParameters{
|
||||||
|
Prefix: prefix,
|
||||||
|
Marker: continuationToken,
|
||||||
|
Delimiter: delimiter,
|
||||||
|
MaxResults: uint(maxKeys),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return result, azureToObjectError(traceError(err), bucket, prefix)
|
||||||
|
}
|
||||||
|
// If NextMarker is not empty, this means response is truncated and NextContinuationToken should be set
|
||||||
|
if resp.NextMarker != "" {
|
||||||
|
result.IsTruncated = true
|
||||||
|
result.NextContinuationToken = resp.NextMarker
|
||||||
|
}
|
||||||
|
for _, object := range resp.Blobs {
|
||||||
|
t, e := time.Parse(time.RFC1123, object.Properties.LastModified)
|
||||||
|
if e != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.Objects = append(result.Objects, ObjectInfo{
|
||||||
|
Bucket: bucket,
|
||||||
|
Name: object.Name,
|
||||||
|
ModTime: t,
|
||||||
|
Size: object.Properties.ContentLength,
|
||||||
|
ETag: azureToS3ETag(object.Properties.Etag),
|
||||||
ContentType: object.Properties.ContentType,
|
ContentType: object.Properties.ContentType,
|
||||||
ContentEncoding: object.Properties.ContentEncoding,
|
ContentEncoding: object.Properties.ContentEncoding,
|
||||||
})
|
})
|
||||||
@@ -323,7 +375,7 @@ func (a *azureObjects) GetObjectInfo(bucket, object string) (objInfo ObjectInfo,
|
|||||||
objInfo = ObjectInfo{
|
objInfo = ObjectInfo{
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
UserDefined: meta,
|
UserDefined: meta,
|
||||||
ETag: canonicalizeETag(prop.Etag),
|
ETag: azureToS3ETag(prop.Etag),
|
||||||
ModTime: t,
|
ModTime: t,
|
||||||
Name: object,
|
Name: object,
|
||||||
Size: prop.ContentLength,
|
Size: prop.ContentLength,
|
||||||
@@ -618,31 +670,6 @@ func (a *azureObjects) CompleteMultipartUpload(bucket, object, uploadID string,
|
|||||||
return a.GetObjectInfo(bucket, object)
|
return a.GetObjectInfo(bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
func anonErrToObjectErr(statusCode int, params ...string) error {
|
|
||||||
bucket := ""
|
|
||||||
object := ""
|
|
||||||
if len(params) >= 1 {
|
|
||||||
bucket = params[0]
|
|
||||||
}
|
|
||||||
if len(params) == 2 {
|
|
||||||
object = params[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch statusCode {
|
|
||||||
case http.StatusNotFound:
|
|
||||||
if object != "" {
|
|
||||||
return ObjectNotFound{bucket, object}
|
|
||||||
}
|
|
||||||
return BucketNotFound{Bucket: bucket}
|
|
||||||
case http.StatusBadRequest:
|
|
||||||
if object != "" {
|
|
||||||
return ObjectNameInvalid{bucket, object}
|
|
||||||
}
|
|
||||||
return BucketNameInvalid{Bucket: bucket}
|
|
||||||
}
|
|
||||||
return errUnexpected
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copied from github.com/Azure/azure-sdk-for-go/storage/blob.go
|
// Copied from github.com/Azure/azure-sdk-for-go/storage/blob.go
|
||||||
func azureListBlobsGetParameters(p storage.ListBlobsParameters) url.Values {
|
func azureListBlobsGetParameters(p storage.ListBlobsParameters) url.Values {
|
||||||
out := url.Values{}
|
out := url.Values{}
|
||||||
|
|||||||
@@ -18,21 +18,41 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Azure/azure-sdk-for-go/storage"
|
"github.com/Azure/azure-sdk-for-go/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Test azureToS3ETag.
|
||||||
|
func TestAzureToS3ETag(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
etag string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{`"etag"`, `etag-1`},
|
||||||
|
{"etag", "etag-1"},
|
||||||
|
}
|
||||||
|
for i, test := range tests {
|
||||||
|
got := azureToS3ETag(test.etag)
|
||||||
|
if got != test.expected {
|
||||||
|
t.Errorf("test %d: got:%s expected:%s", i+1, got, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Test canonical metadata.
|
// Test canonical metadata.
|
||||||
func TestS3ToAzureHeaders(t *testing.T) {
|
func TestS3ToAzureHeaders(t *testing.T) {
|
||||||
headers := map[string]string{
|
headers := map[string]string{
|
||||||
"accept-encoding": "gzip",
|
"accept-encoding": "gzip",
|
||||||
"content-encoding": "gzip",
|
"content-encoding": "gzip",
|
||||||
|
"X-Amz-Meta-Hdr": "value",
|
||||||
}
|
}
|
||||||
expectedHeaders := map[string]string{
|
expectedHeaders := map[string]string{
|
||||||
"Accept-Encoding": "gzip",
|
"Accept-Encoding": "gzip",
|
||||||
"Content-Encoding": "gzip",
|
"Content-Encoding": "gzip",
|
||||||
|
"X-Ms-Meta-Hdr": "value",
|
||||||
}
|
}
|
||||||
actualHeaders := s3ToAzureHeaders(headers)
|
actualHeaders := s3ToAzureHeaders(headers)
|
||||||
if !reflect.DeepEqual(actualHeaders, expectedHeaders) {
|
if !reflect.DeepEqual(actualHeaders, expectedHeaders) {
|
||||||
@@ -155,3 +175,81 @@ func TestAzureParseBlockID(t *testing.T) {
|
|||||||
t.Fatal("Expected azureParseBlockID() to return error")
|
t.Fatal("Expected azureParseBlockID() to return error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test azureListBlobsGetParameters()
|
||||||
|
func TestAzureListBlobsGetParameters(t *testing.T) {
|
||||||
|
|
||||||
|
// Test values set 1
|
||||||
|
expectedURLValues := url.Values{}
|
||||||
|
expectedURLValues.Set("prefix", "test")
|
||||||
|
expectedURLValues.Set("delimiter", "_")
|
||||||
|
expectedURLValues.Set("marker", "marker")
|
||||||
|
expectedURLValues.Set("include", "hello")
|
||||||
|
expectedURLValues.Set("maxresults", "20")
|
||||||
|
expectedURLValues.Set("timeout", "10")
|
||||||
|
|
||||||
|
setBlobParameters := storage.ListBlobsParameters{"test", "_", "marker", "hello", 20, 10}
|
||||||
|
|
||||||
|
// Test values set 2
|
||||||
|
expectedURLValues1 := url.Values{}
|
||||||
|
|
||||||
|
setBlobParameters1 := storage.ListBlobsParameters{"", "", "", "", 0, 0}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args storage.ListBlobsParameters
|
||||||
|
want url.Values
|
||||||
|
}{
|
||||||
|
{"TestIfValuesSet", setBlobParameters, expectedURLValues},
|
||||||
|
{"TestIfValuesNotSet", setBlobParameters1, expectedURLValues1},
|
||||||
|
}
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if got := azureListBlobsGetParameters(test.args); !reflect.DeepEqual(got, test.want) {
|
||||||
|
t.Errorf("azureListBlobsGetParameters() = %v, want %v", got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonErrToObjectErr(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
statusCode int
|
||||||
|
params []string
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{"ObjectNotFound",
|
||||||
|
http.StatusNotFound,
|
||||||
|
[]string{"testBucket", "testObject"},
|
||||||
|
ObjectNotFound{Bucket: "testBucket", Object: "testObject"},
|
||||||
|
},
|
||||||
|
{"BucketNotFound",
|
||||||
|
http.StatusNotFound,
|
||||||
|
[]string{"testBucket", ""},
|
||||||
|
BucketNotFound{Bucket: "testBucket"},
|
||||||
|
},
|
||||||
|
{"ObjectNameInvalid",
|
||||||
|
http.StatusBadRequest,
|
||||||
|
[]string{"testBucket", "testObject"},
|
||||||
|
ObjectNameInvalid{Bucket: "testBucket", Object: "testObject"},
|
||||||
|
},
|
||||||
|
{"BucketNameInvalid",
|
||||||
|
http.StatusBadRequest,
|
||||||
|
[]string{"testBucket", ""},
|
||||||
|
BucketNameInvalid{Bucket: "testBucket"},
|
||||||
|
},
|
||||||
|
{"UnexpectedError",
|
||||||
|
http.StatusBadGateway,
|
||||||
|
[]string{"testBucket", "testObject"},
|
||||||
|
errUnexpected,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if err := anonErrToObjectErr(test.statusCode, test.params...); !reflect.DeepEqual(err, test.wantErr) {
|
||||||
|
t.Errorf("anonErrToObjectErr() error = %v, wantErr %v", err, test.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* 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 (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toGCSPublicURL(bucket, object string) string {
|
||||||
|
return fmt.Sprintf("https://storage.googleapis.com/%s/%s", bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnonPutObject creates a new object anonymously with the incoming data,
|
||||||
|
func (l *gcsGateway) AnonPutObject(bucket, object string, size int64, data io.Reader, metadata map[string]string, sha256sum string) (ObjectInfo, error) {
|
||||||
|
|
||||||
|
return ObjectInfo{}, NotImplemented{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnonGetObject - Get object anonymously
|
||||||
|
func (l *gcsGateway) AnonGetObject(bucket string, object string, startOffset int64, length int64, writer io.Writer) error {
|
||||||
|
req, err := http.NewRequest("GET", toGCSPublicURL(bucket, object), nil)
|
||||||
|
if err != nil {
|
||||||
|
return gcsToObjectError(traceError(err), bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
if length > 0 && startOffset > 0 {
|
||||||
|
req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", startOffset, startOffset+length-1))
|
||||||
|
} else if startOffset > 0 {
|
||||||
|
req.Header.Add("Range", fmt.Sprintf("bytes=%d-", startOffset))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return gcsToObjectError(traceError(err), bucket, object)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusPartialContent && resp.StatusCode != http.StatusOK {
|
||||||
|
return gcsToObjectError(traceError(anonErrToObjectErr(resp.StatusCode, bucket, object)), bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(writer, resp.Body)
|
||||||
|
return gcsToObjectError(traceError(err), bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnonGetObjectInfo - Get object info anonymously
|
||||||
|
func (l *gcsGateway) AnonGetObjectInfo(bucket string, object string) (objInfo ObjectInfo, err error) {
|
||||||
|
resp, err := http.Head(toGCSPublicURL(bucket, object))
|
||||||
|
if err != nil {
|
||||||
|
return objInfo, gcsToObjectError(traceError(err), bucket, object)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return objInfo, gcsToObjectError(traceError(anonErrToObjectErr(resp.StatusCode, bucket, object)), bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentLength int64
|
||||||
|
contentLengthStr := resp.Header.Get("Content-Length")
|
||||||
|
if contentLengthStr != "" {
|
||||||
|
contentLength, err = strconv.ParseInt(contentLengthStr, 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
return objInfo, gcsToObjectError(traceError(errUnexpected), bucket, object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := time.Parse(time.RFC1123, resp.Header.Get("Last-Modified"))
|
||||||
|
if err != nil {
|
||||||
|
return objInfo, traceError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
objInfo.ModTime = t
|
||||||
|
objInfo.Bucket = bucket
|
||||||
|
objInfo.UserDefined = make(map[string]string)
|
||||||
|
if resp.Header.Get("Content-Encoding") != "" {
|
||||||
|
objInfo.UserDefined["Content-Encoding"] = resp.Header.Get("Content-Encoding")
|
||||||
|
}
|
||||||
|
objInfo.UserDefined["Content-Type"] = resp.Header.Get("Content-Type")
|
||||||
|
objInfo.ETag = resp.Header.Get("Etag")
|
||||||
|
objInfo.ModTime = t
|
||||||
|
objInfo.Name = object
|
||||||
|
objInfo.Size = contentLength
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnonListObjects - List objects anonymously
|
||||||
|
func (l *gcsGateway) AnonListObjects(bucket string, prefix string, marker string, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
||||||
|
result, err := l.anonClient.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
|
||||||
|
if err != nil {
|
||||||
|
return ListObjectsInfo{}, s3ToObjectError(traceError(err), bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fromMinioClientListBucketResult(bucket, result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnonListObjectsV2 - List objects in V2 mode, anonymously
|
||||||
|
func (l *gcsGateway) AnonListObjectsV2(bucket, prefix, continuationToken string, fetchOwner bool, delimiter string, maxKeys int) (ListObjectsV2Info, error) {
|
||||||
|
// Request V1 List Object to the backend
|
||||||
|
result, err := l.anonClient.ListObjects(bucket, prefix, continuationToken, delimiter, maxKeys)
|
||||||
|
if err != nil {
|
||||||
|
return ListObjectsV2Info{}, s3ToObjectError(traceError(err), bucket)
|
||||||
|
}
|
||||||
|
// translate V1 Result to V2Info
|
||||||
|
return fromMinioClientListBucketResultToV2Info(bucket, result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnonGetBucketInfo - Get bucket metadata anonymously.
|
||||||
|
func (l *gcsGateway) AnonGetBucketInfo(bucket string) (bucketInfo BucketInfo, err error) {
|
||||||
|
resp, err := http.Head(toGCSPublicURL(bucket, ""))
|
||||||
|
if err != nil {
|
||||||
|
return bucketInfo, gcsToObjectError(traceError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return bucketInfo, gcsToObjectError(traceError(anonErrToObjectErr(resp.StatusCode, bucket)), bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last-Modified date being returned by GCS
|
||||||
|
return BucketInfo{
|
||||||
|
Name: bucket,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Project ID format is not valid.
|
||||||
|
errGCSInvalidProjectID = errors.New("GCS project id is either empty or invalid")
|
||||||
|
|
||||||
|
// Project ID not found
|
||||||
|
errGCSProjectIDNotFound = errors.New("unknown project id")
|
||||||
|
)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
// HealBucket - Not relevant.
|
||||||
|
func (l *gcsGateway) HealBucket(bucket string) error {
|
||||||
|
return traceError(NotImplemented{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListBucketsHeal - Not relevant.
|
||||||
|
func (l *gcsGateway) ListBucketsHeal() (buckets []BucketInfo, err error) {
|
||||||
|
return []BucketInfo{}, traceError(NotImplemented{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealObject - Not relevant.
|
||||||
|
func (l *gcsGateway) HealObject(bucket string, object string) (int, int, error) {
|
||||||
|
return 0, 0, traceError(NotImplemented{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListObjectsHeal - Not relevant.
|
||||||
|
func (l *gcsGateway) ListObjectsHeal(bucket string, prefix string, marker string, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
||||||
|
return ListObjectsInfo{}, traceError(NotImplemented{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUploadsHeal - Not relevant.
|
||||||
|
func (l *gcsGateway) ListUploadsHeal(bucket string, prefix string, marker string, uploadIDMarker string, delimiter string, maxUploads int) (ListMultipartsInfo, error) {
|
||||||
|
return ListMultipartsInfo{}, traceError(NotImplemented{})
|
||||||
|
}
|
||||||
+1123
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
|||||||
|
/*
|
||||||
|
* 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 (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
minio "github.com/minio/minio-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToGCSPageToken(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
Name string
|
||||||
|
Token string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "A",
|
||||||
|
Token: "CgFB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "AAAAAAAAAA",
|
||||||
|
Token: "CgpBQUFBQUFBQUFB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
Token: "CmRBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
Token: "CpEDQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE=",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
Token: "CpIDQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
Token: "CpMDQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
Token: "CvQDQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE=",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
if toGCSPageToken(testCase.Name) != testCase.Token {
|
||||||
|
t.Errorf("Test %d: Expected %s, got %s", i+1, toGCSPageToken(testCase.Name), testCase.Token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsValidGCSProjectIDFormat tests isValidGCSProjectIDFormat
|
||||||
|
func TestValidGCSProjectIDFormat(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
ProjectID string
|
||||||
|
Valid bool
|
||||||
|
}{
|
||||||
|
{"", false},
|
||||||
|
{"a", false},
|
||||||
|
{"Abc", false},
|
||||||
|
{"1bcd", false},
|
||||||
|
// 5 chars
|
||||||
|
{"abcdb", false},
|
||||||
|
// 6 chars
|
||||||
|
{"abcdbz", true},
|
||||||
|
// 30 chars
|
||||||
|
{"project-id-1-project-id-more-1", true},
|
||||||
|
// 31 chars
|
||||||
|
{"project-id-1-project-id-more-11", false},
|
||||||
|
{"storage.googleapis.com", false},
|
||||||
|
{"http://storage.googleapis.com", false},
|
||||||
|
{"http://localhost:9000", false},
|
||||||
|
{"project-id-1", true},
|
||||||
|
{"project-id-1988832", true},
|
||||||
|
{"projectid1414", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
valid := isValidGCSProjectIDFormat(testCase.ProjectID)
|
||||||
|
if valid != testCase.Valid {
|
||||||
|
t.Errorf("Test %d: Expected %v, got %v", i+1, valid, testCase.Valid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for isGCSMarker.
|
||||||
|
func TestIsGCSMarker(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
marker string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
marker: "{minio}gcs123",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
marker: "{mini_no}tgcs123",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
marker: "{minioagainnotgcs123",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
marker: "obj1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range testCases {
|
||||||
|
if actual := isGCSMarker(tc.marker); actual != tc.expected {
|
||||||
|
t.Errorf("Test %d: marker is %s, expected %v but got %v",
|
||||||
|
i+1, tc.marker, tc.expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for gcsMultipartMetaName.
|
||||||
|
func TestGCSMultipartMetaName(t *testing.T) {
|
||||||
|
uploadID := "a"
|
||||||
|
expected := pathJoin(gcsMinioMultipartPathV1, uploadID, gcsMinioMultipartMeta)
|
||||||
|
got := gcsMultipartMetaName(uploadID)
|
||||||
|
if expected != got {
|
||||||
|
t.Errorf("expected: %s, got: %s", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for gcsMultipartDataName.
|
||||||
|
func TestGCSMultipartDataName(t *testing.T) {
|
||||||
|
var (
|
||||||
|
uploadID = "a"
|
||||||
|
etag = "b"
|
||||||
|
partNumber = 1
|
||||||
|
)
|
||||||
|
expected := pathJoin(gcsMinioMultipartPathV1, uploadID, fmt.Sprintf("%05d.%s", partNumber, etag))
|
||||||
|
got := gcsMultipartDataName(uploadID, partNumber, etag)
|
||||||
|
if expected != got {
|
||||||
|
t.Errorf("expected: %s, got: %s", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFromMinioClientListBucketResultToV2Info(t *testing.T) {
|
||||||
|
|
||||||
|
listBucketResult := minio.ListBucketResult{
|
||||||
|
IsTruncated: false,
|
||||||
|
Marker: "testMarker",
|
||||||
|
NextMarker: "testMarker2",
|
||||||
|
CommonPrefixes: []minio.CommonPrefix{{Prefix: "one"}, {Prefix: "two"}},
|
||||||
|
Contents: []minio.ObjectInfo{{Key: "testobj", ContentType: ""}},
|
||||||
|
}
|
||||||
|
|
||||||
|
listBucketV2Info := ListObjectsV2Info{
|
||||||
|
Prefixes: []string{"one", "two"},
|
||||||
|
Objects: []ObjectInfo{{Name: "testobj", Bucket: "testbucket", UserDefined: map[string]string{"Content-Type": ""}}},
|
||||||
|
IsTruncated: false,
|
||||||
|
ContinuationToken: "testMarker",
|
||||||
|
NextContinuationToken: "testMarker2",
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := fromMinioClientListBucketResultToV2Info("testbucket", listBucketResult); !reflect.DeepEqual(got, listBucketV2Info) {
|
||||||
|
t.Errorf("fromMinioClientListBucketResultToV2Info() = %v, want %v", got, listBucketV2Info)
|
||||||
|
}
|
||||||
|
}
|
||||||
+150
-113
@@ -19,12 +19,12 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
|
||||||
|
|
||||||
router "github.com/gorilla/mux"
|
router "github.com/gorilla/mux"
|
||||||
"github.com/minio/minio-go/pkg/policy"
|
"github.com/minio/minio-go/pkg/policy"
|
||||||
@@ -140,7 +140,7 @@ func (api gatewayAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reads the object at startOffset and writes to mw.
|
// Reads the object at startOffset and writes to mw.
|
||||||
if err := getObject(bucket, object, startOffset, length, writer); err != nil {
|
if err = getObject(bucket, object, startOffset, length, writer); err != nil {
|
||||||
errorIf(err, "Unable to write to client.")
|
errorIf(err, "Unable to write to client.")
|
||||||
if !dataWritten {
|
if !dataWritten {
|
||||||
// Error response only if no data has been written to client yet. i.e if
|
// Error response only if no data has been written to client yet. i.e if
|
||||||
@@ -157,6 +157,23 @@ func (api gatewayAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
// call wrter.Write(nil) to set appropriate headers.
|
// call wrter.Write(nil) to set appropriate headers.
|
||||||
writer.Write(nil)
|
writer.Write(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get host and port from Request.RemoteAddr.
|
||||||
|
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
host, port = "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify object accessed via a GET request.
|
||||||
|
eventNotify(eventData{
|
||||||
|
Type: ObjectAccessedGet,
|
||||||
|
Bucket: bucket,
|
||||||
|
ObjInfo: objInfo,
|
||||||
|
ReqParams: extractReqParams(r),
|
||||||
|
UserAgent: r.UserAgent(),
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutObjectHandler - PUT Object
|
// PutObjectHandler - PUT Object
|
||||||
@@ -180,6 +197,8 @@ func (api gatewayAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
bucket = vars["bucket"]
|
bucket = vars["bucket"]
|
||||||
object = vars["object"]
|
object = vars["object"]
|
||||||
|
|
||||||
|
// TODO: we should validate the object name here
|
||||||
|
|
||||||
// Get Content-Md5 sent by client and verify if valid
|
// Get Content-Md5 sent by client and verify if valid
|
||||||
md5Bytes, err := checkValidMD5(r.Header.Get("Content-Md5"))
|
md5Bytes, err := checkValidMD5(r.Header.Get("Content-Md5"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -212,7 +231,12 @@ func (api gatewayAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract metadata to be saved from incoming HTTP header.
|
// Extract metadata to be saved from incoming HTTP header.
|
||||||
metadata := extractMetadataFromHeader(r.Header)
|
metadata, err := extractMetadataFromHeader(r.Header)
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "found invalid http request header")
|
||||||
|
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
if reqAuthType == authTypeStreamingSigned {
|
if reqAuthType == authTypeStreamingSigned {
|
||||||
if contentEncoding, ok := metadata["content-encoding"]; ok {
|
if contentEncoding, ok := metadata["content-encoding"]; ok {
|
||||||
contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
|
contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
|
||||||
@@ -236,8 +260,6 @@ func (api gatewayAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
// Make sure we hex encode md5sum here.
|
// Make sure we hex encode md5sum here.
|
||||||
metadata["etag"] = hex.EncodeToString(md5Bytes)
|
metadata["etag"] = hex.EncodeToString(md5Bytes)
|
||||||
|
|
||||||
sha256sum := ""
|
|
||||||
|
|
||||||
// Lock the object.
|
// Lock the object.
|
||||||
objectLock := globalNSMutex.NewNSLock(bucket, object)
|
objectLock := globalNSMutex.NewNSLock(bucket, object)
|
||||||
objectLock.Lock()
|
objectLock.Lock()
|
||||||
@@ -247,7 +269,7 @@ func (api gatewayAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
switch reqAuthType {
|
switch reqAuthType {
|
||||||
case authTypeAnonymous:
|
case authTypeAnonymous:
|
||||||
// Create anonymous object.
|
// Create anonymous object.
|
||||||
objInfo, err = objectAPI.AnonPutObject(bucket, object, size, r.Body, metadata, sha256sum)
|
objInfo, err = objectAPI.AnonPutObject(bucket, object, size, r.Body, metadata, "")
|
||||||
case authTypeStreamingSigned:
|
case authTypeStreamingSigned:
|
||||||
// Initialize stream signature verifier.
|
// Initialize stream signature verifier.
|
||||||
reader, s3Error := newSignV4ChunkedReader(r)
|
reader, s3Error := newSignV4ChunkedReader(r)
|
||||||
@@ -256,7 +278,7 @@ func (api gatewayAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
objInfo, err = objectAPI.PutObject(bucket, object, size, reader, metadata, sha256sum)
|
objInfo, err = objectAPI.PutObject(bucket, object, size, reader, metadata, "")
|
||||||
case authTypeSignedV2, authTypePresignedV2:
|
case authTypeSignedV2, authTypePresignedV2:
|
||||||
s3Error := isReqAuthenticatedV2(r)
|
s3Error := isReqAuthenticatedV2(r)
|
||||||
if s3Error != ErrNone {
|
if s3Error != ErrNone {
|
||||||
@@ -264,16 +286,19 @@ func (api gatewayAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
objInfo, err = objectAPI.PutObject(bucket, object, size, r.Body, metadata, sha256sum)
|
objInfo, err = objectAPI.PutObject(bucket, object, size, r.Body, metadata, "")
|
||||||
case authTypePresigned, authTypeSigned:
|
case authTypePresigned, authTypeSigned:
|
||||||
if s3Error := reqSignatureV4Verify(r, serverConfig.GetRegion()); s3Error != ErrNone {
|
if s3Error := reqSignatureV4Verify(r, serverConfig.GetRegion()); s3Error != ErrNone {
|
||||||
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sha256sum := ""
|
||||||
if !skipContentSha256Cksum(r) {
|
if !skipContentSha256Cksum(r) {
|
||||||
sha256sum = r.Header.Get("X-Amz-Content-Sha256")
|
sha256sum = getContentSha256Cksum(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create object.
|
// Create object.
|
||||||
objInfo, err = objectAPI.PutObject(bucket, object, size, r.Body, metadata, sha256sum)
|
objInfo, err = objectAPI.PutObject(bucket, object, size, r.Body, metadata, sha256sum)
|
||||||
default:
|
default:
|
||||||
@@ -283,12 +308,30 @@ func (api gatewayAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
errorIf(err, "Unable to save an object %s", r.URL.Path)
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("ETag", "\""+objInfo.ETag+"\"")
|
w.Header().Set("ETag", "\""+objInfo.ETag+"\"")
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
|
||||||
|
// Get host and port from Request.RemoteAddr.
|
||||||
|
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
host, port = "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify object created event.
|
||||||
|
eventNotify(eventData{
|
||||||
|
Type: ObjectCreatedPut,
|
||||||
|
Bucket: bucket,
|
||||||
|
ObjInfo: objInfo,
|
||||||
|
ReqParams: extractReqParams(r),
|
||||||
|
UserAgent: r.UserAgent(),
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HeadObjectHandler - HEAD Object
|
// HeadObjectHandler - HEAD Object
|
||||||
@@ -357,97 +400,23 @@ func (api gatewayAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.R
|
|||||||
|
|
||||||
// Successful response.
|
// Successful response.
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteMultipleObjectsHandler - deletes multiple objects.
|
// Get host and port from Request.RemoteAddr.
|
||||||
func (api gatewayAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
|
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
vars := router.Vars(r)
|
if err != nil {
|
||||||
bucket := vars["bucket"]
|
host, port = "", ""
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
|
||||||
if objectAPI == nil {
|
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(r, bucket, "s3:DeleteObject", serverConfig.GetRegion()); s3Error != ErrNone {
|
// Notify object accessed via a HEAD request.
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
eventNotify(eventData{
|
||||||
return
|
Type: ObjectAccessedHead,
|
||||||
}
|
Bucket: bucket,
|
||||||
|
ObjInfo: objInfo,
|
||||||
// Content-Length is required and should be non-zero
|
ReqParams: extractReqParams(r),
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
UserAgent: r.UserAgent(),
|
||||||
if r.ContentLength <= 0 {
|
Host: host,
|
||||||
writeErrorResponse(w, ErrMissingContentLength, r.URL)
|
Port: port,
|
||||||
return
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// Content-Md5 is requied should be set
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
|
||||||
if _, ok := r.Header["Content-Md5"]; !ok {
|
|
||||||
writeErrorResponse(w, ErrMissingContentMD5, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate incoming content length bytes.
|
|
||||||
deleteXMLBytes := make([]byte, r.ContentLength)
|
|
||||||
|
|
||||||
// Read incoming body XML bytes.
|
|
||||||
if _, err := io.ReadFull(r.Body, deleteXMLBytes); err != nil {
|
|
||||||
errorIf(err, "Unable to read HTTP body.")
|
|
||||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal list of keys to be deleted.
|
|
||||||
deleteObjects := &DeleteObjectsRequest{}
|
|
||||||
if err := xml.Unmarshal(deleteXMLBytes, deleteObjects); err != nil {
|
|
||||||
errorIf(err, "Unable to unmarshal delete objects request XML.")
|
|
||||||
writeErrorResponse(w, ErrMalformedXML, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var dErrs = make([]error, len(deleteObjects.Objects))
|
|
||||||
|
|
||||||
// Delete all requested objects in parallel.
|
|
||||||
for index, object := range deleteObjects.Objects {
|
|
||||||
dErr := objectAPI.DeleteObject(bucket, object.ObjectName)
|
|
||||||
if dErr != nil {
|
|
||||||
dErrs[index] = dErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect deleted objects and errors if any.
|
|
||||||
var deletedObjects []ObjectIdentifier
|
|
||||||
var deleteErrors []DeleteError
|
|
||||||
for index, err := range dErrs {
|
|
||||||
object := deleteObjects.Objects[index]
|
|
||||||
// Success deleted objects are collected separately.
|
|
||||||
if err == nil {
|
|
||||||
deletedObjects = append(deletedObjects, object)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := errorCause(err).(ObjectNotFound); ok {
|
|
||||||
// If the object is not found it should be
|
|
||||||
// accounted as deleted as per S3 spec.
|
|
||||||
deletedObjects = append(deletedObjects, object)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
errorIf(err, "Unable to delete object. %s", object.ObjectName)
|
|
||||||
// Error during delete should be collected separately.
|
|
||||||
deleteErrors = append(deleteErrors, DeleteError{
|
|
||||||
Code: errorCodeResponse[toAPIErrorCode(err)].Code,
|
|
||||||
Message: errorCodeResponse[toAPIErrorCode(err)].Description,
|
|
||||||
Key: object.ObjectName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate response
|
|
||||||
response := generateMultiDeleteResponse(deleteObjects.Quiet, deletedObjects, deleteErrors)
|
|
||||||
encodedSuccessResponse := encodeResponse(response)
|
|
||||||
|
|
||||||
// Write success response.
|
|
||||||
writeSuccessResponseXML(w, encodedSuccessResponse)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutBucketPolicyHandler - PUT Bucket policy
|
// PutBucketPolicyHandler - PUT Bucket policy
|
||||||
@@ -649,15 +618,6 @@ func (api gatewayAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// validating region here, because isValidLocationConstraint
|
|
||||||
// reads body which has been read already. So only validating
|
|
||||||
// region here.
|
|
||||||
serverRegion := serverConfig.GetRegion()
|
|
||||||
if serverRegion != location {
|
|
||||||
writeErrorResponse(w, ErrInvalidRegion, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||||
bucketLock.Lock()
|
bucketLock.Lock()
|
||||||
defer bucketLock.Unlock()
|
defer bucketLock.Unlock()
|
||||||
@@ -746,15 +706,11 @@ func (api gatewayAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *htt
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract all the litsObjectsV1 query params to their native values.
|
// Extract all the listObjectsV1 query params to their native
|
||||||
|
// values. N B We delegate validation of params to respective
|
||||||
|
// gateway backends.
|
||||||
prefix, marker, delimiter, maxKeys, _ := getListObjectsV1Args(r.URL.Query())
|
prefix, marker, delimiter, maxKeys, _ := getListObjectsV1Args(r.URL.Query())
|
||||||
|
|
||||||
// Validate all the query params before beginning to serve the request.
|
|
||||||
if s3Error := validateListObjectsArgs(prefix, marker, delimiter, maxKeys); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
listObjects := objectAPI.ListObjects
|
listObjects := objectAPI.ListObjects
|
||||||
if reqAuthType == authTypeAnonymous {
|
if reqAuthType == authTypeAnonymous {
|
||||||
listObjects = objectAPI.AnonListObjects
|
listObjects = objectAPI.AnonListObjects
|
||||||
@@ -769,6 +725,87 @@ func (api gatewayAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *htt
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, maxKeys, listObjectsInfo)
|
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, maxKeys, listObjectsInfo)
|
||||||
|
// Write success response.
|
||||||
|
writeSuccessResponseXML(w, encodeResponse(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListObjectsV2Handler - GET Bucket (List Objects) Version 2.
|
||||||
|
// --------------------------
|
||||||
|
// This implementation of the GET operation returns some or all (up to 1000)
|
||||||
|
// of the objects in a bucket. You can use the request parameters as selection
|
||||||
|
// criteria to return a subset of the objects in a bucket.
|
||||||
|
//
|
||||||
|
// NOTE: It is recommended that this API to be used for application development.
|
||||||
|
// Minio continues to support ListObjectsV1 for supporting legacy tools.
|
||||||
|
func (api gatewayAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := router.Vars(r)
|
||||||
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
|
objectAPI := api.ObjectAPI()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reqAuthType := getRequestAuthType(r)
|
||||||
|
|
||||||
|
switch reqAuthType {
|
||||||
|
case authTypePresignedV2, authTypeSignedV2:
|
||||||
|
// Signature V2 validation.
|
||||||
|
s3Error := isReqAuthenticatedV2(r)
|
||||||
|
if s3Error != ErrNone {
|
||||||
|
errorIf(errSignatureMismatch, dumpRequest(r))
|
||||||
|
writeErrorResponse(w, s3Error, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case authTypeSigned, authTypePresigned:
|
||||||
|
s3Error := isReqAuthenticated(r, serverConfig.GetRegion())
|
||||||
|
if s3Error != ErrNone {
|
||||||
|
errorIf(errSignatureMismatch, dumpRequest(r))
|
||||||
|
writeErrorResponse(w, s3Error, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case authTypeAnonymous:
|
||||||
|
// No verification needed for anonymous requests.
|
||||||
|
default:
|
||||||
|
// For all unknown auth types return error.
|
||||||
|
writeErrorResponse(w, ErrAccessDenied, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract all the listObjectsV2 query params to their native values.
|
||||||
|
prefix, token, startAfter, delimiter, fetchOwner, maxKeys, _ := getListObjectsV2Args(r.URL.Query())
|
||||||
|
|
||||||
|
// In ListObjectsV2 'continuation-token' is the marker.
|
||||||
|
marker := token
|
||||||
|
// Check if 'continuation-token' is empty.
|
||||||
|
if token == "" {
|
||||||
|
// Then we need to use 'start-after' as marker instead.
|
||||||
|
marker = startAfter
|
||||||
|
}
|
||||||
|
|
||||||
|
listObjectsV2 := objectAPI.ListObjectsV2
|
||||||
|
if reqAuthType == authTypeAnonymous {
|
||||||
|
listObjectsV2 = objectAPI.AnonListObjectsV2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the query params before beginning to serve the request.
|
||||||
|
// fetch-owner is not validated since it is a boolean
|
||||||
|
if s3Error := validateListObjectsArgs(prefix, marker, delimiter, maxKeys); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(w, s3Error, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Inititate a list objects operation based on the input params.
|
||||||
|
// On success would return back ListObjectsV2Info object to be
|
||||||
|
// serialized as XML and sent as S3 compatible response body.
|
||||||
|
listObjectsV2Info, err := listObjectsV2(bucket, prefix, token, fetchOwner, delimiter, maxKeys)
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "Unable to list objects. Args to listObjectsV2 are bucket=%s, prefix=%s, token=%s, delimiter=%s", bucket, prefix, token, delimiter)
|
||||||
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsV2Info.ContinuationToken, startAfter, delimiter, fetchOwner, listObjectsV2Info.IsTruncated, maxKeys, listObjectsV2Info.Objects, listObjectsV2Info.Prefixes)
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessResponseXML(w, encodeResponse(response))
|
writeSuccessResponseXML(w, encodeResponse(response))
|
||||||
|
|||||||
+149
-134
@@ -21,11 +21,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/minio/cli"
|
"github.com/minio/cli"
|
||||||
|
miniohttp "github.com/minio/minio/pkg/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
const azureGatewayTemplate = `NAME:
|
const azureGatewayTemplate = `NAME:
|
||||||
@@ -53,26 +56,13 @@ EXAMPLES:
|
|||||||
$ export MINIO_ACCESS_KEY=azureaccountname
|
$ export MINIO_ACCESS_KEY=azureaccountname
|
||||||
$ export MINIO_SECRET_KEY=azureaccountkey
|
$ export MINIO_SECRET_KEY=azureaccountkey
|
||||||
$ {{.HelpName}}
|
$ {{.HelpName}}
|
||||||
|
|
||||||
2. Start minio gateway server for Azure Blob Storage backend on custom endpoint.
|
2. Start minio gateway server for Azure Blob Storage backend on custom endpoint.
|
||||||
$ export MINIO_ACCESS_KEY=azureaccountname
|
$ export MINIO_ACCESS_KEY=azureaccountname
|
||||||
$ export MINIO_SECRET_KEY=azureaccountkey
|
$ export MINIO_SECRET_KEY=azureaccountkey
|
||||||
$ {{.HelpName}} https://azure.example.com
|
$ {{.HelpName}} https://azure.example.com
|
||||||
`
|
`
|
||||||
|
|
||||||
var azureBackendCmd = cli.Command{
|
|
||||||
Name: "azure",
|
|
||||||
Usage: "Microsoft Azure Blob Storage.",
|
|
||||||
Action: azureGatewayMain,
|
|
||||||
CustomHelpTemplate: azureGatewayTemplate,
|
|
||||||
Flags: append(serverFlags,
|
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "quiet",
|
|
||||||
Usage: "Disable startup banner.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
HideHelpCommand: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const s3GatewayTemplate = `NAME:
|
const s3GatewayTemplate = `NAME:
|
||||||
{{.HelpName}} - {{.Usage}}
|
{{.HelpName}} - {{.Usage}}
|
||||||
|
|
||||||
@@ -105,26 +95,71 @@ EXAMPLES:
|
|||||||
$ {{.HelpName}} https://play.minio.io:9000
|
$ {{.HelpName}} https://play.minio.io:9000
|
||||||
`
|
`
|
||||||
|
|
||||||
var s3BackendCmd = cli.Command{
|
const gcsGatewayTemplate = `NAME:
|
||||||
Name: "s3",
|
{{.HelpName}} - {{.Usage}}
|
||||||
Usage: "Amazon Simple Storage Service (S3).",
|
|
||||||
Action: s3GatewayMain,
|
|
||||||
CustomHelpTemplate: s3GatewayTemplate,
|
|
||||||
Flags: append(serverFlags,
|
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "quiet",
|
|
||||||
Usage: "Disable startup banner.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
HideHelpCommand: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
var gatewayCmd = cli.Command{
|
USAGE:
|
||||||
Name: "gateway",
|
{{.HelpName}} {{if .VisibleFlags}}[FLAGS]{{end}} PROJECTID
|
||||||
Usage: "Start object storage gateway.",
|
{{if .VisibleFlags}}
|
||||||
HideHelpCommand: true,
|
FLAGS:
|
||||||
Subcommands: []cli.Command{azureBackendCmd, s3BackendCmd},
|
{{range .VisibleFlags}}{{.}}
|
||||||
}
|
{{end}}{{end}}
|
||||||
|
PROJECTID:
|
||||||
|
GCS project id, there are no defaults this is mandatory.
|
||||||
|
|
||||||
|
ENVIRONMENT VARIABLES:
|
||||||
|
ACCESS:
|
||||||
|
MINIO_ACCESS_KEY: Username or access key of GCS.
|
||||||
|
MINIO_SECRET_KEY: Password or secret key of GCS.
|
||||||
|
|
||||||
|
BROWSER:
|
||||||
|
MINIO_BROWSER: To disable web browser access, set this value to "off".
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
1. Start minio gateway server for GCS backend.
|
||||||
|
$ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
|
||||||
|
(Instructions to generate credentials : https://developers.google.com/identity/protocols/application-default-credentials)
|
||||||
|
$ export MINIO_ACCESS_KEY=accesskey
|
||||||
|
$ export MINIO_SECRET_KEY=secretkey
|
||||||
|
$ {{.HelpName}} mygcsprojectid
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
var (
|
||||||
|
azureBackendCmd = cli.Command{
|
||||||
|
Name: "azure",
|
||||||
|
Usage: "Microsoft Azure Blob Storage.",
|
||||||
|
Action: azureGatewayMain,
|
||||||
|
CustomHelpTemplate: azureGatewayTemplate,
|
||||||
|
Flags: append(serverFlags, globalFlags...),
|
||||||
|
HideHelpCommand: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
s3BackendCmd = cli.Command{
|
||||||
|
Name: "s3",
|
||||||
|
Usage: "Amazon Simple Storage Service (S3).",
|
||||||
|
Action: s3GatewayMain,
|
||||||
|
CustomHelpTemplate: s3GatewayTemplate,
|
||||||
|
Flags: append(serverFlags, globalFlags...),
|
||||||
|
HideHelpCommand: true,
|
||||||
|
}
|
||||||
|
gcsBackendCmd = cli.Command{
|
||||||
|
Name: "gcs",
|
||||||
|
Usage: "Google Cloud Storage.",
|
||||||
|
Action: gcsGatewayMain,
|
||||||
|
CustomHelpTemplate: gcsGatewayTemplate,
|
||||||
|
Flags: append(serverFlags, globalFlags...),
|
||||||
|
HideHelpCommand: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
gatewayCmd = cli.Command{
|
||||||
|
Name: "gateway",
|
||||||
|
Usage: "Start object storage gateway.",
|
||||||
|
Flags: append(serverFlags, globalFlags...),
|
||||||
|
HideHelpCommand: true,
|
||||||
|
Subcommands: []cli.Command{azureBackendCmd, s3BackendCmd, gcsBackendCmd},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Represents the type of the gateway backend.
|
// Represents the type of the gateway backend.
|
||||||
type gatewayBackend string
|
type gatewayBackend string
|
||||||
@@ -132,79 +167,33 @@ type gatewayBackend string
|
|||||||
const (
|
const (
|
||||||
azureBackend gatewayBackend = "azure"
|
azureBackend gatewayBackend = "azure"
|
||||||
s3Backend gatewayBackend = "s3"
|
s3Backend gatewayBackend = "s3"
|
||||||
|
gcsBackend gatewayBackend = "gcs"
|
||||||
// Add more backends here.
|
// Add more backends here.
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns access and secretkey set from environment variables.
|
|
||||||
func mustGetGatewayCredsFromEnv() (accessKey, secretKey string) {
|
|
||||||
// Fetch access keys from environment variables.
|
|
||||||
accessKey = os.Getenv("MINIO_ACCESS_KEY")
|
|
||||||
secretKey = os.Getenv("MINIO_SECRET_KEY")
|
|
||||||
if accessKey == "" || secretKey == "" {
|
|
||||||
fatalIf(errors.New("Missing credentials"), "Access and secret keys are mandatory to run Minio gateway server.")
|
|
||||||
}
|
|
||||||
return accessKey, secretKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set browser setting from environment variables
|
|
||||||
func mustSetBrowserSettingFromEnv() {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize gateway layer depending on the backend type.
|
// Initialize gateway layer depending on the backend type.
|
||||||
// Supported backend types are
|
// Supported backend types are
|
||||||
//
|
//
|
||||||
// - Azure Blob Storage.
|
// - Azure Blob Storage.
|
||||||
|
// - AWS S3.
|
||||||
|
// - Google Cloud Storage.
|
||||||
// - Add your favorite backend here.
|
// - Add your favorite backend here.
|
||||||
func newGatewayLayer(backendType gatewayBackend, endpoint, accessKey, secretKey string, secure bool) (GatewayLayer, error) {
|
func newGatewayLayer(backendType gatewayBackend, arg string) (GatewayLayer, error) {
|
||||||
|
switch backendType {
|
||||||
switch gatewayBackend(backendType) {
|
|
||||||
case azureBackend:
|
case azureBackend:
|
||||||
return newAzureLayer(endpoint, accessKey, secretKey, secure)
|
return newAzureLayer(arg)
|
||||||
case s3Backend:
|
case s3Backend:
|
||||||
return newS3Gateway(endpoint, accessKey, secretKey, secure)
|
return newS3Gateway(arg)
|
||||||
|
case gcsBackend:
|
||||||
|
// FIXME: The following print command is temporary and
|
||||||
|
// will be removed when gcs is ready for production use.
|
||||||
|
log.Println(colorYellow("\n *** Warning: Not Ready for Production ***"))
|
||||||
|
return newGCSGateway(arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("Unrecognized backend type %s", backendType)
|
return nil, fmt.Errorf("Unrecognized backend type %s", backendType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a new gateway config.
|
|
||||||
//
|
|
||||||
// DO NOT save this config, this is meant to be
|
|
||||||
// only used in memory.
|
|
||||||
func newGatewayConfig(accessKey, secretKey, region string) error {
|
|
||||||
// Initialize server config.
|
|
||||||
srvCfg := newServerConfigV18()
|
|
||||||
|
|
||||||
// If env is set for a fresh start, save them to config file.
|
|
||||||
srvCfg.SetCredential(credential{
|
|
||||||
AccessKey: accessKey,
|
|
||||||
SecretKey: secretKey,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set custom region.
|
|
||||||
srvCfg.SetRegion(region)
|
|
||||||
|
|
||||||
// hold the mutex lock before a new config is assigned.
|
|
||||||
// Save the new config globally.
|
|
||||||
// unlock the mutex.
|
|
||||||
serverConfigMu.Lock()
|
|
||||||
serverConfig = srvCfg
|
|
||||||
serverConfigMu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return endpoint.
|
// Return endpoint.
|
||||||
func parseGatewayEndpoint(arg string) (endPoint string, secure bool, err error) {
|
func parseGatewayEndpoint(arg string) (endPoint string, secure bool, err error) {
|
||||||
schemeSpecified := len(strings.Split(arg, "://")) > 1
|
schemeSpecified := len(strings.Split(arg, "://")) > 1
|
||||||
@@ -264,6 +253,9 @@ func azureGatewayMain(ctx *cli.Context) {
|
|||||||
cli.ShowCommandHelpAndExit(ctx, "azure", 1)
|
cli.ShowCommandHelpAndExit(ctx, "azure", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate gateway arguments.
|
||||||
|
fatalIf(validateGatewayArguments(ctx.GlobalString("address"), ctx.Args().First()), "Invalid argument")
|
||||||
|
|
||||||
gatewayMain(ctx, azureBackend)
|
gatewayMain(ctx, azureBackend)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,54 +265,78 @@ func s3GatewayMain(ctx *cli.Context) {
|
|||||||
cli.ShowCommandHelpAndExit(ctx, "s3", 1)
|
cli.ShowCommandHelpAndExit(ctx, "s3", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate gateway arguments.
|
||||||
|
fatalIf(validateGatewayArguments(ctx.GlobalString("address"), ctx.Args().First()), "Invalid argument")
|
||||||
|
|
||||||
gatewayMain(ctx, s3Backend)
|
gatewayMain(ctx, s3Backend)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler for 'minio gateway gcs' command line
|
||||||
|
func gcsGatewayMain(ctx *cli.Context) {
|
||||||
|
if ctx.Args().Present() && ctx.Args().First() == "help" {
|
||||||
|
cli.ShowCommandHelpAndExit(ctx, "gcs", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidGCSProjectIDFormat(ctx.Args().First()) {
|
||||||
|
errorIf(errGCSInvalidProjectID, "Unable to start GCS gateway with %s", ctx.Args().First())
|
||||||
|
cli.ShowCommandHelpAndExit(ctx, "gcs", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
gatewayMain(ctx, gcsBackend)
|
||||||
|
}
|
||||||
|
|
||||||
// Handler for 'minio gateway'.
|
// Handler for 'minio gateway'.
|
||||||
func gatewayMain(ctx *cli.Context, backendType gatewayBackend) {
|
func gatewayMain(ctx *cli.Context, backendType gatewayBackend) {
|
||||||
// Fetch access and secret key from env.
|
|
||||||
accessKey, secretKey := mustGetGatewayCredsFromEnv()
|
|
||||||
|
|
||||||
// Fetch browser env setting
|
|
||||||
mustSetBrowserSettingFromEnv()
|
|
||||||
|
|
||||||
// Initialize new gateway config.
|
|
||||||
|
|
||||||
newGatewayConfig(accessKey, secretKey, globalMinioDefaultRegion)
|
|
||||||
|
|
||||||
// Get quiet flag from command line argument.
|
// Get quiet flag from command line argument.
|
||||||
quietFlag := ctx.Bool("quiet") || ctx.GlobalBool("quiet")
|
quietFlag := ctx.Bool("quiet") || ctx.GlobalBool("quiet")
|
||||||
if quietFlag {
|
if quietFlag {
|
||||||
log.EnableQuiet()
|
log.EnableQuiet()
|
||||||
}
|
}
|
||||||
|
|
||||||
serverAddr := ctx.String("address")
|
// Fetch address option
|
||||||
endpointAddr := ctx.Args().Get(0)
|
gatewayAddr := ctx.GlobalString("address")
|
||||||
err := validateGatewayArguments(serverAddr, endpointAddr)
|
if gatewayAddr == ":"+globalMinioPort {
|
||||||
fatalIf(err, "Invalid argument")
|
gatewayAddr = ctx.String("address")
|
||||||
|
}
|
||||||
|
|
||||||
// Second argument is endpoint. If no endpoint is specified then the
|
// Handle common command args.
|
||||||
// gateway implementation should use a default setting.
|
handleCommonCmdArgs(ctx)
|
||||||
endPoint, secure, err := parseGatewayEndpoint(endpointAddr)
|
|
||||||
fatalIf(err, "Unable to parse endpoint")
|
|
||||||
|
|
||||||
// Create certs path for SSL configuration.
|
// Handle common env vars.
|
||||||
fatalIf(createConfigDir(), "Unable to create configuration directory")
|
handleCommonEnvVars()
|
||||||
|
|
||||||
newObject, err := newGatewayLayer(backendType, endPoint, accessKey, secretKey, secure)
|
// Validate if we have access, secret set through environment.
|
||||||
fatalIf(err, "Unable to initialize gateway layer")
|
if !globalIsEnvCreds {
|
||||||
|
fatalIf(fmt.Errorf("Access and Secret keys should be set through ENVs for backend [%s]", backendType), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create certs path.
|
||||||
|
fatalIf(createConfigDir(), "Unable to create configuration directories.")
|
||||||
|
|
||||||
|
// Initialize gateway config.
|
||||||
|
initConfig()
|
||||||
|
|
||||||
|
// Enable loggers as per configuration file.
|
||||||
|
enableLoggers()
|
||||||
|
|
||||||
|
// Init the error tracing module.
|
||||||
|
initError()
|
||||||
|
|
||||||
|
// Check and load SSL certificates.
|
||||||
|
var err error
|
||||||
|
globalPublicCerts, globalRootCAs, globalTLSCertificate, globalIsSSL, err = getSSLConfig()
|
||||||
|
fatalIf(err, "Invalid SSL certificate file")
|
||||||
|
|
||||||
initNSLock(false) // Enable local namespace lock.
|
initNSLock(false) // Enable local namespace lock.
|
||||||
|
|
||||||
router := mux.NewRouter().SkipClean(true)
|
newObject, err := newGatewayLayer(backendType, ctx.Args().First())
|
||||||
|
fatalIf(err, "Unable to initialize gateway layer")
|
||||||
|
|
||||||
// credentials Envs are set globally.
|
router := mux.NewRouter().SkipClean(true)
|
||||||
globalIsEnvCreds = true
|
|
||||||
|
|
||||||
// Register web router when its enabled.
|
// Register web router when its enabled.
|
||||||
if globalIsBrowserEnabled {
|
if globalIsBrowserEnabled {
|
||||||
aerr := registerWebRouter(router)
|
fatalIf(registerWebRouter(router), "Unable to configure web browser")
|
||||||
fatalIf(aerr, "Unable to configure web browser")
|
|
||||||
}
|
}
|
||||||
registerGatewayAPIRouter(router, newObject)
|
registerGatewayAPIRouter(router, newObject)
|
||||||
|
|
||||||
@@ -353,22 +369,15 @@ func gatewayMain(ctx *cli.Context, backendType gatewayBackend) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apiServer := NewServerMux(serverAddr, registerHandlers(router, handlerFns...))
|
globalHTTPServer = miniohttp.NewServer([]string{gatewayAddr}, registerHandlers(router, handlerFns...), globalTLSCertificate)
|
||||||
|
|
||||||
_, _, globalIsSSL, err = getSSLConfig()
|
|
||||||
fatalIf(err, "Invalid SSL key file")
|
|
||||||
|
|
||||||
// Start server, automatically configures TLS if certs are available.
|
// Start server, automatically configures TLS if certs are available.
|
||||||
go func() {
|
go func() {
|
||||||
cert, key := "", ""
|
globalHTTPServerErrorCh <- globalHTTPServer.Start()
|
||||||
if globalIsSSL {
|
|
||||||
cert, key = getPublicCertFile(), getPrivateKeyFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
aerr := apiServer.ListenAndServe(cert, key)
|
|
||||||
fatalIf(aerr, "Failed to start minio server")
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
signal.Notify(globalOSSignalCh, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
// Once endpoints are finalized, initialize the new object api.
|
// Once endpoints are finalized, initialize the new object api.
|
||||||
globalObjLayerMutex.Lock()
|
globalObjLayerMutex.Lock()
|
||||||
globalObjectAPI = newObject
|
globalObjectAPI = newObject
|
||||||
@@ -377,15 +386,21 @@ func gatewayMain(ctx *cli.Context, backendType gatewayBackend) {
|
|||||||
// Prints the formatted startup message once object layer is initialized.
|
// Prints the formatted startup message once object layer is initialized.
|
||||||
if !quietFlag {
|
if !quietFlag {
|
||||||
mode := ""
|
mode := ""
|
||||||
if gatewayBackend(backendType) == azureBackend {
|
switch gatewayBackend(backendType) {
|
||||||
|
case azureBackend:
|
||||||
mode = globalMinioModeGatewayAzure
|
mode = globalMinioModeGatewayAzure
|
||||||
} else if gatewayBackend(backendType) == s3Backend {
|
case gcsBackend:
|
||||||
|
mode = globalMinioModeGatewayGCS
|
||||||
|
case s3Backend:
|
||||||
mode = globalMinioModeGatewayS3
|
mode = globalMinioModeGatewayS3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check update mode.
|
||||||
checkUpdate(mode)
|
checkUpdate(mode)
|
||||||
apiEndpoints := getAPIEndpoints(apiServer.Addr)
|
|
||||||
printGatewayStartupMessage(apiEndpoints, accessKey, secretKey, backendType)
|
// Print gateway startup message.
|
||||||
|
printGatewayStartupMessage(getAPIEndpoints(gatewayAddr), backendType)
|
||||||
}
|
}
|
||||||
|
|
||||||
<-globalServiceDoneCh
|
handleSignals()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -53,28 +52,6 @@ func TestParseGatewayEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetBrowserFromEnv(t *testing.T) {
|
|
||||||
browser := os.Getenv("MINIO_BROWSER")
|
|
||||||
|
|
||||||
os.Setenv("MINIO_BROWSER", "on")
|
|
||||||
mustSetBrowserSettingFromEnv()
|
|
||||||
if globalIsBrowserEnabled != true {
|
|
||||||
t.Errorf("Expected the response status to be `%t`, but instead found `%t`", globalIsBrowserEnabled, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Setenv("MINIO_BROWSER", "off")
|
|
||||||
mustSetBrowserSettingFromEnv()
|
|
||||||
if globalIsBrowserEnabled != false {
|
|
||||||
t.Errorf("Expected the response status to be `%t`, but instead found `%t`", globalIsBrowserEnabled, true)
|
|
||||||
}
|
|
||||||
os.Setenv("MINIO_BROWSER", "")
|
|
||||||
mustSetBrowserSettingFromEnv()
|
|
||||||
if globalIsBrowserEnabled != false {
|
|
||||||
t.Errorf("Expected the response status to be `%t`, but instead found `%t`", globalIsBrowserEnabled, true)
|
|
||||||
}
|
|
||||||
os.Setenv("MINIO_BROWSER", browser)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test validateGatewayArguments
|
// Test validateGatewayArguments
|
||||||
func TestValidateGatewayArguments(t *testing.T) {
|
func TestValidateGatewayArguments(t *testing.T) {
|
||||||
nonLoopBackIPs := localIP4.FuncMatch(func(ip string, matchString string) bool {
|
nonLoopBackIPs := localIP4.FuncMatch(func(ip string, matchString string) bool {
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ type GatewayLayer interface {
|
|||||||
GetBucketPolicies(string) (policy.BucketAccessPolicy, error)
|
GetBucketPolicies(string) (policy.BucketAccessPolicy, error)
|
||||||
DeleteBucketPolicies(string) error
|
DeleteBucketPolicies(string) error
|
||||||
AnonListObjects(bucket, prefix, marker, delimiter string, maxKeys int) (result ListObjectsInfo, err error)
|
AnonListObjects(bucket, prefix, marker, delimiter string, maxKeys int) (result ListObjectsInfo, err error)
|
||||||
|
AnonListObjectsV2(bucket, prefix, continuationToken string, fetchOwner bool, delimiter string, maxKeys int) (result ListObjectsV2Info, err error)
|
||||||
|
ListObjectsV2(bucket, prefix, continuationToken string, fetchOwner bool, delimiter string, maxKeys int) (result ListObjectsV2Info, err error)
|
||||||
AnonGetBucketInfo(bucket string) (bucketInfo BucketInfo, err error)
|
AnonGetBucketInfo(bucket string) (bucketInfo BucketInfo, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-13
@@ -24,14 +24,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AnonPutObject creates a new object anonymously with the incoming data,
|
// AnonPutObject creates a new object anonymously with the incoming data,
|
||||||
func (l *s3Objects) AnonPutObject(bucket string, object string, size int64, data io.Reader, metadata map[string]string, sha256sum string) (ObjectInfo, error) {
|
func (l *s3Objects) AnonPutObject(bucket string, object string, size int64, data io.Reader, metadata map[string]string, sha256sum string) (objInfo ObjectInfo, e error) {
|
||||||
var sha256sumBytes []byte
|
var sha256sumBytes []byte
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if sha256sum != "" {
|
if sha256sum != "" {
|
||||||
sha256sumBytes, err = hex.DecodeString(sha256sum)
|
sha256sumBytes, err = hex.DecodeString(sha256sum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, s3ToObjectError(traceError(err), bucket, object)
|
return objInfo, s3ToObjectError(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,14 +40,14 @@ func (l *s3Objects) AnonPutObject(bucket string, object string, size int64, data
|
|||||||
if md5sum != "" {
|
if md5sum != "" {
|
||||||
md5sumBytes, err = hex.DecodeString(md5sum)
|
md5sumBytes, err = hex.DecodeString(md5sum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, s3ToObjectError(traceError(err), bucket, object)
|
return objInfo, s3ToObjectError(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
delete(metadata, "etag")
|
delete(metadata, "etag")
|
||||||
}
|
}
|
||||||
|
|
||||||
oi, err := l.anonClient.PutObject(bucket, object, size, data, md5sumBytes, sha256sumBytes, toMinioClientMetadata(metadata))
|
oi, err := l.anonClient.PutObject(bucket, object, size, data, md5sumBytes, sha256sumBytes, toMinioClientMetadata(metadata))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, s3ToObjectError(traceError(err), bucket, object)
|
return objInfo, s3ToObjectError(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromMinioClientObjectInfo(bucket, oi), nil
|
return fromMinioClientObjectInfo(bucket, oi), nil
|
||||||
@@ -74,37 +74,47 @@ func (l *s3Objects) AnonGetObject(bucket string, key string, startOffset int64,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AnonGetObjectInfo - Get object info anonymously
|
// AnonGetObjectInfo - Get object info anonymously
|
||||||
func (l *s3Objects) AnonGetObjectInfo(bucket string, object string) (ObjectInfo, error) {
|
func (l *s3Objects) AnonGetObjectInfo(bucket string, object string) (objInfo ObjectInfo, e error) {
|
||||||
r := minio.NewHeadReqHeaders()
|
r := minio.NewHeadReqHeaders()
|
||||||
oi, err := l.anonClient.StatObject(bucket, object, r)
|
oi, err := l.anonClient.StatObject(bucket, object, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, s3ToObjectError(traceError(err), bucket, object)
|
return objInfo, s3ToObjectError(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromMinioClientObjectInfo(bucket, oi), nil
|
return fromMinioClientObjectInfo(bucket, oi), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnonListObjects - List objects anonymously
|
// AnonListObjects - List objects anonymously
|
||||||
func (l *s3Objects) AnonListObjects(bucket string, prefix string, marker string, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
func (l *s3Objects) AnonListObjects(bucket string, prefix string, marker string, delimiter string, maxKeys int) (loi ListObjectsInfo, e error) {
|
||||||
result, err := l.anonClient.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
|
result, err := l.anonClient.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListObjectsInfo{}, s3ToObjectError(traceError(err), bucket)
|
return loi, s3ToObjectError(traceError(err), bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromMinioClientListBucketResult(bucket, result), nil
|
return fromMinioClientListBucketResult(bucket, result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AnonListObjectsV2 - List objects in V2 mode, anonymously
|
||||||
|
func (l *s3Objects) AnonListObjectsV2(bucket, prefix, continuationToken string, fetchOwner bool, delimiter string, maxKeys int) (loi ListObjectsV2Info, e error) {
|
||||||
|
result, err := l.anonClient.ListObjectsV2(bucket, prefix, continuationToken, fetchOwner, delimiter, maxKeys)
|
||||||
|
if err != nil {
|
||||||
|
return loi, s3ToObjectError(traceError(err), bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fromMinioClientListBucketV2Result(bucket, result), nil
|
||||||
|
}
|
||||||
|
|
||||||
// AnonGetBucketInfo - Get bucket metadata anonymously.
|
// AnonGetBucketInfo - Get bucket metadata anonymously.
|
||||||
func (l *s3Objects) AnonGetBucketInfo(bucket string) (BucketInfo, error) {
|
func (l *s3Objects) AnonGetBucketInfo(bucket string) (bi BucketInfo, e error) {
|
||||||
if exists, err := l.anonClient.BucketExists(bucket); err != nil {
|
if exists, err := l.anonClient.BucketExists(bucket); err != nil {
|
||||||
return BucketInfo{}, s3ToObjectError(traceError(err), bucket)
|
return bi, s3ToObjectError(traceError(err), bucket)
|
||||||
} else if !exists {
|
} else if !exists {
|
||||||
return BucketInfo{}, traceError(BucketNotFound{Bucket: bucket})
|
return bi, traceError(BucketNotFound{Bucket: bucket})
|
||||||
}
|
}
|
||||||
|
|
||||||
buckets, err := l.anonClient.ListBuckets()
|
buckets, err := l.anonClient.ListBuckets()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return BucketInfo{}, s3ToObjectError(traceError(err), bucket)
|
return bi, s3ToObjectError(traceError(err), bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, bi := range buckets {
|
for _, bi := range buckets {
|
||||||
@@ -118,5 +128,5 @@ func (l *s3Objects) AnonGetBucketInfo(bucket string) (BucketInfo, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return BucketInfo{}, traceError(BucketNotFound{Bucket: bucket})
|
return bi, traceError(BucketNotFound{Bucket: bucket})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ func (l *s3Objects) HealObject(bucket string, object string) (int, int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListObjectsHeal - Not relevant.
|
// ListObjectsHeal - Not relevant.
|
||||||
func (l *s3Objects) ListObjectsHeal(bucket string, prefix string, marker string, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
func (l *s3Objects) ListObjectsHeal(bucket string, prefix string, marker string, delimiter string, maxKeys int) (loi ListObjectsInfo, e error) {
|
||||||
return ListObjectsInfo{}, traceError(NotImplemented{})
|
return loi, traceError(NotImplemented{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUploadsHeal - Not relevant.
|
// ListUploadsHeal - Not relevant.
|
||||||
func (l *s3Objects) ListUploadsHeal(bucket string, prefix string, marker string, uploadIDMarker string, delimiter string, maxUploads int) (ListMultipartsInfo, error) {
|
func (l *s3Objects) ListUploadsHeal(bucket string, prefix string, marker string, uploadIDMarker string, delimiter string, maxUploads int) (lmi ListMultipartsInfo, e error) {
|
||||||
return ListMultipartsInfo{}, traceError(NotImplemented{})
|
return lmi, traceError(NotImplemented{})
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-33
@@ -17,11 +17,12 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
minio "github.com/minio/minio-go"
|
minio "github.com/minio/minio-go"
|
||||||
"github.com/minio/minio-go/pkg/policy"
|
"github.com/minio/minio-go/pkg/policy"
|
||||||
)
|
)
|
||||||
@@ -97,14 +98,30 @@ type s3Objects struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newS3Gateway returns s3 gatewaylayer
|
// newS3Gateway returns s3 gatewaylayer
|
||||||
func newS3Gateway(endpoint string, accessKey, secretKey string, secure bool) (GatewayLayer, error) {
|
func newS3Gateway(host string) (GatewayLayer, error) {
|
||||||
if endpoint == "" {
|
|
||||||
endpoint = "s3.amazonaws.com"
|
var err error
|
||||||
secure = true
|
var endpoint string
|
||||||
|
var secure = true
|
||||||
|
|
||||||
|
// Validate host parameters.
|
||||||
|
if host != "" {
|
||||||
|
// Override default params if the host is provided
|
||||||
|
endpoint, secure, err = parseGatewayEndpoint(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default endpoint parameters
|
||||||
|
if endpoint == "" {
|
||||||
|
endpoint = "s3.amazonaws.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
creds := serverConfig.GetCredential()
|
||||||
|
|
||||||
// Initialize minio client object.
|
// Initialize minio client object.
|
||||||
client, err := minio.NewCore(endpoint, accessKey, secretKey, secure)
|
client, err := minio.NewCore(endpoint, creds.AccessKey, creds.SecretKey, secure)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -128,8 +145,8 @@ func (l *s3Objects) Shutdown() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StorageInfo is not relevant to S3 backend.
|
// StorageInfo is not relevant to S3 backend.
|
||||||
func (l *s3Objects) StorageInfo() StorageInfo {
|
func (l *s3Objects) StorageInfo() (si StorageInfo) {
|
||||||
return StorageInfo{}
|
return si
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeBucket creates a new container on S3 backend.
|
// MakeBucket creates a new container on S3 backend.
|
||||||
@@ -142,10 +159,10 @@ func (l *s3Objects) MakeBucketWithLocation(bucket, location string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketInfo gets bucket metadata..
|
// GetBucketInfo gets bucket metadata..
|
||||||
func (l *s3Objects) GetBucketInfo(bucket string) (BucketInfo, error) {
|
func (l *s3Objects) GetBucketInfo(bucket string) (bi BucketInfo, e error) {
|
||||||
buckets, err := l.Client.ListBuckets()
|
buckets, err := l.Client.ListBuckets()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return BucketInfo{}, s3ToObjectError(traceError(err), bucket)
|
return bi, s3ToObjectError(traceError(err), bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, bi := range buckets {
|
for _, bi := range buckets {
|
||||||
@@ -159,7 +176,7 @@ func (l *s3Objects) GetBucketInfo(bucket string) (BucketInfo, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return BucketInfo{}, traceError(BucketNotFound{Bucket: bucket})
|
return bi, traceError(BucketNotFound{Bucket: bucket})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListBuckets lists all S3 buckets
|
// ListBuckets lists all S3 buckets
|
||||||
@@ -190,20 +207,20 @@ func (l *s3Objects) DeleteBucket(bucket string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListObjects lists all blobs in S3 bucket filtered by prefix
|
// ListObjects lists all blobs in S3 bucket filtered by prefix
|
||||||
func (l *s3Objects) ListObjects(bucket string, prefix string, marker string, delimiter string, maxKeys int) (ListObjectsInfo, error) {
|
func (l *s3Objects) ListObjects(bucket string, prefix string, marker string, delimiter string, maxKeys int) (loi ListObjectsInfo, e error) {
|
||||||
result, err := l.Client.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
|
result, err := l.Client.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListObjectsInfo{}, s3ToObjectError(traceError(err), bucket)
|
return loi, s3ToObjectError(traceError(err), bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromMinioClientListBucketResult(bucket, result), nil
|
return fromMinioClientListBucketResult(bucket, result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListObjectsV2 lists all blobs in S3 bucket filtered by prefix
|
// ListObjectsV2 lists all blobs in S3 bucket filtered by prefix
|
||||||
func (l *s3Objects) ListObjectsV2(bucket, prefix, continuationToken string, fetchOwner bool, delimiter string, maxKeys int) (ListObjectsV2Info, error) {
|
func (l *s3Objects) ListObjectsV2(bucket, prefix, continuationToken string, fetchOwner bool, delimiter string, maxKeys int) (loi ListObjectsV2Info, e error) {
|
||||||
result, err := l.Client.ListObjectsV2(bucket, prefix, continuationToken, fetchOwner, delimiter, maxKeys)
|
result, err := l.Client.ListObjectsV2(bucket, prefix, continuationToken, fetchOwner, delimiter, maxKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListObjectsV2Info{}, s3ToObjectError(traceError(err), bucket)
|
return loi, s3ToObjectError(traceError(err), bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromMinioClientListBucketV2Result(bucket, result), nil
|
return fromMinioClientListBucketV2Result(bucket, result), nil
|
||||||
@@ -294,7 +311,7 @@ func fromMinioClientObjectInfo(bucket string, oi minio.ObjectInfo) ObjectInfo {
|
|||||||
Name: oi.Key,
|
Name: oi.Key,
|
||||||
ModTime: oi.LastModified,
|
ModTime: oi.LastModified,
|
||||||
Size: oi.Size,
|
Size: oi.Size,
|
||||||
ETag: oi.ETag,
|
ETag: canonicalizeETag(oi.ETag),
|
||||||
UserDefined: userDefined,
|
UserDefined: userDefined,
|
||||||
ContentType: oi.ContentType,
|
ContentType: oi.ContentType,
|
||||||
ContentEncoding: oi.Metadata.Get("Content-Encoding"),
|
ContentEncoding: oi.Metadata.Get("Content-Encoding"),
|
||||||
@@ -313,14 +330,14 @@ func (l *s3Objects) GetObjectInfo(bucket string, object string) (objInfo ObjectI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PutObject creates a new object with the incoming data,
|
// PutObject creates a new object with the incoming data,
|
||||||
func (l *s3Objects) PutObject(bucket string, object string, size int64, data io.Reader, metadata map[string]string, sha256sum string) (ObjectInfo, error) {
|
func (l *s3Objects) PutObject(bucket string, object string, size int64, data io.Reader, metadata map[string]string, sha256sum string) (objInfo ObjectInfo, e error) {
|
||||||
var sha256sumBytes []byte
|
var sha256sumBytes []byte
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if sha256sum != "" {
|
if sha256sum != "" {
|
||||||
sha256sumBytes, err = hex.DecodeString(sha256sum)
|
sha256sumBytes, err = hex.DecodeString(sha256sum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, s3ToObjectError(traceError(err), bucket, object)
|
return objInfo, s3ToObjectError(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,29 +346,29 @@ func (l *s3Objects) PutObject(bucket string, object string, size int64, data io.
|
|||||||
if md5sum != "" {
|
if md5sum != "" {
|
||||||
md5sumBytes, err = hex.DecodeString(md5sum)
|
md5sumBytes, err = hex.DecodeString(md5sum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, s3ToObjectError(traceError(err), bucket, object)
|
return objInfo, s3ToObjectError(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
delete(metadata, "etag")
|
delete(metadata, "etag")
|
||||||
}
|
}
|
||||||
|
|
||||||
oi, err := l.Client.PutObject(bucket, object, size, data, md5sumBytes, sha256sumBytes, toMinioClientMetadata(metadata))
|
oi, err := l.Client.PutObject(bucket, object, size, data, md5sumBytes, sha256sumBytes, toMinioClientMetadata(metadata))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, s3ToObjectError(traceError(err), bucket, object)
|
return objInfo, s3ToObjectError(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromMinioClientObjectInfo(bucket, oi), nil
|
return fromMinioClientObjectInfo(bucket, oi), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyObject copies a blob from source container to destination container.
|
// CopyObject copies a blob from source container to destination container.
|
||||||
func (l *s3Objects) CopyObject(srcBucket string, srcObject string, destBucket string, destObject string, metadata map[string]string) (ObjectInfo, error) {
|
func (l *s3Objects) CopyObject(srcBucket string, srcObject string, destBucket string, destObject string, metadata map[string]string) (objInfo ObjectInfo, e error) {
|
||||||
err := l.Client.CopyObject(destBucket, destObject, path.Join(srcBucket, srcObject), minio.CopyConditions{})
|
err := l.Client.CopyObject(destBucket, destObject, path.Join(srcBucket, srcObject), minio.CopyConditions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, s3ToObjectError(traceError(err), srcBucket, srcObject)
|
return objInfo, s3ToObjectError(traceError(err), srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
oi, err := l.GetObjectInfo(destBucket, destObject)
|
oi, err := l.GetObjectInfo(destBucket, destObject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, s3ToObjectError(traceError(err), destBucket, destObject)
|
return objInfo, s3ToObjectError(traceError(err), destBucket, destObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
return oi, nil
|
return oi, nil
|
||||||
@@ -406,10 +423,10 @@ func fromMinioClientListMultipartsInfo(lmur minio.ListMultipartUploadsResult) Li
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListMultipartUploads lists all multipart uploads.
|
// ListMultipartUploads lists all multipart uploads.
|
||||||
func (l *s3Objects) ListMultipartUploads(bucket string, prefix string, keyMarker string, uploadIDMarker string, delimiter string, maxUploads int) (ListMultipartsInfo, error) {
|
func (l *s3Objects) ListMultipartUploads(bucket string, prefix string, keyMarker string, uploadIDMarker string, delimiter string, maxUploads int) (lmi ListMultipartsInfo, e error) {
|
||||||
result, err := l.Client.ListMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
result, err := l.Client.ListMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListMultipartsInfo{}, err
|
return lmi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromMinioClientListMultipartsInfo(result), nil
|
return fromMinioClientListMultipartsInfo(result), nil
|
||||||
@@ -455,20 +472,20 @@ func fromMinioClientObjectPart(op minio.ObjectPart) PartInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PutObjectPart puts a part of object in bucket
|
// PutObjectPart puts a part of object in bucket
|
||||||
func (l *s3Objects) PutObjectPart(bucket string, object string, uploadID string, partID int, size int64, data io.Reader, md5Hex string, sha256sum string) (PartInfo, error) {
|
func (l *s3Objects) PutObjectPart(bucket string, object string, uploadID string, partID int, size int64, data io.Reader, md5Hex string, sha256sum string) (pi PartInfo, e error) {
|
||||||
md5HexBytes, err := hex.DecodeString(md5Hex)
|
md5HexBytes, err := hex.DecodeString(md5Hex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PartInfo{}, err
|
return pi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sha256sumBytes, err := hex.DecodeString(sha256sum)
|
sha256sumBytes, err := hex.DecodeString(sha256sum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PartInfo{}, err
|
return pi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := l.Client.PutObjectPart(bucket, object, uploadID, partID, size, data, md5HexBytes, sha256sumBytes)
|
info, err := l.Client.PutObjectPart(bucket, object, uploadID, partID, size, data, md5HexBytes, sha256sumBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PartInfo{}, err
|
return pi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromMinioClientObjectPart(info), nil
|
return fromMinioClientObjectPart(info), nil
|
||||||
@@ -500,10 +517,10 @@ func fromMinioClientListPartsInfo(lopr minio.ListObjectPartsResult) ListPartsInf
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListObjectParts returns all object parts for specified object in specified bucket
|
// ListObjectParts returns all object parts for specified object in specified bucket
|
||||||
func (l *s3Objects) ListObjectParts(bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (ListPartsInfo, error) {
|
func (l *s3Objects) ListObjectParts(bucket string, object string, uploadID string, partNumberMarker int, maxParts int) (lpi ListPartsInfo, e error) {
|
||||||
result, err := l.Client.ListObjectParts(bucket, object, uploadID, partNumberMarker, maxParts)
|
result, err := l.Client.ListObjectParts(bucket, object, uploadID, partNumberMarker, maxParts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListPartsInfo{}, err
|
return lpi, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromMinioClientListPartsInfo(result), nil
|
return fromMinioClientListPartsInfo(result), nil
|
||||||
@@ -532,10 +549,10 @@ func toMinioClientCompleteParts(parts []completePart) []minio.CompletePart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CompleteMultipartUpload completes ongoing multipart upload and finalizes object
|
// CompleteMultipartUpload completes ongoing multipart upload and finalizes object
|
||||||
func (l *s3Objects) CompleteMultipartUpload(bucket string, object string, uploadID string, uploadedParts []completePart) (ObjectInfo, error) {
|
func (l *s3Objects) CompleteMultipartUpload(bucket string, object string, uploadID string, uploadedParts []completePart) (oi ObjectInfo, e error) {
|
||||||
err := l.Client.CompleteMultipartUpload(bucket, object, uploadID, toMinioClientCompleteParts(uploadedParts))
|
err := l.Client.CompleteMultipartUpload(bucket, object, uploadID, toMinioClientCompleteParts(uploadedParts))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, s3ToObjectError(traceError(err), bucket, object)
|
return oi, s3ToObjectError(traceError(err), bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
return l.GetObjectInfo(bucket, object)
|
return l.GetObjectInfo(bucket, object)
|
||||||
|
|||||||
+14
-17
@@ -18,28 +18,19 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Prints the formatted startup message.
|
// Prints the formatted startup message.
|
||||||
func printGatewayStartupMessage(apiEndPoints []string, accessKey, secretKey string, backendType gatewayBackend) {
|
func printGatewayStartupMessage(apiEndPoints []string, backendType gatewayBackend) {
|
||||||
|
strippedAPIEndpoints := stripStandardPorts(apiEndPoints)
|
||||||
|
|
||||||
// Prints credential.
|
// Prints credential.
|
||||||
printGatewayCommonMsg(apiEndPoints, accessKey, secretKey)
|
printGatewayCommonMsg(strippedAPIEndpoints)
|
||||||
|
|
||||||
// Prints `mc` cli configuration message chooses
|
// Prints `mc` cli configuration message chooses
|
||||||
// first endpoint as default.
|
// first endpoint as default.
|
||||||
endPoint := apiEndPoints[0]
|
printCLIAccessMsg(strippedAPIEndpoints[0], fmt.Sprintf("my%s", backendType))
|
||||||
|
|
||||||
// Configure 'mc', following block prints platform specific information for minio client.
|
|
||||||
log.Println(colorBlue("\nCommand-line Access: ") + mcQuickStartGuide)
|
|
||||||
if runtime.GOOS == globalWindowsOSName {
|
|
||||||
mcMessage := fmt.Sprintf("$ mc.exe config host add my%s %s %s %s", backendType, endPoint, accessKey, secretKey)
|
|
||||||
log.Println(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
|
||||||
} else {
|
|
||||||
mcMessage := fmt.Sprintf("$ mc config host add my%s %s %s %s", backendType, endPoint, accessKey, secretKey)
|
|
||||||
log.Println(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prints documentation message.
|
// Prints documentation message.
|
||||||
printObjectAPIMsg()
|
printObjectAPIMsg()
|
||||||
@@ -52,10 +43,16 @@ func printGatewayStartupMessage(apiEndPoints []string, accessKey, secretKey stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prints common server startup message. Prints credential, region and browser access.
|
// Prints common server startup message. Prints credential, region and browser access.
|
||||||
func printGatewayCommonMsg(apiEndpoints []string, accessKey, secretKey string) {
|
func printGatewayCommonMsg(apiEndpoints []string) {
|
||||||
|
// Get saved credentials.
|
||||||
|
cred := serverConfig.GetCredential()
|
||||||
|
|
||||||
apiEndpointStr := strings.Join(apiEndpoints, " ")
|
apiEndpointStr := strings.Join(apiEndpoints, " ")
|
||||||
// Colorize the message and print.
|
// Colorize the message and print.
|
||||||
log.Println(colorBlue("\nEndpoint: ") + colorBold(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 1), apiEndpointStr)))
|
log.Println(colorBlue("\nEndpoint: ") + colorBold(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 1), apiEndpointStr)))
|
||||||
log.Println(colorBlue("AccessKey: ") + colorBold(fmt.Sprintf("%s ", accessKey)))
|
log.Println(colorBlue("AccessKey: ") + colorBold(fmt.Sprintf("%s ", cred.AccessKey)))
|
||||||
log.Println(colorBlue("SecretKey: ") + colorBold(fmt.Sprintf("%s ", secretKey)))
|
log.Println(colorBlue("SecretKey: ") + colorBold(fmt.Sprintf("%s ", cred.SecretKey)))
|
||||||
|
|
||||||
|
log.Println(colorBlue("\nBrowser Access:"))
|
||||||
|
log.Println(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 3), apiEndpointStr))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,24 @@ import "testing"
|
|||||||
|
|
||||||
// Test printing Gateway common message.
|
// Test printing Gateway common message.
|
||||||
func TestPrintGatewayCommonMessage(t *testing.T) {
|
func TestPrintGatewayCommonMessage(t *testing.T) {
|
||||||
apiEndpoints := []string{"127.0.0.1:9000"}
|
root, err := newTestConfig(globalMinioDefaultRegion)
|
||||||
printGatewayCommonMsg(apiEndpoints, "abcd1", "abcd123")
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer removeAll(root)
|
||||||
|
|
||||||
|
apiEndpoints := []string{"http://127.0.0.1:9000"}
|
||||||
|
printGatewayCommonMsg(apiEndpoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test print gateway startup message.
|
// Test print gateway startup message.
|
||||||
func TestPrintGatewayStartupMessage(t *testing.T) {
|
func TestPrintGatewayStartupMessage(t *testing.T) {
|
||||||
apiEndpoints := []string{"127.0.0.1:9000"}
|
root, err := newTestConfig(globalMinioDefaultRegion)
|
||||||
printGatewayStartupMessage(apiEndpoints, "abcd1", "abcd123", "azure")
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer removeAll(root)
|
||||||
|
|
||||||
|
apiEndpoints := []string{"http://127.0.0.1:9000"}
|
||||||
|
printGatewayStartupMessage(apiEndpoints, "azure")
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-4
@@ -17,20 +17,32 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
humanize "github.com/dustin/go-humanize"
|
humanize "github.com/dustin/go-humanize"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
miniohttp "github.com/minio/minio/pkg/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// minio configuration related constants.
|
// minio configuration related constants.
|
||||||
const (
|
const (
|
||||||
globalMinioCertExpireWarnDays = time.Hour * 24 * 30 // 30 days.
|
globalMinioCertExpireWarnDays = time.Hour * 24 * 30 // 30 days.
|
||||||
|
|
||||||
globalMinioDefaultRegion = ""
|
globalMinioDefaultRegion = ""
|
||||||
globalMinioDefaultOwnerID = "minio"
|
// This is a sha256 output of ``arn:aws:iam::minio:user/admin``,
|
||||||
|
// this is kept in present form to be compatible with S3 owner ID
|
||||||
|
// requirements -
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// The canonical user ID is the Amazon S3–only concept.
|
||||||
|
// It is 64-character obfuscated version of the account ID.
|
||||||
|
// ```
|
||||||
|
// http://docs.aws.amazon.com/AmazonS3/latest/dev/example-walkthroughs-managing-access-example4.html
|
||||||
|
globalMinioDefaultOwnerID = "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
|
||||||
globalMinioDefaultStorageClass = "STANDARD"
|
globalMinioDefaultStorageClass = "STANDARD"
|
||||||
globalWindowsOSName = "windows"
|
globalWindowsOSName = "windows"
|
||||||
globalNetBSDOSName = "netbsd"
|
globalNetBSDOSName = "netbsd"
|
||||||
@@ -40,6 +52,7 @@ const (
|
|||||||
globalMinioModeDistXL = "mode-server-distributed-xl"
|
globalMinioModeDistXL = "mode-server-distributed-xl"
|
||||||
globalMinioModeGatewayAzure = "mode-gateway-azure"
|
globalMinioModeGatewayAzure = "mode-gateway-azure"
|
||||||
globalMinioModeGatewayS3 = "mode-gateway-s3"
|
globalMinioModeGatewayS3 = "mode-gateway-s3"
|
||||||
|
globalMinioModeGatewayGCS = "mode-gateway-gcs"
|
||||||
// Add new global values here.
|
// Add new global values here.
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,6 +109,12 @@ var (
|
|||||||
// IsSSL indicates if the server is configured with SSL.
|
// IsSSL indicates if the server is configured with SSL.
|
||||||
globalIsSSL bool
|
globalIsSSL bool
|
||||||
|
|
||||||
|
globalTLSCertificate *tls.Certificate
|
||||||
|
|
||||||
|
globalHTTPServer *miniohttp.Server
|
||||||
|
globalHTTPServerErrorCh = make(chan error)
|
||||||
|
globalOSSignalCh = make(chan os.Signal, 1)
|
||||||
|
|
||||||
// List of admin peers.
|
// List of admin peers.
|
||||||
globalAdminPeers = adminPeers{}
|
globalAdminPeers = adminPeers{}
|
||||||
|
|
||||||
@@ -127,8 +146,9 @@ var (
|
|||||||
|
|
||||||
// global colors.
|
// global colors.
|
||||||
var (
|
var (
|
||||||
colorBold = color.New(color.Bold).SprintFunc()
|
colorBold = color.New(color.Bold).SprintFunc()
|
||||||
colorBlue = color.New(color.FgBlue).SprintfFunc()
|
colorBlue = color.New(color.FgBlue).SprintfFunc()
|
||||||
|
colorYellow = color.New(color.FgYellow).SprintfFunc()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns minio global information, as a key value map.
|
// Returns minio global information, as a key value map.
|
||||||
|
|||||||
+11
-15
@@ -98,9 +98,9 @@ func path2BucketAndObject(path string) (bucket, object string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extractMetadataFromHeader extracts metadata from HTTP header.
|
// extractMetadataFromHeader extracts metadata from HTTP header.
|
||||||
func extractMetadataFromHeader(header http.Header) map[string]string {
|
func extractMetadataFromHeader(header http.Header) (map[string]string, error) {
|
||||||
if header == nil {
|
if header == nil {
|
||||||
return nil
|
return nil, traceError(errInvalidArgument)
|
||||||
}
|
}
|
||||||
metadata := make(map[string]string)
|
metadata := make(map[string]string)
|
||||||
// Save standard supported headers.
|
// Save standard supported headers.
|
||||||
@@ -116,16 +116,17 @@ func extractMetadataFromHeader(header http.Header) map[string]string {
|
|||||||
}
|
}
|
||||||
// Go through all other headers for any additional headers that needs to be saved.
|
// Go through all other headers for any additional headers that needs to be saved.
|
||||||
for key := range header {
|
for key := range header {
|
||||||
cKey := http.CanonicalHeaderKey(key)
|
if key != http.CanonicalHeaderKey(key) {
|
||||||
if strings.HasPrefix(cKey, "X-Amz-Meta-") {
|
return nil, traceError(errInvalidArgument)
|
||||||
metadata[cKey] = header.Get(key)
|
}
|
||||||
} else if strings.HasPrefix(key, "X-Minio-Meta-") {
|
if strings.HasPrefix(key, "X-Amz-Meta-") {
|
||||||
metadata[cKey] = header.Get(key)
|
metadata[key] = header.Get(key)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(key, "X-Minio-Meta-") {
|
||||||
|
metadata[key] = header.Get(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return metadata, nil
|
||||||
// Success.
|
|
||||||
return metadata
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Query string for the redirect URL the client is
|
// The Query string for the redirect URL the client is
|
||||||
@@ -168,11 +169,6 @@ func trimAwsChunkedContentEncoding(contentEnc string) (trimmedContentEnc string)
|
|||||||
return strings.Join(newEncs, ",")
|
return strings.Join(newEncs, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractMetadataFromForm extracts metadata from Post Form.
|
|
||||||
func extractMetadataFromForm(formValues http.Header) map[string]string {
|
|
||||||
return extractMetadataFromHeader(formValues)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate form field size for s3 specification requirement.
|
// Validate form field size for s3 specification requirement.
|
||||||
func validateFormFieldSize(formValues http.Header) error {
|
func validateFormFieldSize(formValues http.Header) error {
|
||||||
// Iterate over form values
|
// Iterate over form values
|
||||||
|
|||||||
+32
-10
@@ -123,8 +123,9 @@ func TestValidateFormFieldSize(t *testing.T) {
|
|||||||
// Tests validate metadata extraction from http headers.
|
// Tests validate metadata extraction from http headers.
|
||||||
func TestExtractMetadataHeaders(t *testing.T) {
|
func TestExtractMetadataHeaders(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
header http.Header
|
header http.Header
|
||||||
metadata map[string]string
|
metadata map[string]string
|
||||||
|
shouldFail bool
|
||||||
}{
|
}{
|
||||||
// Validate if there a known 'content-type'.
|
// Validate if there a known 'content-type'.
|
||||||
{
|
{
|
||||||
@@ -134,15 +135,17 @@ func TestExtractMetadataHeaders(t *testing.T) {
|
|||||||
metadata: map[string]string{
|
metadata: map[string]string{
|
||||||
"content-type": "image/png",
|
"content-type": "image/png",
|
||||||
},
|
},
|
||||||
|
shouldFail: false,
|
||||||
},
|
},
|
||||||
// Validate if there are no keys to extract.
|
// Validate if there are no keys to extract.
|
||||||
{
|
{
|
||||||
header: http.Header{
|
header: http.Header{
|
||||||
"test-1": []string{"123"},
|
"Test-1": []string{"123"},
|
||||||
},
|
},
|
||||||
metadata: map[string]string{},
|
metadata: map[string]string{},
|
||||||
|
shouldFail: false,
|
||||||
},
|
},
|
||||||
// Validate if there are no keys to extract.
|
// Validate that there are all headers extracted
|
||||||
{
|
{
|
||||||
header: http.Header{
|
header: http.Header{
|
||||||
"X-Amz-Meta-Appid": []string{"amz-meta"},
|
"X-Amz-Meta-Appid": []string{"amz-meta"},
|
||||||
@@ -150,19 +153,38 @@ func TestExtractMetadataHeaders(t *testing.T) {
|
|||||||
},
|
},
|
||||||
metadata: map[string]string{
|
metadata: map[string]string{
|
||||||
"X-Amz-Meta-Appid": "amz-meta",
|
"X-Amz-Meta-Appid": "amz-meta",
|
||||||
"X-Minio-Meta-Appid": "minio-meta"},
|
"X-Minio-Meta-Appid": "minio-meta",
|
||||||
|
},
|
||||||
|
shouldFail: false,
|
||||||
|
},
|
||||||
|
// Fail if header key is not in canonicalized form
|
||||||
|
{
|
||||||
|
header: http.Header{
|
||||||
|
"x-amz-meta-appid": []string{"amz-meta"},
|
||||||
|
},
|
||||||
|
metadata: map[string]string{
|
||||||
|
"X-Amz-Meta-Appid": "amz-meta",
|
||||||
|
},
|
||||||
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
// Empty header input returns empty metadata.
|
// Empty header input returns empty metadata.
|
||||||
{
|
{
|
||||||
header: nil,
|
header: nil,
|
||||||
metadata: nil,
|
metadata: nil,
|
||||||
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate if the extracting headers.
|
// Validate if the extracting headers.
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
metadata := extractMetadataFromHeader(testCase.header)
|
metadata, err := extractMetadataFromHeader(testCase.header)
|
||||||
if !reflect.DeepEqual(metadata, testCase.metadata) {
|
if err != nil && !testCase.shouldFail {
|
||||||
|
t.Fatalf("Test %d failed to extract metadata: %v", i+1, err)
|
||||||
|
}
|
||||||
|
if err == nil && testCase.shouldFail {
|
||||||
|
t.Fatalf("Test %d should fail, but it passed", i+1)
|
||||||
|
}
|
||||||
|
if err == nil && !reflect.DeepEqual(metadata, testCase.metadata) {
|
||||||
t.Fatalf("Test %d failed: Expected \"%#v\", got \"%#v\"", i+1, testCase.metadata, metadata)
|
t.Fatalf("Test %d failed: Expected \"%#v\", got \"%#v\"", i+1, testCase.metadata, metadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/rpc"
|
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -64,7 +63,7 @@ type lockServer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start lock maintenance from all lock servers.
|
// Start lock maintenance from all lock servers.
|
||||||
func startLockMaintainence(lockServers []*lockServer) {
|
func startLockMaintenance(lockServers []*lockServer) {
|
||||||
for _, locker := range lockServers {
|
for _, locker := range lockServers {
|
||||||
// Start loop for stale lock maintenance
|
// Start loop for stale lock maintenance
|
||||||
go func(lk *lockServer) {
|
go func(lk *lockServer) {
|
||||||
@@ -90,7 +89,7 @@ func startLockMaintainence(lockServers []*lockServer) {
|
|||||||
// Register distributed NS lock handlers.
|
// Register distributed NS lock handlers.
|
||||||
func registerDistNSLockRouter(mux *router.Router, endpoints EndpointList) error {
|
func registerDistNSLockRouter(mux *router.Router, endpoints EndpointList) error {
|
||||||
// Start lock maintenance from all lock servers.
|
// Start lock maintenance from all lock servers.
|
||||||
startLockMaintainence(globalLockServers)
|
startLockMaintenance(globalLockServers)
|
||||||
|
|
||||||
// Register initialized lock servers to their respective rpc endpoints.
|
// Register initialized lock servers to their respective rpc endpoints.
|
||||||
return registerStorageLockers(mux, globalLockServers)
|
return registerStorageLockers(mux, globalLockServers)
|
||||||
@@ -99,7 +98,7 @@ func registerDistNSLockRouter(mux *router.Router, endpoints EndpointList) error
|
|||||||
// registerStorageLockers - register locker rpc handlers for net/rpc library clients
|
// registerStorageLockers - register locker rpc handlers for net/rpc library clients
|
||||||
func registerStorageLockers(mux *router.Router, lockServers []*lockServer) error {
|
func registerStorageLockers(mux *router.Router, lockServers []*lockServer) error {
|
||||||
for _, lockServer := range lockServers {
|
for _, lockServer := range lockServers {
|
||||||
lockRPCServer := rpc.NewServer()
|
lockRPCServer := newRPCServer()
|
||||||
if err := lockRPCServer.RegisterName(lockServiceName, lockServer); err != nil {
|
if err := lockRPCServer.RegisterName(lockServiceName, lockServer); err != nil {
|
||||||
return traceError(err)
|
return traceError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type notifier struct {
|
|||||||
Kafka kafkaConfigs `json:"kafka"`
|
Kafka kafkaConfigs `json:"kafka"`
|
||||||
Webhook webhookConfigs `json:"webhook"`
|
Webhook webhookConfigs `json:"webhook"`
|
||||||
MySQL mySQLConfigs `json:"mysql"`
|
MySQL mySQLConfigs `json:"mysql"`
|
||||||
|
MQTT mqttConfigs `json:"mqtt"`
|
||||||
// Add new notification queues.
|
// Add new notification queues.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +55,25 @@ func (a amqpConfigs) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mqttConfigs map[string]mqttNotify
|
||||||
|
|
||||||
|
func (a mqttConfigs) Clone() mqttConfigs {
|
||||||
|
a2 := make(mqttConfigs, len(a))
|
||||||
|
for k, v := range a {
|
||||||
|
a2[k] = v
|
||||||
|
}
|
||||||
|
return a2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a mqttConfigs) Validate() error {
|
||||||
|
for k, v := range a {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("MQTT [%s] configuration invalid: %s", k, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type natsConfigs map[string]natsNotify
|
type natsConfigs map[string]natsNotify
|
||||||
|
|
||||||
func (a natsConfigs) Clone() natsConfigs {
|
func (a natsConfigs) Clone() natsConfigs {
|
||||||
@@ -215,6 +235,9 @@ func (n *notifier) Validate() error {
|
|||||||
if err := n.MySQL.Validate(); err != nil {
|
if err := n.MySQL.Validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := n.MQTT.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +259,24 @@ func (n *notifier) GetAMQPByID(accountID string) amqpNotify {
|
|||||||
return n.AMQP[accountID]
|
return n.AMQP[accountID]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *notifier) SetMQTTByID(accountID string, mqttn mqttNotify) {
|
||||||
|
n.Lock()
|
||||||
|
defer n.Unlock()
|
||||||
|
n.MQTT[accountID] = mqttn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *notifier) GetMQTT() map[string]mqttNotify {
|
||||||
|
n.RLock()
|
||||||
|
defer n.RUnlock()
|
||||||
|
return n.MQTT.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *notifier) GetMQTTByID(accountID string) mqttNotify {
|
||||||
|
n.RLock()
|
||||||
|
defer n.RUnlock()
|
||||||
|
return n.MQTT[accountID]
|
||||||
|
}
|
||||||
|
|
||||||
func (n *notifier) SetNATSByID(accountID string, natsn natsNotify) {
|
func (n *notifier) SetNATSByID(accountID string, natsn natsNotify) {
|
||||||
n.Lock()
|
n.Lock()
|
||||||
defer n.Unlock()
|
defer n.Unlock()
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const (
|
|||||||
|
|
||||||
// Static string indicating queue type 'amqp'.
|
// Static string indicating queue type 'amqp'.
|
||||||
queueTypeAMQP = "amqp"
|
queueTypeAMQP = "amqp"
|
||||||
|
// Static string indicating queue type 'mqtt'.
|
||||||
|
queueTypeMQTT = "mqtt"
|
||||||
// Static string indicating queue type 'nats'.
|
// Static string indicating queue type 'nats'.
|
||||||
queueTypeNATS = "nats"
|
queueTypeNATS = "nats"
|
||||||
// Static string indicating queue type 'elasticsearch'.
|
// Static string indicating queue type 'elasticsearch'.
|
||||||
@@ -80,6 +82,25 @@ func isAMQPQueue(sqsArn arnSQS) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true if mqttARN is for an MQTT queue.
|
||||||
|
func isMQTTQueue(sqsArn arnSQS) bool {
|
||||||
|
if sqsArn.Type != queueTypeMQTT {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mqttL := serverConfig.Notify.GetMQTTByID(sqsArn.AccountID)
|
||||||
|
if !mqttL.Enable {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Connect to mqtt server to validate.
|
||||||
|
mqttC, err := dialMQTT(mqttL)
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "Unable to connect to mqtt service. %#v", mqttL)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer mqttC.Client.Disconnect(250)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Returns true if natsArn is for an NATS queue.
|
// Returns true if natsArn is for an NATS queue.
|
||||||
func isNATSQueue(sqsArn arnSQS) bool {
|
func isNATSQueue(sqsArn arnSQS) bool {
|
||||||
if sqsArn.Type != queueTypeNATS {
|
if sqsArn.Type != queueTypeNATS {
|
||||||
|
|||||||
+3
-3
@@ -59,13 +59,13 @@ type amqpConn struct {
|
|||||||
// dialAMQP - dials and returns an amqpConnection instance,
|
// dialAMQP - dials and returns an amqpConnection instance,
|
||||||
// for sending notifications. Returns error if amqp logger
|
// for sending notifications. Returns error if amqp logger
|
||||||
// is not enabled.
|
// is not enabled.
|
||||||
func dialAMQP(amqpL amqpNotify) (amqpConn, error) {
|
func dialAMQP(amqpL amqpNotify) (ac amqpConn, e error) {
|
||||||
if !amqpL.Enable {
|
if !amqpL.Enable {
|
||||||
return amqpConn{}, errNotifyNotEnabled
|
return ac, errNotifyNotEnabled
|
||||||
}
|
}
|
||||||
conn, err := amqp.Dial(amqpL.URL)
|
conn, err := amqp.Dial(amqpL.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return amqpConn{}, err
|
return ac, err
|
||||||
}
|
}
|
||||||
return amqpConn{Connection: conn, params: amqpL}, nil
|
return amqpConn{Connection: conn, params: amqpL}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -66,13 +66,13 @@ type kafkaConn struct {
|
|||||||
topic string
|
topic string
|
||||||
}
|
}
|
||||||
|
|
||||||
func dialKafka(kn kafkaNotify) (kafkaConn, error) {
|
func dialKafka(kn kafkaNotify) (kc kafkaConn, e error) {
|
||||||
if !kn.Enable {
|
if !kn.Enable {
|
||||||
return kafkaConn{}, errNotifyNotEnabled
|
return kc, errNotifyNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
if kn.Topic == "" {
|
if kn.Topic == "" {
|
||||||
return kafkaConn{}, kkErrFunc(
|
return kc, kkErrFunc(
|
||||||
"Topic was not specified in configuration")
|
"Topic was not specified in configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ func dialKafka(kn kafkaNotify) (kafkaConn, error) {
|
|||||||
|
|
||||||
p, err := sarama.NewSyncProducer(kn.Brokers, config)
|
p, err := sarama.NewSyncProducer(kn.Brokers, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return kafkaConn{}, kkErrFunc("Failed to start producer: %v", err)
|
return kc, kkErrFunc("Failed to start producer: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return kafkaConn{p, kn.Topic}, nil
|
return kafkaConn{p, kn.Topic}, nil
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* 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 (
|
||||||
|
"crypto/tls"
|
||||||
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
MQTT "github.com/eclipse/paho.mqtt.golang"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mqttNotify struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Broker string `json:"broker"`
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
QoS int `json:"qos"`
|
||||||
|
ClientID string `json:"clientId"`
|
||||||
|
User string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mqttNotify) Validate() error {
|
||||||
|
if !m.Enable {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := checkURL(m.Broker); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mqttConn struct {
|
||||||
|
params mqttNotify
|
||||||
|
Client MQTT.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialMQTT(mqttL mqttNotify) (mc mqttConn, e error) {
|
||||||
|
if !mqttL.Enable {
|
||||||
|
return mc, errNotifyNotEnabled
|
||||||
|
}
|
||||||
|
connOpts := &MQTT.ClientOptions{
|
||||||
|
ClientID: mqttL.ClientID,
|
||||||
|
CleanSession: true,
|
||||||
|
Username: mqttL.User,
|
||||||
|
Password: mqttL.Password,
|
||||||
|
MaxReconnectInterval: 1 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
TLSConfig: tls.Config{RootCAs: globalRootCAs},
|
||||||
|
}
|
||||||
|
connOpts.AddBroker(mqttL.Broker)
|
||||||
|
client := MQTT.NewClient(connOpts)
|
||||||
|
if token := client.Connect(); token.Wait() && token.Error() != nil {
|
||||||
|
return mc, token.Error()
|
||||||
|
}
|
||||||
|
return mqttConn{Client: client, params: mqttL}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMQTTNotify(accountID string) (*logrus.Logger, error) {
|
||||||
|
mqttL := serverConfig.Notify.GetMQTTByID(accountID)
|
||||||
|
|
||||||
|
//connect to MQTT Server
|
||||||
|
mqttC, err := dialMQTT(mqttL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mqttLog := logrus.New()
|
||||||
|
|
||||||
|
// Disable writing to console.
|
||||||
|
mqttLog.Out = ioutil.Discard
|
||||||
|
|
||||||
|
// Add a mqtt hook.
|
||||||
|
mqttLog.Hooks.Add(mqttC)
|
||||||
|
|
||||||
|
// Set default JSON formatter
|
||||||
|
mqttLog.Formatter = new(logrus.JSONFormatter)
|
||||||
|
|
||||||
|
// successfully enabled all MQTTs
|
||||||
|
return mqttLog, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire if called when an event should be sent to the message broker.
|
||||||
|
func (q mqttConn) Fire(entry *logrus.Entry) error {
|
||||||
|
body, err := entry.String()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !q.Client.IsConnected() {
|
||||||
|
if token := q.Client.Connect(); token.Wait() && token.Error() != nil {
|
||||||
|
return token.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
token := q.Client.Publish(q.params.Topic, byte(q.params.QoS), false, body)
|
||||||
|
if token.Wait() && token.Error() != nil {
|
||||||
|
return token.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Levels is available logging levels.
|
||||||
|
func (q mqttConn) Levels() []logrus.Level {
|
||||||
|
return []logrus.Level{
|
||||||
|
logrus.InfoLevel,
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-10
@@ -145,9 +145,9 @@ type mySQLConn struct {
|
|||||||
*sql.DB
|
*sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func dialMySQL(msql mySQLNotify) (mySQLConn, error) {
|
func dialMySQL(msql mySQLNotify) (mc mySQLConn, e error) {
|
||||||
if !msql.Enable {
|
if !msql.Enable {
|
||||||
return mySQLConn{}, errNotifyNotEnabled
|
return mc, errNotifyNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
dsnStr := msql.DsnString
|
dsnStr := msql.DsnString
|
||||||
@@ -166,7 +166,7 @@ func dialMySQL(msql mySQLNotify) (mySQLConn, error) {
|
|||||||
|
|
||||||
db, err := sql.Open("mysql", dsnStr)
|
db, err := sql.Open("mysql", dsnStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mySQLConn{}, mysqlErrFunc(
|
return mc, mysqlErrFunc(
|
||||||
"Connection opening failure (dsnStr=%s): %v",
|
"Connection opening failure (dsnStr=%s): %v",
|
||||||
dsnStr, err)
|
dsnStr, err)
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ func dialMySQL(msql mySQLNotify) (mySQLConn, error) {
|
|||||||
// ping to check that server is actually reachable.
|
// ping to check that server is actually reachable.
|
||||||
err = db.Ping()
|
err = db.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mySQLConn{}, mysqlErrFunc(
|
return mc, mysqlErrFunc(
|
||||||
"Ping to server failed with: %v", err)
|
"Ping to server failed with: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ func dialMySQL(msql mySQLNotify) (mySQLConn, error) {
|
|||||||
_, errCreate := db.Exec(fmt.Sprintf(createStmt, msql.Table))
|
_, errCreate := db.Exec(fmt.Sprintf(createStmt, msql.Table))
|
||||||
if errCreate != nil {
|
if errCreate != nil {
|
||||||
// failed to create the table. error out.
|
// failed to create the table. error out.
|
||||||
return mySQLConn{}, mysqlErrFunc(
|
return mc, mysqlErrFunc(
|
||||||
"'Select' failed with %v, then 'Create Table' failed with %v",
|
"'Select' failed with %v, then 'Create Table' failed with %v",
|
||||||
err, errCreate,
|
err, errCreate,
|
||||||
)
|
)
|
||||||
@@ -205,22 +205,20 @@ func dialMySQL(msql mySQLNotify) (mySQLConn, error) {
|
|||||||
stmts["upsertRow"], err = db.Prepare(fmt.Sprintf(upsertRowForNSMySQL,
|
stmts["upsertRow"], err = db.Prepare(fmt.Sprintf(upsertRowForNSMySQL,
|
||||||
msql.Table))
|
msql.Table))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mySQLConn{},
|
return mc, mysqlErrFunc("create UPSERT prepared statement failed with: %v", err)
|
||||||
mysqlErrFunc("create UPSERT prepared statement failed with: %v", err)
|
|
||||||
}
|
}
|
||||||
// delete statement
|
// delete statement
|
||||||
stmts["deleteRow"], err = db.Prepare(fmt.Sprintf(deleteRowForNSMySQL,
|
stmts["deleteRow"], err = db.Prepare(fmt.Sprintf(deleteRowForNSMySQL,
|
||||||
msql.Table))
|
msql.Table))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mySQLConn{},
|
return mc, mysqlErrFunc("create DELETE prepared statement failed with: %v", err)
|
||||||
mysqlErrFunc("create DELETE prepared statement failed with: %v", err)
|
|
||||||
}
|
}
|
||||||
case formatAccess:
|
case formatAccess:
|
||||||
// insert statement
|
// insert statement
|
||||||
stmts["insertRow"], err = db.Prepare(fmt.Sprintf(insertRowForAccessMySQL,
|
stmts["insertRow"], err = db.Prepare(fmt.Sprintf(insertRowForAccessMySQL,
|
||||||
msql.Table))
|
msql.Table))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mySQLConn{}, mysqlErrFunc(
|
return mc, mysqlErrFunc(
|
||||||
"create INSERT prepared statement failed with: %v", err)
|
"create INSERT prepared statement failed with: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -69,9 +69,9 @@ type natsIOConn struct {
|
|||||||
// dialNATS - dials and returns an natsIOConn instance,
|
// dialNATS - dials and returns an natsIOConn instance,
|
||||||
// for sending notifications. Returns error if nats logger
|
// for sending notifications. Returns error if nats logger
|
||||||
// is not enabled.
|
// is not enabled.
|
||||||
func dialNATS(natsL natsNotify, testDial bool) (natsIOConn, error) {
|
func dialNATS(natsL natsNotify, testDial bool) (nioc natsIOConn, e error) {
|
||||||
if !natsL.Enable {
|
if !natsL.Enable {
|
||||||
return natsIOConn{}, errNotifyNotEnabled
|
return nioc, errNotifyNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct natsIOConn which holds all NATS connection information
|
// Construct natsIOConn which holds all NATS connection information
|
||||||
@@ -105,7 +105,7 @@ func dialNATS(natsL natsNotify, testDial bool) (natsIOConn, error) {
|
|||||||
// Do the real connection to the NATS server
|
// Do the real connection to the NATS server
|
||||||
sc, err := stan.Connect(natsL.Streaming.ClusterID, clientID, connOpts...)
|
sc, err := stan.Connect(natsL.Streaming.ClusterID, clientID, connOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return natsIOConn{}, err
|
return nioc, err
|
||||||
}
|
}
|
||||||
// Save the created connection
|
// Save the created connection
|
||||||
conn.stanConn = sc
|
conn.stanConn = sc
|
||||||
@@ -120,7 +120,7 @@ func dialNATS(natsL natsNotify, testDial bool) (natsIOConn, error) {
|
|||||||
// Do the real connection
|
// Do the real connection
|
||||||
nc, err := natsC.Connect()
|
nc, err := natsC.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return natsIOConn{}, err
|
return nioc, err
|
||||||
}
|
}
|
||||||
// Save the created connection
|
// Save the created connection
|
||||||
conn.natsConn = nc
|
conn.natsConn = nc
|
||||||
|
|||||||
@@ -153,9 +153,9 @@ type pgConn struct {
|
|||||||
*sql.DB
|
*sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func dialPostgreSQL(pgN postgreSQLNotify) (pgConn, error) {
|
func dialPostgreSQL(pgN postgreSQLNotify) (pc pgConn, e error) {
|
||||||
if !pgN.Enable {
|
if !pgN.Enable {
|
||||||
return pgConn{}, errNotifyNotEnabled
|
return pc, errNotifyNotEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// collect connection params
|
// collect connection params
|
||||||
@@ -179,7 +179,7 @@ func dialPostgreSQL(pgN postgreSQLNotify) (pgConn, error) {
|
|||||||
|
|
||||||
db, err := sql.Open("postgres", connStr)
|
db, err := sql.Open("postgres", connStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pgConn{}, pgErrFunc(
|
return pc, pgErrFunc(
|
||||||
"Connection opening failure (connectionString=%s): %v",
|
"Connection opening failure (connectionString=%s): %v",
|
||||||
connStr, err)
|
connStr, err)
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@ func dialPostgreSQL(pgN postgreSQLNotify) (pgConn, error) {
|
|||||||
// ping to check that server is actually reachable.
|
// ping to check that server is actually reachable.
|
||||||
err = db.Ping()
|
err = db.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pgConn{}, pgErrFunc("Ping to server failed with: %v",
|
return pc, pgErrFunc("Ping to server failed with: %v",
|
||||||
err)
|
err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ func dialPostgreSQL(pgN postgreSQLNotify) (pgConn, error) {
|
|||||||
_, errCreate := db.Exec(fmt.Sprintf(createStmt, pgN.Table))
|
_, errCreate := db.Exec(fmt.Sprintf(createStmt, pgN.Table))
|
||||||
if errCreate != nil {
|
if errCreate != nil {
|
||||||
// failed to create the table. error out.
|
// failed to create the table. error out.
|
||||||
return pgConn{}, pgErrFunc(
|
return pc, pgErrFunc(
|
||||||
"'Select' failed with %v, then 'Create Table' failed with %v",
|
"'Select' failed with %v, then 'Create Table' failed with %v",
|
||||||
err, errCreate,
|
err, errCreate,
|
||||||
)
|
)
|
||||||
@@ -218,14 +218,14 @@ func dialPostgreSQL(pgN postgreSQLNotify) (pgConn, error) {
|
|||||||
stmts["upsertRow"], err = db.Prepare(fmt.Sprintf(upsertRowForNS,
|
stmts["upsertRow"], err = db.Prepare(fmt.Sprintf(upsertRowForNS,
|
||||||
pgN.Table))
|
pgN.Table))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pgConn{}, pgErrFunc(
|
return pc, pgErrFunc(
|
||||||
"create UPSERT prepared statement failed with: %v", err)
|
"create UPSERT prepared statement failed with: %v", err)
|
||||||
}
|
}
|
||||||
// delete statement
|
// delete statement
|
||||||
stmts["deleteRow"], err = db.Prepare(fmt.Sprintf(deleteRowForNS,
|
stmts["deleteRow"], err = db.Prepare(fmt.Sprintf(deleteRowForNS,
|
||||||
pgN.Table))
|
pgN.Table))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pgConn{}, pgErrFunc(
|
return pc, pgErrFunc(
|
||||||
"create DELETE prepared statement failed with: %v", err)
|
"create DELETE prepared statement failed with: %v", err)
|
||||||
}
|
}
|
||||||
case formatAccess:
|
case formatAccess:
|
||||||
@@ -233,7 +233,7 @@ func dialPostgreSQL(pgN postgreSQLNotify) (pgConn, error) {
|
|||||||
stmts["insertRow"], err = db.Prepare(fmt.Sprintf(insertRowForAccess,
|
stmts["insertRow"], err = db.Prepare(fmt.Sprintf(insertRowForAccess,
|
||||||
pgN.Table))
|
pgN.Table))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pgConn{}, pgErrFunc(
|
return pc, pgErrFunc(
|
||||||
"create INSERT prepared statement failed with: %v", err)
|
"create INSERT prepared statement failed with: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,13 @@ func (e SHA256Mismatch) Error() string {
|
|||||||
return "sha256 computed does not match with what is expected"
|
return "sha256 computed does not match with what is expected"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignatureDoesNotMatch - when content md5 does not match with what was sent from client.
|
||||||
|
type SignatureDoesNotMatch struct{}
|
||||||
|
|
||||||
|
func (e SignatureDoesNotMatch) Error() string {
|
||||||
|
return "The request signature we calculated does not match the signature you provided. Check your key and signing method."
|
||||||
|
}
|
||||||
|
|
||||||
// StorageFull storage ran out of space.
|
// StorageFull storage ran out of space.
|
||||||
type StorageFull struct{}
|
type StorageFull struct{}
|
||||||
|
|
||||||
@@ -144,6 +151,13 @@ func (e BucketNotFound) Error() string {
|
|||||||
return "Bucket not found: " + e.Bucket
|
return "Bucket not found: " + e.Bucket
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BucketAlreadyExists the requested bucket name is not available.
|
||||||
|
type BucketAlreadyExists GenericError
|
||||||
|
|
||||||
|
func (e BucketAlreadyExists) Error() string {
|
||||||
|
return "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."
|
||||||
|
}
|
||||||
|
|
||||||
// BucketAlreadyOwnedByYou already owned by you.
|
// BucketAlreadyOwnedByYou already owned by you.
|
||||||
type BucketAlreadyOwnedByYou GenericError
|
type BucketAlreadyOwnedByYou GenericError
|
||||||
|
|
||||||
@@ -250,6 +264,14 @@ func (e ObjectNameInvalid) Error() string {
|
|||||||
return "Object name invalid: " + e.Bucket + "#" + e.Object
|
return "Object name invalid: " + e.Bucket + "#" + e.Object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllAccessDisabled All access to this object has been disabled
|
||||||
|
type AllAccessDisabled GenericError
|
||||||
|
|
||||||
|
// Return string an error formatted as the given text.
|
||||||
|
func (e AllAccessDisabled) Error() string {
|
||||||
|
return "All access to this object has been disabled"
|
||||||
|
}
|
||||||
|
|
||||||
// IncompleteBody You did not provide the number of bytes specified by the Content-Length HTTP header.
|
// IncompleteBody You did not provide the number of bytes specified by the Content-Length HTTP header.
|
||||||
type IncompleteBody GenericError
|
type IncompleteBody GenericError
|
||||||
|
|
||||||
@@ -307,7 +329,7 @@ func (e InvalidUploadID) Error() string {
|
|||||||
type InvalidPart struct{}
|
type InvalidPart struct{}
|
||||||
|
|
||||||
func (e InvalidPart) Error() string {
|
func (e InvalidPart) Error() string {
|
||||||
return "One or more of the specified parts could not be found"
|
return "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."
|
||||||
}
|
}
|
||||||
|
|
||||||
// PartTooSmall - error if part size is less than 5MB.
|
// PartTooSmall - error if part size is less than 5MB.
|
||||||
@@ -321,6 +343,13 @@ func (e PartTooSmall) Error() string {
|
|||||||
return fmt.Sprintf("Part size for %d should be atleast 5MB", e.PartNumber)
|
return fmt.Sprintf("Part size for %d should be atleast 5MB", e.PartNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PartTooBig returned if size of part is bigger than the allowed limit.
|
||||||
|
type PartTooBig struct{}
|
||||||
|
|
||||||
|
func (e PartTooBig) Error() string {
|
||||||
|
return "Part size bigger than the allowed limit"
|
||||||
|
}
|
||||||
|
|
||||||
// NotImplemented If a feature is not implemented
|
// NotImplemented If a feature is not implemented
|
||||||
type NotImplemented struct{}
|
type NotImplemented struct{}
|
||||||
|
|
||||||
|
|||||||
@@ -1061,7 +1061,7 @@ func testListMultipartUploads(obj ObjectLayer, instanceType string, t TestErrHan
|
|||||||
{
|
{
|
||||||
MaxUploads: 10,
|
MaxUploads: 10,
|
||||||
IsTruncated: false,
|
IsTruncated: false,
|
||||||
Prefix: globalMinioDefaultOwnerID,
|
Prefix: "minio",
|
||||||
UploadIDMarker: uploadIDs[4],
|
UploadIDMarker: uploadIDs[4],
|
||||||
Uploads: []uploadMetadata{
|
Uploads: []uploadMetadata{
|
||||||
{
|
{
|
||||||
@@ -1895,7 +1895,7 @@ func testObjectCompleteMultipartUpload(obj ObjectLayer, instanceType string, t T
|
|||||||
// Part number 0 doesn't exist, expecting InvalidPart error (Test number 12).
|
// Part number 0 doesn't exist, expecting InvalidPart error (Test number 12).
|
||||||
{bucketNames[0], objectNames[0], uploadIDs[0], []completePart{{ETag: "abcd", PartNumber: 0}}, "", InvalidPart{}, false},
|
{bucketNames[0], objectNames[0], uploadIDs[0], []completePart{{ETag: "abcd", PartNumber: 0}}, "", InvalidPart{}, false},
|
||||||
// // Upload and PartNumber exists, But a deliberate ETag mismatch is introduced (Test number 13).
|
// // Upload and PartNumber exists, But a deliberate ETag mismatch is introduced (Test number 13).
|
||||||
{bucketNames[0], objectNames[0], uploadIDs[0], inputParts[0].parts, "", BadDigest{}, false},
|
{bucketNames[0], objectNames[0], uploadIDs[0], inputParts[0].parts, "", InvalidPart{}, false},
|
||||||
// Test case with non existent object name (Test number 14).
|
// Test case with non existent object name (Test number 14).
|
||||||
{bucketNames[0], "my-object", uploadIDs[0], []completePart{{ETag: "abcd", PartNumber: 1}}, "", InvalidUploadID{UploadID: uploadIDs[0]}, false},
|
{bucketNames[0], "my-object", uploadIDs[0], []completePart{{ETag: "abcd", PartNumber: 1}}, "", InvalidUploadID{UploadID: uploadIDs[0]}, false},
|
||||||
// Testing for Part being too small (Test number 15).
|
// Testing for Part being too small (Test number 15).
|
||||||
|
|||||||
+20
-6
@@ -260,7 +260,7 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
|
|
||||||
// Extract metadata relevant for an CopyObject operation based on conditional
|
// Extract metadata relevant for an CopyObject operation based on conditional
|
||||||
// header values specified in X-Amz-Metadata-Directive.
|
// header values specified in X-Amz-Metadata-Directive.
|
||||||
func getCpObjMetadataFromHeader(header http.Header, defaultMeta map[string]string) map[string]string {
|
func getCpObjMetadataFromHeader(header http.Header, defaultMeta map[string]string) (map[string]string, error) {
|
||||||
// if x-amz-metadata-directive says REPLACE then
|
// if x-amz-metadata-directive says REPLACE then
|
||||||
// we extract metadata from the input headers.
|
// we extract metadata from the input headers.
|
||||||
if isMetadataReplace(header) {
|
if isMetadataReplace(header) {
|
||||||
@@ -270,11 +270,11 @@ func getCpObjMetadataFromHeader(header http.Header, defaultMeta map[string]strin
|
|||||||
// if x-amz-metadata-directive says COPY then we
|
// if x-amz-metadata-directive says COPY then we
|
||||||
// return the default metadata.
|
// return the default metadata.
|
||||||
if isMetadataCopy(header) {
|
if isMetadataCopy(header) {
|
||||||
return defaultMeta
|
return defaultMeta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy is default behavior if not x-amz-metadata-directive is set.
|
// Copy is default behavior if not x-amz-metadata-directive is set.
|
||||||
return defaultMeta
|
return defaultMeta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyObjectHandler - Copy Object
|
// CopyObjectHandler - Copy Object
|
||||||
@@ -363,7 +363,11 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
// Make sure to remove saved etag, CopyObject calculates a new one.
|
// Make sure to remove saved etag, CopyObject calculates a new one.
|
||||||
delete(defaultMeta, "etag")
|
delete(defaultMeta, "etag")
|
||||||
|
|
||||||
newMetadata := getCpObjMetadataFromHeader(r.Header, defaultMeta)
|
newMetadata, err := getCpObjMetadataFromHeader(r.Header, defaultMeta)
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "found invalid http request header")
|
||||||
|
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||||
|
}
|
||||||
// Check if x-amz-metadata-directive was not set to REPLACE and source,
|
// Check if x-amz-metadata-directive was not set to REPLACE and source,
|
||||||
// desination are same objects.
|
// desination are same objects.
|
||||||
if !isMetadataReplace(r.Header) && cpSrcDstSame {
|
if !isMetadataReplace(r.Header) && cpSrcDstSame {
|
||||||
@@ -457,7 +461,12 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract metadata to be saved from incoming HTTP header.
|
// Extract metadata to be saved from incoming HTTP header.
|
||||||
metadata := extractMetadataFromHeader(r.Header)
|
metadata, err := extractMetadataFromHeader(r.Header)
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "found invalid http request header")
|
||||||
|
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
if rAuthType == authTypeStreamingSigned {
|
if rAuthType == authTypeStreamingSigned {
|
||||||
if contentEncoding, ok := metadata["content-encoding"]; ok {
|
if contentEncoding, ok := metadata["content-encoding"]; ok {
|
||||||
contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
|
contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
|
||||||
@@ -579,7 +588,12 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract metadata that needs to be saved.
|
// Extract metadata that needs to be saved.
|
||||||
metadata := extractMetadataFromHeader(r.Header)
|
metadata, err := extractMetadataFromHeader(r.Header)
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "found invalid http request header")
|
||||||
|
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
uploadID, err := objectAPI.NewMultipartUpload(bucket, object, metadata)
|
uploadID, err := objectAPI.NewMultipartUpload(bucket, object, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2252,7 +2252,7 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
|
|||||||
accessKey: credentials.AccessKey,
|
accessKey: credentials.AccessKey,
|
||||||
secretKey: credentials.SecretKey,
|
secretKey: credentials.SecretKey,
|
||||||
|
|
||||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(toAPIErrorCode(BadDigest{})),
|
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(toAPIErrorCode(InvalidPart{})),
|
||||||
getGetObjectURL("", bucketName, objectName))),
|
getGetObjectURL("", bucketName, objectName))),
|
||||||
expectedRespStatus: http.StatusBadRequest,
|
expectedRespStatus: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
|||||||
+71
-24
@@ -35,9 +35,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
fsMinFreeSpace = 1 * humanize.GiByte // Min 1GiB free space.
|
diskMinFreeSpace = 1 * humanize.GiByte // Min 1GiB free space.
|
||||||
fsMinFreeInodes = 10000 // Min 10000.
|
diskMinTotalSpace = diskMinFreeSpace // Min 1GiB total space.
|
||||||
maxAllowedIOError = 5
|
diskMinFreeInodes = 10000 // Min 10000 free inodes.
|
||||||
|
diskMinTotalInodes = diskMinFreeInodes // Min 10000 total inodes.
|
||||||
|
maxAllowedIOError = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
// posix - implements StorageAPI interface.
|
// posix - implements StorageAPI interface.
|
||||||
@@ -108,7 +110,7 @@ func newPosix(path string) (StorageAPI, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fs := &posix{
|
st := &posix{
|
||||||
diskPath: diskPath,
|
diskPath: diskPath,
|
||||||
// 1MiB buffer pool for posix internal operations.
|
// 1MiB buffer pool for posix internal operations.
|
||||||
pool: sync.Pool{
|
pool: sync.Pool{
|
||||||
@@ -131,7 +133,19 @@ func newPosix(path string) (StorageAPI, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fs, nil
|
|
||||||
|
di, err := getDiskInfo(preparePath(diskPath))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if disk has minimum required total space.
|
||||||
|
if err = checkDiskMinTotal(di); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success.
|
||||||
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDiskInfo returns given disk information.
|
// getDiskInfo returns given disk information.
|
||||||
@@ -155,11 +169,58 @@ var ignoreDiskFreeOS = []string{
|
|||||||
globalSolarisOSName,
|
globalSolarisOSName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if disk total has minimum required size.
|
||||||
|
func checkDiskMinTotal(di disk.Info) (err error) {
|
||||||
|
// Remove 5% from total space for cumulative disk space
|
||||||
|
// used for journalling, inodes etc.
|
||||||
|
totalDiskSpace := float64(di.Total) * 0.95
|
||||||
|
if int64(totalDiskSpace) <= diskMinTotalSpace {
|
||||||
|
return errDiskFull
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some filesystems do not implement a way to provide total inodes available, instead
|
||||||
|
// inodes are allocated based on available disk space. For example CephDISK, StoreNext CVDISK,
|
||||||
|
// AzureFile driver. Allow for the available disk to be separately validated and we will
|
||||||
|
// validate inodes only if total inodes are provided by the underlying filesystem.
|
||||||
|
if di.Files != 0 && di.FSType != "NFS" {
|
||||||
|
totalFiles := int64(di.Files)
|
||||||
|
if totalFiles <= diskMinTotalInodes {
|
||||||
|
return errDiskFull
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if disk free has minimum required size.
|
||||||
|
func checkDiskMinFree(di disk.Info) error {
|
||||||
|
// Remove 5% from free space for cumulative disk space used for journalling, inodes etc.
|
||||||
|
availableDiskSpace := float64(di.Free) * 0.95
|
||||||
|
if int64(availableDiskSpace) <= diskMinFreeSpace {
|
||||||
|
return errDiskFull
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some filesystems do not implement a way to provide total inodes available, instead inodes
|
||||||
|
// are allocated based on available disk space. For example CephDISK, StoreNext CVDISK, AzureFile driver.
|
||||||
|
// Allow for the available disk to be separately validate and we will validate inodes only if
|
||||||
|
// total inodes are provided by the underlying filesystem.
|
||||||
|
if di.Files != 0 && di.FSType != "NFS" {
|
||||||
|
availableFiles := int64(di.Ffree)
|
||||||
|
if availableFiles <= diskMinFreeInodes {
|
||||||
|
return errDiskFull
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// checkDiskFree verifies if disk path has sufficient minimum free disk space and files.
|
// checkDiskFree verifies if disk path has sufficient minimum free disk space and files.
|
||||||
func checkDiskFree(diskPath string, neededSpace int64) (err error) {
|
func checkDiskFree(diskPath string, neededSpace int64) (err error) {
|
||||||
// We don't validate disk space or inode utilization on windows.
|
// We don't validate disk space or inode utilization on windows.
|
||||||
// Each windows calls to 'GetVolumeInformationW' takes around 3-5seconds.
|
// Each windows call to 'GetVolumeInformationW' takes around
|
||||||
// And StatFS is not supported by Go for solaris and netbsd.
|
// 3-5seconds. And StatDISK is not supported by Go for solaris
|
||||||
|
// and netbsd.
|
||||||
if contains(ignoreDiskFreeOS, runtime.GOOS) {
|
if contains(ignoreDiskFreeOS, runtime.GOOS) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -170,29 +231,15 @@ func checkDiskFree(diskPath string, neededSpace int64) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove 5% from free space for cumulative disk space used for journalling, inodes etc.
|
if err = checkDiskMinFree(di); err != nil {
|
||||||
availableDiskSpace := float64(di.Free) * 0.95
|
return err
|
||||||
if int64(availableDiskSpace) <= fsMinFreeSpace {
|
|
||||||
return errDiskFull
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some filesystems do not implement a way to provide total inodes available, instead inodes
|
|
||||||
// are allocated based on available disk space. For example CephFS, StoreNext CVFS, AzureFile driver.
|
|
||||||
// Allow for the available disk to be separately validate and we will validate inodes only if
|
|
||||||
// total inodes are provided by the underlying filesystem.
|
|
||||||
if di.Files != 0 && di.FSType != "NFS" {
|
|
||||||
availableFiles := int64(di.Ffree)
|
|
||||||
if availableFiles <= fsMinFreeInodes {
|
|
||||||
return errDiskFull
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have enough space to store data
|
// Check if we have enough space to store data
|
||||||
if neededSpace > int64(availableDiskSpace) {
|
if neededSpace > int64(float64(di.Free)*0.95) {
|
||||||
return errDiskFull
|
return errDiskFull
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success.
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/disk"
|
||||||
|
|
||||||
"golang.org/x/crypto/blake2b"
|
"golang.org/x/crypto/blake2b"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1669,3 +1671,106 @@ func TestPosixStatFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks for restrictions for min total disk space and inodes.
|
||||||
|
func TestCheckDiskTotalMin(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
diskInfo disk.Info
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
// Test 1 - when fstype is nfs.
|
||||||
|
{
|
||||||
|
diskInfo: disk.Info{
|
||||||
|
Total: diskMinTotalSpace * 3,
|
||||||
|
FSType: "NFS",
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
// Test 2 - when fstype is xfs and total inodes are small.
|
||||||
|
{
|
||||||
|
diskInfo: disk.Info{
|
||||||
|
Total: diskMinTotalSpace * 3,
|
||||||
|
FSType: "XFS",
|
||||||
|
Files: 9999,
|
||||||
|
},
|
||||||
|
err: errDiskFull,
|
||||||
|
},
|
||||||
|
// Test 3 - when fstype is btrfs and total inodes is empty.
|
||||||
|
{
|
||||||
|
diskInfo: disk.Info{
|
||||||
|
Total: diskMinTotalSpace * 3,
|
||||||
|
FSType: "BTRFS",
|
||||||
|
Files: 0,
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
// Test 4 - when fstype is xfs and total disk space is really small.
|
||||||
|
{
|
||||||
|
diskInfo: disk.Info{
|
||||||
|
Total: diskMinTotalSpace - diskMinTotalSpace/1024,
|
||||||
|
FSType: "XFS",
|
||||||
|
Files: 9999,
|
||||||
|
},
|
||||||
|
err: errDiskFull,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all cases.
|
||||||
|
for i, test := range testCases {
|
||||||
|
if err := checkDiskMinTotal(test.diskInfo); test.err != err {
|
||||||
|
t.Errorf("Test %d: Expected error %s, got %s", i+1, test.err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks for restrictions for min free disk space and inodes.
|
||||||
|
func TestCheckDiskFreeMin(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
diskInfo disk.Info
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
// Test 1 - when fstype is nfs.
|
||||||
|
{
|
||||||
|
diskInfo: disk.Info{
|
||||||
|
Free: diskMinTotalSpace * 3,
|
||||||
|
FSType: "NFS",
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
// Test 2 - when fstype is xfs and total inodes are small.
|
||||||
|
{
|
||||||
|
diskInfo: disk.Info{
|
||||||
|
Free: diskMinTotalSpace * 3,
|
||||||
|
FSType: "XFS",
|
||||||
|
Files: 9999,
|
||||||
|
Ffree: 9999,
|
||||||
|
},
|
||||||
|
err: errDiskFull,
|
||||||
|
},
|
||||||
|
// Test 3 - when fstype is btrfs and total inodes are empty.
|
||||||
|
{
|
||||||
|
diskInfo: disk.Info{
|
||||||
|
Free: diskMinTotalSpace * 3,
|
||||||
|
FSType: "BTRFS",
|
||||||
|
Files: 0,
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
// Test 4 - when fstype is xfs and total disk space is really small.
|
||||||
|
{
|
||||||
|
diskInfo: disk.Info{
|
||||||
|
Free: diskMinTotalSpace - diskMinTotalSpace/1024,
|
||||||
|
FSType: "XFS",
|
||||||
|
Files: 9999,
|
||||||
|
},
|
||||||
|
err: errDiskFull,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate all cases.
|
||||||
|
for i, test := range testCases {
|
||||||
|
if err := checkDiskMinFree(test.diskInfo); test.err != err {
|
||||||
|
t.Errorf("Test %d: Expected error %s, got %s", i+1, test.err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ type PostPolicyForm struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parsePostPolicyForm - Parse JSON policy string into typed POostPolicyForm structure.
|
// parsePostPolicyForm - Parse JSON policy string into typed POostPolicyForm structure.
|
||||||
func parsePostPolicyForm(policy string) (PostPolicyForm, error) {
|
func parsePostPolicyForm(policy string) (ppf PostPolicyForm, e error) {
|
||||||
// Convert po into interfaces and
|
// Convert po into interfaces and
|
||||||
// perform strict type conversion using reflection.
|
// perform strict type conversion using reflection.
|
||||||
var rawPolicy struct {
|
var rawPolicy struct {
|
||||||
@@ -122,7 +122,7 @@ func parsePostPolicyForm(policy string) (PostPolicyForm, error) {
|
|||||||
|
|
||||||
err := json.Unmarshal([]byte(policy), &rawPolicy)
|
err := json.Unmarshal([]byte(policy), &rawPolicy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PostPolicyForm{}, err
|
return ppf, err
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedPolicy := PostPolicyForm{}
|
parsedPolicy := PostPolicyForm{}
|
||||||
@@ -130,7 +130,7 @@ func parsePostPolicyForm(policy string) (PostPolicyForm, error) {
|
|||||||
// Parse expiry time.
|
// Parse expiry time.
|
||||||
parsedPolicy.Expiration, err = time.Parse(time.RFC3339Nano, rawPolicy.Expiration)
|
parsedPolicy.Expiration, err = time.Parse(time.RFC3339Nano, rawPolicy.Expiration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PostPolicyForm{}, err
|
return ppf, err
|
||||||
}
|
}
|
||||||
parsedPolicy.Conditions.Policies = make(map[string]struct {
|
parsedPolicy.Conditions.Policies = make(map[string]struct {
|
||||||
Operator string
|
Operator string
|
||||||
|
|||||||
@@ -133,6 +133,14 @@ func printConfigErrMsg(storageDisks []StorageAPI, sErrs []error, fn printOnceFun
|
|||||||
// Generate a formatted message when cluster is misconfigured.
|
// Generate a formatted message when cluster is misconfigured.
|
||||||
func getConfigErrMsg(storageDisks []StorageAPI, sErrs []error) string {
|
func getConfigErrMsg(storageDisks []StorageAPI, sErrs []error) string {
|
||||||
msg := colorBlue("\nDetected configuration inconsistencies in the cluster. Please fix following servers.")
|
msg := colorBlue("\nDetected configuration inconsistencies in the cluster. Please fix following servers.")
|
||||||
|
return msg + combineDiskErrs(storageDisks, sErrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combines each disk errors in a newline formatted string.
|
||||||
|
// this is a helper function in printing messages across
|
||||||
|
// all disks.
|
||||||
|
func combineDiskErrs(storageDisks []StorageAPI, sErrs []error) string {
|
||||||
|
var msg string
|
||||||
for i, disk := range storageDisks {
|
for i, disk := range storageDisks {
|
||||||
if disk == nil {
|
if disk == nil {
|
||||||
continue
|
continue
|
||||||
|
|||||||
+29
-11
@@ -18,6 +18,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/mc/pkg/console"
|
"github.com/minio/mc/pkg/console"
|
||||||
@@ -118,6 +119,24 @@ func quickErrToActions(errMap map[error]int) InitActions {
|
|||||||
// Preparatory initialization stage for XL validates known errors.
|
// Preparatory initialization stage for XL validates known errors.
|
||||||
// Converts them into specific actions. These actions have special purpose
|
// Converts them into specific actions. These actions have special purpose
|
||||||
// which caller decides on what needs to be done.
|
// which caller decides on what needs to be done.
|
||||||
|
|
||||||
|
// Logic used in this function is as shown below.
|
||||||
|
//
|
||||||
|
// ---- Possible states and handled conditions -----
|
||||||
|
//
|
||||||
|
// - Formatted setup
|
||||||
|
// - InitObjectLayer when `disksFormatted >= readQuorum`
|
||||||
|
// - Wait for quorum when `disksFormatted < readQuorum && disksFormatted + disksOffline >= readQuorum`
|
||||||
|
// (we don't know yet if there are unformatted disks)
|
||||||
|
// - Wait for heal when `disksFormatted >= readQuorum && disksUnformatted > 0`
|
||||||
|
// (here we know there is at least one unformatted disk which requires healing)
|
||||||
|
//
|
||||||
|
// - Unformatted setup
|
||||||
|
// - Format/Wait for format when `disksUnformatted == diskCount`
|
||||||
|
//
|
||||||
|
// - Wait for all when `disksUnformatted + disksOffline == disksCount`
|
||||||
|
//
|
||||||
|
// Under all other conditions should lead to server initialization aborted.
|
||||||
func prepForInitXL(firstDisk bool, sErrs []error, diskCount int) InitActions {
|
func prepForInitXL(firstDisk bool, sErrs []error, diskCount int) InitActions {
|
||||||
// Count errors by error value.
|
// Count errors by error value.
|
||||||
errMap := make(map[error]int)
|
errMap := make(map[error]int)
|
||||||
@@ -135,19 +154,12 @@ func prepForInitXL(firstDisk bool, sErrs []error, diskCount int) InitActions {
|
|||||||
disksOffline := errMap[errDiskNotFound]
|
disksOffline := errMap[errDiskNotFound]
|
||||||
disksFormatted := errMap[nil]
|
disksFormatted := errMap[nil]
|
||||||
disksUnformatted := errMap[errUnformattedDisk]
|
disksUnformatted := errMap[errUnformattedDisk]
|
||||||
disksCorrupted := errMap[errCorruptedFormat]
|
|
||||||
|
|
||||||
// No Quorum lots of offline disks, wait for quorum.
|
// No Quorum lots of offline disks, wait for quorum.
|
||||||
if disksOffline > readQuorum {
|
if disksOffline > readQuorum {
|
||||||
return WaitForQuorum
|
return WaitForQuorum
|
||||||
}
|
}
|
||||||
|
|
||||||
// There is quorum or more corrupted disks, there is not enough good
|
|
||||||
// disks to reconstruct format.json.
|
|
||||||
if disksCorrupted >= quorum {
|
|
||||||
return Abort
|
|
||||||
}
|
|
||||||
|
|
||||||
// All disks are unformatted, proceed to formatting disks.
|
// All disks are unformatted, proceed to formatting disks.
|
||||||
if disksUnformatted == diskCount {
|
if disksUnformatted == diskCount {
|
||||||
// Only the first server formats an uninitialized setup, others wait for notification.
|
// Only the first server formats an uninitialized setup, others wait for notification.
|
||||||
@@ -163,6 +175,7 @@ func prepForInitXL(firstDisk bool, sErrs []error, diskCount int) InitActions {
|
|||||||
if disksUnformatted+disksFormatted+disksOffline == diskCount {
|
if disksUnformatted+disksFormatted+disksOffline == diskCount {
|
||||||
return WaitForAll
|
return WaitForAll
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some disks possibly corrupted and too many unformatted disks.
|
// Some disks possibly corrupted and too many unformatted disks.
|
||||||
return Abort
|
return Abort
|
||||||
}
|
}
|
||||||
@@ -172,10 +185,13 @@ func prepForInitXL(firstDisk bool, sErrs []error, diskCount int) InitActions {
|
|||||||
if disksFormatted+disksOffline == diskCount {
|
if disksFormatted+disksOffline == diskCount {
|
||||||
return InitObjectLayer
|
return InitObjectLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some of the formatted disks are possibly corrupted or unformatted, heal them.
|
// Some of the formatted disks are possibly corrupted or unformatted, heal them.
|
||||||
return WaitForHeal
|
return WaitForHeal
|
||||||
} // Exhausted all our checks, un-handled errors perhaps we Abort.
|
}
|
||||||
return WaitForQuorum
|
|
||||||
|
// Exhausted all our checks, un-handled errors perhaps we Abort.
|
||||||
|
return Abort
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prints retry message upon a specific retry count.
|
// Prints retry message upon a specific retry count.
|
||||||
@@ -227,6 +243,7 @@ func retryFormattingXLDisks(firstDisk bool, endpoints EndpointList, storageDisks
|
|||||||
// actual errors for disks not being available.
|
// actual errors for disks not being available.
|
||||||
printRetryMsg(sErrs, storageDisks)
|
printRetryMsg(sErrs, storageDisks)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-emptively check if one of the formatted disks
|
// Pre-emptively check if one of the formatted disks
|
||||||
// is invalid. This function returns success for the
|
// is invalid. This function returns success for the
|
||||||
// most part unless one of the formats is not consistent
|
// most part unless one of the formats is not consistent
|
||||||
@@ -240,16 +257,17 @@ func retryFormattingXLDisks(firstDisk bool, endpoints EndpointList, storageDisks
|
|||||||
// first server has a wrong format and exit gracefully.
|
// first server has a wrong format and exit gracefully.
|
||||||
// refer - https://github.com/minio/minio/issues/4140
|
// refer - https://github.com/minio/minio/issues/4140
|
||||||
if retryCount > maxRetryAttempts {
|
if retryCount > maxRetryAttempts {
|
||||||
errorIf(err, "Detected disk (%s) in unexpected format",
|
errorIf(err, "%s : Detected disk in unexpected format",
|
||||||
storageDisks[index])
|
storageDisks[index])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a XL or distributed XL, anything > 1 is considered XL backend.
|
// Check if this is a XL or distributed XL, anything > 1 is considered XL backend.
|
||||||
switch prepForInitXL(firstDisk, sErrs, len(storageDisks)) {
|
switch prepForInitXL(firstDisk, sErrs, len(storageDisks)) {
|
||||||
case Abort:
|
case Abort:
|
||||||
return errCorruptedFormat
|
return fmt.Errorf("%s", combineDiskErrs(storageDisks, sErrs))
|
||||||
case FormatDisks:
|
case FormatDisks:
|
||||||
console.Eraseline()
|
console.Eraseline()
|
||||||
printFormatMsg(endpoints, storageDisks, printOnceFn())
|
printFormatMsg(endpoints, storageDisks, printOnceFn())
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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 (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/rpc"
|
||||||
|
|
||||||
|
miniohttp "github.com/minio/minio/pkg/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServeHTTP implements an http.Handler that answers RPC requests,
|
||||||
|
// hijacks the underlying connection and clears all deadlines if any.
|
||||||
|
func (server *rpcServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodConnect {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, _, err := w.(http.Hijacker).Hijack()
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "rpc hijacking failed for: %s", req.RemoteAddr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides Read/Write deadlines if any.
|
||||||
|
bufConn, ok := conn.(*miniohttp.BufConn)
|
||||||
|
if ok {
|
||||||
|
bufConn.RemoveTimeout()
|
||||||
|
conn = bufConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can connect to RPC service using HTTP CONNECT to rpcPath.
|
||||||
|
io.WriteString(conn, "HTTP/1.0 200 Connected to Go RPC\n\n")
|
||||||
|
server.ServeConn(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
type rpcServer struct{ *rpc.Server }
|
||||||
|
|
||||||
|
// Similar to rpc.NewServer() provides a custom ServeHTTP override.
|
||||||
|
func newRPCServer() *rpcServer {
|
||||||
|
return &rpcServer{rpc.NewServer()}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2017 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
router "github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArithArgs struct {
|
||||||
|
A, B int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArithReply struct {
|
||||||
|
C int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Arith int
|
||||||
|
|
||||||
|
// Some of Arith's methods have value args, some have pointer args. That's deliberate.
|
||||||
|
|
||||||
|
func (t *Arith) Add(args ArithArgs, reply *ArithReply) error {
|
||||||
|
reply.C = args.A + args.B
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGoHTTPRPC(t *testing.T) {
|
||||||
|
newServer := newRPCServer()
|
||||||
|
newServer.Register(new(Arith))
|
||||||
|
|
||||||
|
mux := router.NewRouter().SkipClean(true)
|
||||||
|
mux.Path("/foo").Handler(newServer)
|
||||||
|
|
||||||
|
httpServer := httptest.NewServer(mux)
|
||||||
|
defer httpServer.Close()
|
||||||
|
|
||||||
|
client := newRPCClient(httpServer.Listener.Addr().String(), "/foo", false)
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Synchronous calls
|
||||||
|
args := &ArithArgs{7, 8}
|
||||||
|
reply := new(ArithReply)
|
||||||
|
if err := client.Call("Arith.Add", args, reply); err != nil {
|
||||||
|
t.Errorf("Add: expected no error but got string %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reply.C != args.A+args.B {
|
||||||
|
t.Errorf("Add: expected %d got %d", reply.C, args.A+args.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(httpServer.URL + "/foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusMethodNotAllowed {
|
||||||
|
t.Errorf("Expected %d, got %d", http.StatusMethodNotAllowed, resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,8 +17,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/rpc"
|
|
||||||
|
|
||||||
router "github.com/gorilla/mux"
|
router "github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,7 +37,7 @@ func registerS3PeerRPCRouter(mux *router.Router) error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
s3PeerRPCServer := rpc.NewServer()
|
s3PeerRPCServer := newRPCServer()
|
||||||
err := s3PeerRPCServer.RegisterName("S3", s3PeerHandlers)
|
err := s3PeerRPCServer.RegisterName("S3", s3PeerHandlers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return traceError(err)
|
return traceError(err)
|
||||||
|
|||||||
+36
-105
@@ -17,21 +17,21 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"os/signal"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"syscall"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/minio/cli"
|
"github.com/minio/cli"
|
||||||
"github.com/minio/dsync"
|
"github.com/minio/dsync"
|
||||||
|
miniohttp "github.com/minio/minio/pkg/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
var serverFlags = []cli.Flag{
|
var serverFlags = []cli.Flag{
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "address",
|
Name: "address",
|
||||||
Value: ":9000",
|
Value: ":" + globalMinioPort,
|
||||||
Usage: "Bind to a specific ADDRESS:PORT, ADDRESS can be an IP or hostname.",
|
Usage: "Bind to a specific ADDRESS:PORT, ADDRESS can be an IP or hostname.",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -58,6 +58,9 @@ ENVIRONMENT VARIABLES:
|
|||||||
BROWSER:
|
BROWSER:
|
||||||
MINIO_BROWSER: To disable web browser access, set this value to "off".
|
MINIO_BROWSER: To disable web browser access, set this value to "off".
|
||||||
|
|
||||||
|
REGION:
|
||||||
|
MINIO_REGION: To set custom region. By default it is "us-east-1".
|
||||||
|
|
||||||
EXAMPLES:
|
EXAMPLES:
|
||||||
1. Start minio server on "/home/shared" directory.
|
1. Start minio server on "/home/shared" directory.
|
||||||
$ {{.HelpName}} /home/shared
|
$ {{.HelpName}} /home/shared
|
||||||
@@ -78,61 +81,9 @@ EXAMPLES:
|
|||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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'.", v18)
|
|
||||||
} else {
|
|
||||||
fatalIf(newConfig(), "Unable to initialize minio config for the first time.")
|
|
||||||
log.Println("Created minio configuration file successfully at " + getConfigDir())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serverHandleCmdArgs(ctx *cli.Context) {
|
func serverHandleCmdArgs(ctx *cli.Context) {
|
||||||
// Set configuration directory.
|
// Handle common command args.
|
||||||
{
|
handleCommonCmdArgs(ctx)
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server address.
|
// Server address.
|
||||||
serverAddr := ctx.String("address")
|
serverAddr := ctx.String("address")
|
||||||
@@ -159,36 +110,8 @@ func serverHandleCmdArgs(ctx *cli.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func serverHandleEnvVars() {
|
func serverHandleEnvVars() {
|
||||||
// Start profiler if env is set.
|
// Handle common environment variables.
|
||||||
if profiler := os.Getenv("_MINIO_PROFILER"); profiler != "" {
|
handleCommonEnvVars()
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if serverRegion := os.Getenv("MINIO_REGION"); serverRegion != "" {
|
if serverRegion := os.Getenv("MINIO_REGION"); serverRegion != "" {
|
||||||
// region Envs are set globally.
|
// region Envs are set globally.
|
||||||
@@ -210,12 +133,16 @@ func serverMain(ctx *cli.Context) {
|
|||||||
log.EnableQuiet()
|
log.EnableQuiet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle all server command args.
|
||||||
serverHandleCmdArgs(ctx)
|
serverHandleCmdArgs(ctx)
|
||||||
|
|
||||||
|
// Handle all server environment vars.
|
||||||
serverHandleEnvVars()
|
serverHandleEnvVars()
|
||||||
|
|
||||||
// Create certs path.
|
// Create certs path.
|
||||||
fatalIf(createConfigDir(), "Unable to create configuration directories.")
|
fatalIf(createConfigDir(), "Unable to create configuration directories.")
|
||||||
|
|
||||||
|
// Initialize server config.
|
||||||
initConfig()
|
initConfig()
|
||||||
|
|
||||||
// Enable loggers as per configuration file.
|
// Enable loggers as per configuration file.
|
||||||
@@ -226,8 +153,8 @@ func serverMain(ctx *cli.Context) {
|
|||||||
|
|
||||||
// Check and load SSL certificates.
|
// Check and load SSL certificates.
|
||||||
var err error
|
var err error
|
||||||
globalPublicCerts, globalRootCAs, globalIsSSL, err = getSSLConfig()
|
globalPublicCerts, globalRootCAs, globalTLSCertificate, globalIsSSL, err = getSSLConfig()
|
||||||
fatalIf(err, "Invalid SSL key file")
|
fatalIf(err, "Invalid SSL certificate file")
|
||||||
|
|
||||||
if !quietFlag {
|
if !quietFlag {
|
||||||
// Check for new updates from dl.minio.io.
|
// Check for new updates from dl.minio.io.
|
||||||
@@ -253,43 +180,47 @@ func serverMain(ctx *cli.Context) {
|
|||||||
initNSLock(globalIsDistXL)
|
initNSLock(globalIsDistXL)
|
||||||
|
|
||||||
// Configure server.
|
// Configure server.
|
||||||
handler, err := configureServerHandler(globalEndpoints)
|
// Declare handler to avoid lint errors.
|
||||||
|
var handler http.Handler
|
||||||
|
handler, err = configureServerHandler(globalEndpoints)
|
||||||
fatalIf(err, "Unable to configure one of server's RPC services.")
|
fatalIf(err, "Unable to configure one of server's RPC services.")
|
||||||
|
|
||||||
// Initialize a new HTTP server.
|
|
||||||
apiServer := NewServerMux(globalMinioAddr, handler)
|
|
||||||
|
|
||||||
// Initialize S3 Peers inter-node communication only in distributed setup.
|
// Initialize S3 Peers inter-node communication only in distributed setup.
|
||||||
initGlobalS3Peers(globalEndpoints)
|
initGlobalS3Peers(globalEndpoints)
|
||||||
|
|
||||||
// Initialize Admin Peers inter-node communication only in distributed setup.
|
// Initialize Admin Peers inter-node communication only in distributed setup.
|
||||||
initGlobalAdminPeers(globalEndpoints)
|
initGlobalAdminPeers(globalEndpoints)
|
||||||
|
|
||||||
// Start server, automatically configures TLS if certs are available.
|
globalHTTPServer = miniohttp.NewServer([]string{globalMinioAddr}, handler, globalTLSCertificate)
|
||||||
|
globalHTTPServer.UpdateBytesReadFunc = globalConnStats.incInputBytes
|
||||||
|
globalHTTPServer.UpdateBytesWrittenFunc = globalConnStats.incOutputBytes
|
||||||
|
globalHTTPServer.ErrorLogFunc = errorIf
|
||||||
go func() {
|
go func() {
|
||||||
cert, key := "", ""
|
globalHTTPServerErrorCh <- globalHTTPServer.Start()
|
||||||
if globalIsSSL {
|
|
||||||
cert, key = getPublicCertFile(), getPrivateKeyFile()
|
|
||||||
}
|
|
||||||
fatalIf(apiServer.ListenAndServe(cert, key), "Failed to start minio server.")
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
signal.Notify(globalOSSignalCh, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
newObject, err := newObjectLayer(globalEndpoints)
|
newObject, err := newObjectLayer(globalEndpoints)
|
||||||
fatalIf(err, "Initializing object layer failed")
|
if err != nil {
|
||||||
|
errorIf(err, "Initializing object layer failed")
|
||||||
|
err = globalHTTPServer.Shutdown()
|
||||||
|
errorIf(err, "Unable to shutdown http server")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
globalObjLayerMutex.Lock()
|
globalObjLayerMutex.Lock()
|
||||||
globalObjectAPI = newObject
|
globalObjectAPI = newObject
|
||||||
globalObjLayerMutex.Unlock()
|
globalObjLayerMutex.Unlock()
|
||||||
|
|
||||||
// Prints the formatted startup message once object layer is initialized.
|
// Prints the formatted startup message once object layer is initialized.
|
||||||
apiEndpoints := getAPIEndpoints(apiServer.Addr)
|
apiEndpoints := getAPIEndpoints(globalMinioAddr)
|
||||||
printStartupMessage(apiEndpoints)
|
printStartupMessage(apiEndpoints)
|
||||||
|
|
||||||
// Set uptime time after object layer has initialized.
|
// Set uptime time after object layer has initialized.
|
||||||
globalBootTime = UTCNow()
|
globalBootTime = UTCNow()
|
||||||
|
|
||||||
// Waits on the server.
|
handleSignals()
|
||||||
<-globalServiceDoneCh
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize object layer with the supplied disks, objectLayer is nil upon any error.
|
// Initialize object layer with the supplied disks, objectLayer is nil upon any error.
|
||||||
|
|||||||
@@ -1,526 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
serverShutdownPoll = 500 * time.Millisecond
|
|
||||||
)
|
|
||||||
|
|
||||||
// The value chosen below is longest word chosen
|
|
||||||
// from all the http verbs comprising of
|
|
||||||
// "PRI", "OPTIONS", "GET", "HEAD", "POST",
|
|
||||||
// "PUT", "DELETE", "TRACE", "CONNECT".
|
|
||||||
const (
|
|
||||||
maxHTTPVerbLen = 7
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTTP2 PRI method.
|
|
||||||
var httpMethodPRI = "PRI"
|
|
||||||
|
|
||||||
var defaultHTTP2Methods = []string{
|
|
||||||
httpMethodPRI,
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultHTTP1Methods = []string{
|
|
||||||
http.MethodOptions,
|
|
||||||
http.MethodGet,
|
|
||||||
http.MethodHead,
|
|
||||||
http.MethodPost,
|
|
||||||
http.MethodPut,
|
|
||||||
http.MethodDelete,
|
|
||||||
http.MethodTrace,
|
|
||||||
http.MethodConnect,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnMux - Peeks into the incoming connection for relevant
|
|
||||||
// protocol without advancing the underlying net.Conn (io.Reader).
|
|
||||||
// ConnMux - allows us to multiplex between TLS and Regular HTTP
|
|
||||||
// connections on the same listeners.
|
|
||||||
type ConnMux struct {
|
|
||||||
net.Conn
|
|
||||||
// To peek net.Conn incoming data
|
|
||||||
peeker *bufio.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewConnMux - creates a new ConnMux instance
|
|
||||||
func NewConnMux(c net.Conn) *ConnMux {
|
|
||||||
br := bufio.NewReader(c)
|
|
||||||
return &ConnMux{
|
|
||||||
Conn: c,
|
|
||||||
peeker: bufio.NewReader(br),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// List of protocols to be detected by PeekProtocol function.
|
|
||||||
const (
|
|
||||||
protocolTLS = "tls"
|
|
||||||
protocolHTTP1 = "http"
|
|
||||||
protocolHTTP2 = "http2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PeekProtocol - reads the first bytes, then checks if it is similar
|
|
||||||
// to one of the default http methods. Returns error if there are any
|
|
||||||
// errors in peeking over the connection.
|
|
||||||
func (c *ConnMux) PeekProtocol() (string, error) {
|
|
||||||
// Peek for HTTP verbs.
|
|
||||||
buf, err := c.peeker.Peek(maxHTTPVerbLen)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for HTTP2 methods first.
|
|
||||||
for _, m := range defaultHTTP2Methods {
|
|
||||||
if strings.HasPrefix(string(buf), m) {
|
|
||||||
return protocolHTTP2, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for HTTP1 methods.
|
|
||||||
for _, m := range defaultHTTP1Methods {
|
|
||||||
if strings.HasPrefix(string(buf), m) {
|
|
||||||
return protocolHTTP1, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to TLS, this is not a real indication
|
|
||||||
// that the connection is TLS but that will be
|
|
||||||
// validated later by doing a handshake.
|
|
||||||
return protocolTLS, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads from the tcp session for data sent by
|
|
||||||
// the client, additionally sets deadline for 15 secs
|
|
||||||
// after each successful read. Deadline cancels and
|
|
||||||
// returns error if the client does not send any
|
|
||||||
// data in 15 secs. Also keeps track of the total
|
|
||||||
// bytes received from the client.
|
|
||||||
func (c *ConnMux) Read(b []byte) (n int, err error) {
|
|
||||||
// Update total incoming number of bytes.
|
|
||||||
defer func() {
|
|
||||||
globalConnStats.incInputBytes(n)
|
|
||||||
}()
|
|
||||||
|
|
||||||
n, err = c.peeker.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read deadline was already set previously, set again
|
|
||||||
// after a successful read operation for future read
|
|
||||||
// operations.
|
|
||||||
c.Conn.SetReadDeadline(UTCNow().Add(defaultTCPReadTimeout))
|
|
||||||
|
|
||||||
// Success.
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to the client over a tcp session, additionally
|
|
||||||
// keeps track of the total bytes written by the server.
|
|
||||||
func (c *ConnMux) Write(b []byte) (n int, err error) {
|
|
||||||
// Update total outgoing number of bytes.
|
|
||||||
defer func() {
|
|
||||||
globalConnStats.incOutputBytes(n)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Call the conn write wrapper.
|
|
||||||
return c.Conn.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the underlying tcp connection.
|
|
||||||
func (c *ConnMux) Close() (err error) {
|
|
||||||
// Make sure that we always close a connection,
|
|
||||||
return c.Conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenerMux wraps the standard net.Listener to inspect
|
|
||||||
// the communication protocol upon network connection
|
|
||||||
// ListenerMux also wraps net.Listener to ensure that once
|
|
||||||
// Listener.Close returns, the underlying socket has been closed.
|
|
||||||
//
|
|
||||||
// - https://github.com/golang/go/issues/10527
|
|
||||||
//
|
|
||||||
// The default Listener returns from Close before the underlying
|
|
||||||
// socket has been closed if another goroutine has an active
|
|
||||||
// reference (e.g. is in Accept).
|
|
||||||
//
|
|
||||||
// The following sequence of events can happen:
|
|
||||||
//
|
|
||||||
// Goroutine 1 is running Accept, and is blocked, waiting for epoll
|
|
||||||
//
|
|
||||||
// Goroutine 2 calls Close. It sees an extra reference, and so cannot
|
|
||||||
// destroy the socket, but instead decrements a reference, marks the
|
|
||||||
// connection as closed and unblocks epoll.
|
|
||||||
//
|
|
||||||
// Goroutine 2 returns to the caller, makes a new connection.
|
|
||||||
// The new connection is sent to the socket (since it hasn't been destroyed)
|
|
||||||
//
|
|
||||||
// Goroutine 1 returns from epoll, and accepts the new connection.
|
|
||||||
//
|
|
||||||
// To avoid accepting connections after Close, we block Goroutine 2
|
|
||||||
// from returning from Close till Accept returns an error to the user.
|
|
||||||
type ListenerMux struct {
|
|
||||||
net.Listener
|
|
||||||
config *tls.Config
|
|
||||||
// acceptResCh is a channel for transporting wrapped net.Conn (regular or tls)
|
|
||||||
// after peeking the content of the latter
|
|
||||||
acceptResCh chan ListenerMuxAcceptRes
|
|
||||||
// Cond is used to signal Close when there are no references to the listener.
|
|
||||||
cond *sync.Cond
|
|
||||||
refs int
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenerMuxAcceptRes contains then final net.Conn data (wrapper by tls or not) to be sent to the http handler
|
|
||||||
type ListenerMuxAcceptRes struct {
|
|
||||||
conn net.Conn
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default keep alive interval timeout, on your Linux system to figure out
|
|
||||||
// maximum probes sent
|
|
||||||
//
|
|
||||||
// > cat /proc/sys/net/ipv4/tcp_keepalive_probes
|
|
||||||
// ! 9
|
|
||||||
//
|
|
||||||
// Final value of total keep-alive comes upto 9 x 10 * seconds = 1.5 minutes.
|
|
||||||
const defaultKeepAliveTimeout = 10 * time.Second // 10 seconds.
|
|
||||||
|
|
||||||
// Timeout to close and return error to the client when not sending any data.
|
|
||||||
const defaultTCPReadTimeout = 15 * time.Second // 15 seconds.
|
|
||||||
|
|
||||||
// newListenerMux listens and wraps accepted connections with tls after protocol peeking
|
|
||||||
func newListenerMux(listener net.Listener, config *tls.Config) *ListenerMux {
|
|
||||||
l := ListenerMux{
|
|
||||||
Listener: listener,
|
|
||||||
config: config,
|
|
||||||
cond: sync.NewCond(&sync.Mutex{}),
|
|
||||||
acceptResCh: make(chan ListenerMuxAcceptRes),
|
|
||||||
}
|
|
||||||
// Start listening, wrap connections with tls when needed
|
|
||||||
go func() {
|
|
||||||
// Extract tcp listener.
|
|
||||||
tcpListener, ok := l.Listener.(*net.TCPListener)
|
|
||||||
if !ok {
|
|
||||||
l.acceptResCh <- ListenerMuxAcceptRes{err: errInvalidArgument}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop for accepting new connections
|
|
||||||
for {
|
|
||||||
// Use accept TCP method to receive the connection.
|
|
||||||
conn, err := tcpListener.AcceptTCP()
|
|
||||||
if err != nil {
|
|
||||||
l.acceptResCh <- ListenerMuxAcceptRes{err: err}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable Read timeout
|
|
||||||
conn.SetReadDeadline(UTCNow().Add(defaultTCPReadTimeout))
|
|
||||||
|
|
||||||
// Enable keep alive for each connection.
|
|
||||||
conn.SetKeepAlive(true)
|
|
||||||
conn.SetKeepAlivePeriod(defaultKeepAliveTimeout)
|
|
||||||
|
|
||||||
// Allocate new conn muxer.
|
|
||||||
connMux := NewConnMux(conn)
|
|
||||||
|
|
||||||
// Wrap the connection with ConnMux to be able to peek the data in the incoming connection
|
|
||||||
// and decide if we need to wrap the connection itself with a TLS or not
|
|
||||||
go func(connMux *ConnMux) {
|
|
||||||
protocol, cerr := connMux.PeekProtocol()
|
|
||||||
if cerr != nil {
|
|
||||||
// io.EOF is usually returned by non-http clients,
|
|
||||||
// just close the connection to avoid any leak.
|
|
||||||
if cerr != io.EOF {
|
|
||||||
errorIf(cerr, "Unable to peek into incoming protocol")
|
|
||||||
}
|
|
||||||
connMux.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch protocol {
|
|
||||||
case protocolTLS:
|
|
||||||
tlsConn := tls.Server(connMux, l.config)
|
|
||||||
// Make sure to handshake so that we know that this
|
|
||||||
// is a TLS connection, if not we should close and reject
|
|
||||||
// such a connection.
|
|
||||||
if cerr = tlsConn.Handshake(); cerr != nil {
|
|
||||||
// Close for junk message.
|
|
||||||
tlsConn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
l.acceptResCh <- ListenerMuxAcceptRes{
|
|
||||||
conn: tlsConn,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
l.acceptResCh <- ListenerMuxAcceptRes{
|
|
||||||
conn: connMux,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(connMux)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return &l
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsClosed - Returns if the underlying listener is closed fully.
|
|
||||||
func (l *ListenerMux) IsClosed() bool {
|
|
||||||
l.cond.L.Lock()
|
|
||||||
defer l.cond.L.Unlock()
|
|
||||||
return l.refs == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *ListenerMux) incRef() {
|
|
||||||
l.cond.L.Lock()
|
|
||||||
l.refs++
|
|
||||||
l.cond.L.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *ListenerMux) decRef() {
|
|
||||||
l.cond.L.Lock()
|
|
||||||
l.refs--
|
|
||||||
newRefs := l.refs
|
|
||||||
l.cond.L.Unlock()
|
|
||||||
if newRefs == 0 {
|
|
||||||
l.cond.Broadcast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the listener.
|
|
||||||
// Any blocked Accept operations will be unblocked and return errors.
|
|
||||||
func (l *ListenerMux) Close() error {
|
|
||||||
if l == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := l.Listener.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
l.cond.L.Lock()
|
|
||||||
for l.refs > 0 {
|
|
||||||
l.cond.Wait()
|
|
||||||
}
|
|
||||||
l.cond.L.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept - peek the protocol to decide if we should wrap the
|
|
||||||
// network stream with the TLS server
|
|
||||||
func (l *ListenerMux) Accept() (net.Conn, error) {
|
|
||||||
l.incRef()
|
|
||||||
defer l.decRef()
|
|
||||||
|
|
||||||
res := <-l.acceptResCh
|
|
||||||
return res.conn, res.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerMux - the main mux server
|
|
||||||
type ServerMux struct {
|
|
||||||
Addr string
|
|
||||||
handler http.Handler
|
|
||||||
listeners []*ListenerMux
|
|
||||||
|
|
||||||
// Current number of concurrent http requests
|
|
||||||
currentReqs int32
|
|
||||||
// Time to wait before forcing server shutdown
|
|
||||||
gracefulTimeout time.Duration
|
|
||||||
|
|
||||||
mu sync.RWMutex // guards closing, and listeners
|
|
||||||
closing bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewServerMux constructor to create a ServerMux
|
|
||||||
func NewServerMux(addr string, handler http.Handler) *ServerMux {
|
|
||||||
m := &ServerMux{
|
|
||||||
Addr: addr,
|
|
||||||
handler: handler,
|
|
||||||
// Wait for 5 seconds for new incoming connnections, otherwise
|
|
||||||
// forcibly close them during graceful stop or restart.
|
|
||||||
gracefulTimeout: 5 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns configured HTTP server.
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize listeners on all ports.
|
|
||||||
func initListeners(serverAddr string, tls *tls.Config) ([]*ListenerMux, error) {
|
|
||||||
host, port, err := net.SplitHostPort(serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var listeners []*ListenerMux
|
|
||||||
if host == "" {
|
|
||||||
var listener net.Listener
|
|
||||||
listener, err = net.Listen("tcp", serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
listeners = append(listeners, newListenerMux(listener, tls))
|
|
||||||
return listeners, nil
|
|
||||||
}
|
|
||||||
var addrs []string
|
|
||||||
if net.ParseIP(host) != nil {
|
|
||||||
addrs = append(addrs, host)
|
|
||||||
} else {
|
|
||||||
addrs, err = net.LookupHost(host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(addrs) == 0 {
|
|
||||||
return nil, errUnexpected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, addr := range addrs {
|
|
||||||
var listener net.Listener
|
|
||||||
listener, err = net.Listen("tcp", net.JoinHostPort(addr, port))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
listeners = append(listeners, newListenerMux(listener, tls))
|
|
||||||
}
|
|
||||||
return listeners, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenAndServe - serve HTTP requests with protocol multiplexing support
|
|
||||||
// TLS is actived when certFile and keyFile parameters are not empty.
|
|
||||||
func (m *ServerMux) ListenAndServe(certFile, keyFile string) (err error) {
|
|
||||||
|
|
||||||
tlsEnabled := certFile != "" && keyFile != ""
|
|
||||||
|
|
||||||
config := &tls.Config{
|
|
||||||
// Causes servers to use Go's default ciphersuite preferences,
|
|
||||||
// which are tuned to avoid attacks. Does nothing on clients.
|
|
||||||
PreferServerCipherSuites: true,
|
|
||||||
// Set minimum version to TLS 1.2
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
} // Always instantiate.
|
|
||||||
|
|
||||||
if tlsEnabled {
|
|
||||||
// Configure TLS in the server
|
|
||||||
if config.NextProtos == nil {
|
|
||||||
config.NextProtos = []string{"http/1.1", "h2"}
|
|
||||||
}
|
|
||||||
config.Certificates = make([]tls.Certificate, 1)
|
|
||||||
config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go m.handleServiceSignals()
|
|
||||||
|
|
||||||
listeners, err := initListeners(m.Addr, config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.mu.Lock()
|
|
||||||
m.listeners = listeners
|
|
||||||
m.mu.Unlock()
|
|
||||||
|
|
||||||
// All http requests start to be processed by httpHandler
|
|
||||||
httpHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if tlsEnabled && r.TLS == nil {
|
|
||||||
// TLS is enabled but request is not TLS
|
|
||||||
// configured - return error to client.
|
|
||||||
writeErrorResponse(w, ErrInsecureClientRequest, &url.URL{})
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Return ServiceUnavailable for clients which are sending requests
|
|
||||||
// in shutdown phase
|
|
||||||
m.mu.RLock()
|
|
||||||
closing := m.closing
|
|
||||||
m.mu.RUnlock()
|
|
||||||
if closing {
|
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute registered handlers, update currentReqs to keep
|
|
||||||
// track of concurrent requests processing on the server
|
|
||||||
atomic.AddInt32(&m.currentReqs, 1)
|
|
||||||
m.handler.ServeHTTP(w, r)
|
|
||||||
atomic.AddInt32(&m.currentReqs, -1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var wg = &sync.WaitGroup{}
|
|
||||||
for _, listener := range listeners {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(listener *ListenerMux) {
|
|
||||||
defer wg.Done()
|
|
||||||
serr := http.Serve(listener, httpHandler)
|
|
||||||
// Do not print the error if the listener is closed.
|
|
||||||
if !listener.IsClosed() {
|
|
||||||
errorIf(serr, "Unable to serve incoming requests.")
|
|
||||||
}
|
|
||||||
}(listener)
|
|
||||||
}
|
|
||||||
// Wait for all http.Serve's to return.
|
|
||||||
wg.Wait()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close initiates the graceful shutdown
|
|
||||||
func (m *ServerMux) Close() error {
|
|
||||||
m.mu.Lock()
|
|
||||||
|
|
||||||
if m.closing {
|
|
||||||
m.mu.Unlock()
|
|
||||||
return errors.New("Server has been closed")
|
|
||||||
}
|
|
||||||
// Closed completely.
|
|
||||||
m.closing = true
|
|
||||||
|
|
||||||
// Close the listeners.
|
|
||||||
for _, listener := range m.listeners {
|
|
||||||
if err := listener.Close(); err != nil {
|
|
||||||
m.mu.Unlock()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.mu.Unlock()
|
|
||||||
|
|
||||||
// Starting graceful shutdown. Check if all requests are finished
|
|
||||||
// in regular interval or force the shutdown
|
|
||||||
ticker := time.NewTicker(serverShutdownPoll)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-time.After(m.gracefulTimeout):
|
|
||||||
return nil
|
|
||||||
case <-ticker.C:
|
|
||||||
if atomic.LoadInt32(&m.currentReqs) <= 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,506 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"math/big"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestListenerAcceptAfterClose(t *testing.T) {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for i := 0; i < 16; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
runTest(t)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runTest(t *testing.T) {
|
|
||||||
const connectionsBeforeClose = 1
|
|
||||||
|
|
||||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ln = newListenerMux(ln, &tls.Config{})
|
|
||||||
|
|
||||||
addr := ln.Addr().String()
|
|
||||||
waitForListener := make(chan error)
|
|
||||||
go func() {
|
|
||||||
defer close(waitForListener)
|
|
||||||
|
|
||||||
var connCount int
|
|
||||||
for {
|
|
||||||
conn, aerr := ln.Accept()
|
|
||||||
if aerr != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
connCount++
|
|
||||||
if connCount > connectionsBeforeClose {
|
|
||||||
waitForListener <- errUnexpected
|
|
||||||
return
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for i := 0; i < connectionsBeforeClose; i++ {
|
|
||||||
err = dial(addr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ln.Close()
|
|
||||||
dial(addr)
|
|
||||||
|
|
||||||
err = <-waitForListener
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dial(addr string) error {
|
|
||||||
conn, err := net.Dial("tcp", addr)
|
|
||||||
if err == nil {
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests initializing listeners.
|
|
||||||
func TestInitListeners(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
serverAddr string
|
|
||||||
shouldPass bool
|
|
||||||
}{
|
|
||||||
// Test 1 with ip and port.
|
|
||||||
{
|
|
||||||
serverAddr: net.JoinHostPort("127.0.0.1", "0"),
|
|
||||||
shouldPass: true,
|
|
||||||
},
|
|
||||||
// Test 2 only port.
|
|
||||||
{
|
|
||||||
serverAddr: net.JoinHostPort("", "0"),
|
|
||||||
shouldPass: true,
|
|
||||||
},
|
|
||||||
// Test 3 with no port error.
|
|
||||||
{
|
|
||||||
serverAddr: "127.0.0.1",
|
|
||||||
shouldPass: false,
|
|
||||||
},
|
|
||||||
// Test 4 with 'foobar' host not resolvable.
|
|
||||||
{
|
|
||||||
serverAddr: "foobar:9000",
|
|
||||||
shouldPass: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
listeners, err := initListeners(testCase.serverAddr, &tls.Config{})
|
|
||||||
if testCase.shouldPass {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d: Unable to initialize listeners %s", i+1, err)
|
|
||||||
}
|
|
||||||
for _, listener := range listeners {
|
|
||||||
if err = listener.Close(); err != nil {
|
|
||||||
t.Fatalf("Test %d: Unable to close listeners %s", i+1, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil && !testCase.shouldPass {
|
|
||||||
t.Fatalf("Test %d: Should fail but is successful", i+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Windows doesn't have 'localhost' hostname.
|
|
||||||
if runtime.GOOS != globalWindowsOSName {
|
|
||||||
listeners, err := initListeners("localhost:"+getFreePort(), &tls.Config{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test 3: Unable to initialize listeners %s", err)
|
|
||||||
}
|
|
||||||
for _, listener := range listeners {
|
|
||||||
if err = listener.Close(); err != nil {
|
|
||||||
t.Fatalf("Test 3: Unable to close listeners %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClose(t *testing.T) {
|
|
||||||
// Create ServerMux
|
|
||||||
m := NewServerMux("", nil)
|
|
||||||
|
|
||||||
if err := m.Close(); err != nil {
|
|
||||||
t.Error("Server errored while trying to Close", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closing again should return an error.
|
|
||||||
if err := m.Close(); err.Error() != "Server has been closed" {
|
|
||||||
t.Error("Unexepcted error expected \"Server has been closed\", got", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerMux(t *testing.T) {
|
|
||||||
var err error
|
|
||||||
var got []byte
|
|
||||||
var res *http.Response
|
|
||||||
|
|
||||||
// Create ServerMux
|
|
||||||
m := NewServerMux("127.0.0.1:0", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprint(w, "hello")
|
|
||||||
}))
|
|
||||||
// Start serving requests
|
|
||||||
go m.ListenAndServe("", "")
|
|
||||||
|
|
||||||
// Issue a GET request. Since we started server in a goroutine, it could be not ready
|
|
||||||
// at this point. So we allow until 5 failed retries before declare there is an error
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
// Sleep one second
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
// Check if one listener is ready
|
|
||||||
m.mu.Lock()
|
|
||||||
listenersCount := len(m.listeners)
|
|
||||||
m.mu.Unlock()
|
|
||||||
if listenersCount == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m.mu.Lock()
|
|
||||||
listenerAddr := m.listeners[0].Addr().String()
|
|
||||||
m.mu.Unlock()
|
|
||||||
// Issue the GET request
|
|
||||||
client := http.Client{}
|
|
||||||
res, err = client.Get("http://" + listenerAddr)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Read the request response
|
|
||||||
got, err = ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// We've got a response, quit the loop
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for error persisted after 5 times
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the web service response
|
|
||||||
if string(got) != "hello" {
|
|
||||||
t.Errorf("got %q, want hello", string(got))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerCloseBlocking(t *testing.T) {
|
|
||||||
// Create ServerMux
|
|
||||||
m := NewServerMux("127.0.0.1:0", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprint(w, "hello")
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Start serving requests in a goroutine
|
|
||||||
go m.ListenAndServe("", "")
|
|
||||||
|
|
||||||
// Dial, try until 5 times before declaring a failure
|
|
||||||
dial := func() (net.Conn, error) {
|
|
||||||
var c net.Conn
|
|
||||||
var err error
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
// Sleep one second in case of the server is not ready yet
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
// Check if there is at least one listener configured
|
|
||||||
m.mu.Lock()
|
|
||||||
if len(m.listeners) == 0 {
|
|
||||||
m.mu.Unlock()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m.mu.Unlock()
|
|
||||||
// Run the actual Dial
|
|
||||||
m.mu.Lock()
|
|
||||||
c, err = net.Dial("tcp", m.listeners[0].Addr().String())
|
|
||||||
m.mu.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial to open a StateNew but don't send anything
|
|
||||||
cnew, err := dial()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer cnew.Close()
|
|
||||||
|
|
||||||
// Dial another connection but idle after a request to have StateIdle
|
|
||||||
cidle, err := dial()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer cidle.Close()
|
|
||||||
|
|
||||||
cidle.Write([]byte("HEAD / HTTP/1.1\r\nHost: foo\r\n\r\n"))
|
|
||||||
_, err = http.ReadResponse(bufio.NewReader(cidle), nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure we don't block forever.
|
|
||||||
m.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerListenAndServePlain(t *testing.T) {
|
|
||||||
wait := make(chan struct{})
|
|
||||||
addr := net.JoinHostPort("127.0.0.1", getFreePort())
|
|
||||||
errc := make(chan error)
|
|
||||||
once := &sync.Once{}
|
|
||||||
|
|
||||||
// Initialize done channel specifically for each tests.
|
|
||||||
globalServiceDoneCh = make(chan struct{}, 1)
|
|
||||||
|
|
||||||
// Create ServerMux and when we receive a request we stop waiting
|
|
||||||
m := NewServerMux(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprint(w, "hello")
|
|
||||||
once.Do(func() { close(wait) })
|
|
||||||
}))
|
|
||||||
|
|
||||||
// ListenAndServe in a goroutine, but we don't know when it's ready
|
|
||||||
go func() { errc <- m.ListenAndServe("", "") }()
|
|
||||||
|
|
||||||
// Keep trying the server until it's accepting connections
|
|
||||||
go func() {
|
|
||||||
client := http.Client{Timeout: time.Millisecond * 10}
|
|
||||||
for {
|
|
||||||
res, _ := client.Get("http://" + addr)
|
|
||||||
if res != nil && res.StatusCode == http.StatusOK {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
select {
|
|
||||||
case err := <-errc:
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
case <-wait:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait until we get an error or wait closed
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Shutdown the ServerMux
|
|
||||||
m.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerListenAndServeTLS(t *testing.T) {
|
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
defer removeAll(rootPath)
|
|
||||||
|
|
||||||
wait := make(chan struct{})
|
|
||||||
addr := net.JoinHostPort("127.0.0.1", getFreePort())
|
|
||||||
errc := make(chan error)
|
|
||||||
once := &sync.Once{}
|
|
||||||
|
|
||||||
// Initialize done channel specifically for each tests.
|
|
||||||
globalServiceDoneCh = make(chan struct{}, 1)
|
|
||||||
|
|
||||||
// Create ServerMux and when we receive a request we stop waiting
|
|
||||||
m := NewServerMux(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprint(w, "hello")
|
|
||||||
once.Do(func() { close(wait) })
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Create a cert
|
|
||||||
err = createConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
certFile := getPublicCertFile()
|
|
||||||
keyFile := getPrivateKeyFile()
|
|
||||||
defer os.RemoveAll(certFile)
|
|
||||||
defer os.RemoveAll(keyFile)
|
|
||||||
|
|
||||||
err = generateTestCert(addr)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenAndServe in a goroutine, but we don't know when it's ready
|
|
||||||
go func() { errc <- m.ListenAndServe(certFile, keyFile) }()
|
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
tr := &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
}
|
|
||||||
client := http.Client{
|
|
||||||
Timeout: time.Millisecond * 10,
|
|
||||||
Transport: tr,
|
|
||||||
}
|
|
||||||
// Keep trying the server until it's accepting connections
|
|
||||||
start := UTCNow()
|
|
||||||
for {
|
|
||||||
res, _ := client.Get("https://" + addr)
|
|
||||||
if res != nil && res.StatusCode == http.StatusOK {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// Explicit check to terminate loop after 5 minutes
|
|
||||||
// (for investigational purpose of issue #4461)
|
|
||||||
if UTCNow().Sub(start) >= 5*time.Minute {
|
|
||||||
t.Fatalf("Failed to establish connection after 5 minutes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once a request succeeds, subsequent requests should
|
|
||||||
// work fine.
|
|
||||||
res, err := client.Get("http://" + addr)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Got unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
// Without TLS we expect a Bad-Request response from the server.
|
|
||||||
if !(res != nil && res.StatusCode == http.StatusBadRequest && res.Request.URL.Scheme == httpScheme) {
|
|
||||||
t.Fatalf("Plaintext request to TLS server did not have expected response!")
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error reading body")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the expected error is received.
|
|
||||||
bodyStr := string(body)
|
|
||||||
apiErr := getAPIError(ErrInsecureClientRequest)
|
|
||||||
if !(strings.Contains(bodyStr, apiErr.Code) && strings.Contains(bodyStr, apiErr.Description)) {
|
|
||||||
t.Fatalf("Plaintext request to TLS server did not have expected response body!")
|
|
||||||
}
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
select {
|
|
||||||
case err := <-errc:
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-wait:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait until we get an error or wait closed
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Shutdown the ServerMux
|
|
||||||
m.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateTestCert creates a cert and a key used for testing only
|
|
||||||
func generateTestCert(host string) error {
|
|
||||||
certPath := getPublicCertFile()
|
|
||||||
keyPath := getPrivateKeyFile()
|
|
||||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
||||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
template := x509.Certificate{
|
|
||||||
SerialNumber: serialNumber,
|
|
||||||
Subject: pkix.Name{
|
|
||||||
Organization: []string{"Minio Test Cert"},
|
|
||||||
},
|
|
||||||
NotBefore: UTCNow(),
|
|
||||||
NotAfter: UTCNow().Add(time.Minute * 1),
|
|
||||||
|
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip := net.ParseIP(host); ip != nil {
|
|
||||||
template.IPAddresses = append(template.IPAddresses, ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
template.IsCA = true
|
|
||||||
template.KeyUsage |= x509.KeyUsageCertSign
|
|
||||||
|
|
||||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
certOut, err := os.Create(certPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
|
||||||
certOut.Close()
|
|
||||||
|
|
||||||
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
|
||||||
keyOut.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -52,7 +52,7 @@ func printStartupMessage(apiEndPoints []string) {
|
|||||||
|
|
||||||
// Prints `mc` cli configuration message chooses
|
// Prints `mc` cli configuration message chooses
|
||||||
// first endpoint as default.
|
// first endpoint as default.
|
||||||
printCLIAccessMsg(strippedAPIEndpoints[0])
|
printCLIAccessMsg(strippedAPIEndpoints[0], "myminio")
|
||||||
|
|
||||||
// Prints documentation message.
|
// Prints documentation message.
|
||||||
printObjectAPIMsg()
|
printObjectAPIMsg()
|
||||||
@@ -109,7 +109,7 @@ func printServerCommonMsg(apiEndpoints []string) {
|
|||||||
apiEndpointStr := strings.Join(apiEndpoints, " ")
|
apiEndpointStr := strings.Join(apiEndpoints, " ")
|
||||||
|
|
||||||
// Colorize the message and print.
|
// Colorize the message and print.
|
||||||
log.Println(colorBlue("\nEndpoint: ") + colorBold(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 1), apiEndpointStr)))
|
log.Println(colorBlue("Endpoint: ") + colorBold(fmt.Sprintf(getFormatStr(len(apiEndpointStr), 1), apiEndpointStr)))
|
||||||
log.Println(colorBlue("AccessKey: ") + colorBold(fmt.Sprintf("%s ", cred.AccessKey)))
|
log.Println(colorBlue("AccessKey: ") + colorBold(fmt.Sprintf("%s ", cred.AccessKey)))
|
||||||
log.Println(colorBlue("SecretKey: ") + colorBold(fmt.Sprintf("%s ", cred.SecretKey)))
|
log.Println(colorBlue("SecretKey: ") + colorBold(fmt.Sprintf("%s ", cred.SecretKey)))
|
||||||
if region != "" {
|
if region != "" {
|
||||||
@@ -141,17 +141,17 @@ func printEventNotifiers() {
|
|||||||
|
|
||||||
// Prints startup message for command line access. Prints link to our documentation
|
// Prints startup message for command line access. Prints link to our documentation
|
||||||
// and custom platform specific message.
|
// and custom platform specific message.
|
||||||
func printCLIAccessMsg(endPoint string) {
|
func printCLIAccessMsg(endPoint string, alias string) {
|
||||||
// Get saved credentials.
|
// Get saved credentials.
|
||||||
cred := serverConfig.GetCredential()
|
cred := serverConfig.GetCredential()
|
||||||
|
|
||||||
// Configure 'mc', following block prints platform specific information for minio client.
|
// Configure 'mc', following block prints platform specific information for minio client.
|
||||||
log.Println(colorBlue("\nCommand-line Access: ") + mcQuickStartGuide)
|
log.Println(colorBlue("\nCommand-line Access: ") + mcQuickStartGuide)
|
||||||
if runtime.GOOS == globalWindowsOSName {
|
if runtime.GOOS == globalWindowsOSName {
|
||||||
mcMessage := fmt.Sprintf("$ mc.exe config host add myminio %s %s %s", endPoint, cred.AccessKey, cred.SecretKey)
|
mcMessage := fmt.Sprintf("$ mc.exe config host add %s %s %s %s", alias, endPoint, cred.AccessKey, cred.SecretKey)
|
||||||
log.Println(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
log.Println(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
||||||
} else {
|
} else {
|
||||||
mcMessage := fmt.Sprintf("$ mc config host add myminio %s %s %s", endPoint, cred.AccessKey, cred.SecretKey)
|
mcMessage := fmt.Sprintf("$ mc config host add %s %s %s %s", alias, endPoint, cred.AccessKey, cred.SecretKey)
|
||||||
log.Println(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
log.Println(fmt.Sprintf(getFormatStr(len(mcMessage), 3), mcMessage))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ func getStorageInfoMsg(storageInfo StorageInfo) string {
|
|||||||
if storageInfo.Backend.Type == Erasure {
|
if storageInfo.Backend.Type == Erasure {
|
||||||
diskInfo := fmt.Sprintf(" %d Online, %d Offline. ", storageInfo.Backend.OnlineDisks, storageInfo.Backend.OfflineDisks)
|
diskInfo := fmt.Sprintf(" %d Online, %d Offline. ", storageInfo.Backend.OnlineDisks, storageInfo.Backend.OfflineDisks)
|
||||||
if maxDiskFailures := storageInfo.Backend.ReadQuorum - storageInfo.Backend.OfflineDisks; maxDiskFailures >= 0 {
|
if maxDiskFailures := storageInfo.Backend.ReadQuorum - storageInfo.Backend.OfflineDisks; maxDiskFailures >= 0 {
|
||||||
diskInfo += fmt.Sprintf("We can withstand [%d] more drive failure(s).", maxDiskFailures)
|
diskInfo += fmt.Sprintf("We can withstand [%d] drive failure(s).", maxDiskFailures)
|
||||||
}
|
}
|
||||||
msg += colorBlue("\nStatus:") + fmt.Sprintf(getFormatStr(len(diskInfo), 8), diskInfo)
|
msg += colorBlue("\nStatus:") + fmt.Sprintf(getFormatStr(len(diskInfo), 8), diskInfo)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func TestPrintCLIAccessMsg(t *testing.T) {
|
|||||||
defer removeAll(root)
|
defer removeAll(root)
|
||||||
|
|
||||||
apiEndpoints := []string{"http://127.0.0.1:9000"}
|
apiEndpoints := []string{"http://127.0.0.1:9000"}
|
||||||
printCLIAccessMsg(apiEndpoints[0])
|
printCLIAccessMsg(apiEndpoints[0], "myminio")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test print startup message.
|
// Test print startup message.
|
||||||
|
|||||||
+2
-1
@@ -1619,7 +1619,8 @@ func (s *TestSuiteCommon) TestListObjectsHandler(c *C) {
|
|||||||
c.Assert(err, IsNil)
|
c.Assert(err, IsNil)
|
||||||
|
|
||||||
c.Assert(strings.Contains(string(getContent), "<Key>bar</Key>"), Equals, true)
|
c.Assert(strings.Contains(string(getContent), "<Key>bar</Key>"), Equals, true)
|
||||||
c.Assert(strings.Contains(string(getContent), "<Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner>"), Equals, true)
|
c.Assert(strings.Contains(string(getContent), fmt.Sprintf("<Owner><ID>%s</ID><DisplayName></DisplayName></Owner>",
|
||||||
|
globalMinioDefaultOwnerID)), Equals, true)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Type of service signals currently supported.
|
// Type of service signals currently supported.
|
||||||
@@ -64,59 +62,3 @@ func restartProcess() error {
|
|||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
return cmd.Start()
|
return cmd.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles all serviceSignal and execute service functions.
|
|
||||||
func (m *ServerMux) handleServiceSignals() error {
|
|
||||||
// Custom exit function
|
|
||||||
runExitFn := func(err error) {
|
|
||||||
// If global profiler is set stop before we exit.
|
|
||||||
if globalProfiler != nil {
|
|
||||||
globalProfiler.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call user supplied user exit function
|
|
||||||
fatalIf(err, "Unable to gracefully complete service operation.")
|
|
||||||
|
|
||||||
// We are usually done here, close global service done channel.
|
|
||||||
globalServiceDoneCh <- struct{}{}
|
|
||||||
}
|
|
||||||
// Wait for SIGTERM in a go-routine.
|
|
||||||
trapCh := signalTrap(os.Interrupt, syscall.SIGTERM)
|
|
||||||
go func(trapCh <-chan bool) {
|
|
||||||
<-trapCh
|
|
||||||
globalServiceSignalCh <- serviceStop
|
|
||||||
}(trapCh)
|
|
||||||
|
|
||||||
// Start listening on service signal. Monitor signals.
|
|
||||||
for {
|
|
||||||
signal := <-globalServiceSignalCh
|
|
||||||
switch signal {
|
|
||||||
case serviceStatus:
|
|
||||||
/// We don't do anything for this.
|
|
||||||
case serviceRestart:
|
|
||||||
if err := m.Close(); err != nil {
|
|
||||||
errorIf(err, "Unable to close server gracefully")
|
|
||||||
}
|
|
||||||
if err := restartProcess(); err != nil {
|
|
||||||
errorIf(err, "Unable to restart the server.")
|
|
||||||
}
|
|
||||||
runExitFn(nil)
|
|
||||||
case serviceStop:
|
|
||||||
log.Println("Received signal to exit.")
|
|
||||||
go func() {
|
|
||||||
time.Sleep(serverShutdownPoll + time.Millisecond*100)
|
|
||||||
log.Println("Waiting for active connections to terminate - press Ctrl+C to quit immediately.")
|
|
||||||
}()
|
|
||||||
if err := m.Close(); err != nil {
|
|
||||||
errorIf(err, "Unable to close server gracefully")
|
|
||||||
}
|
|
||||||
objAPI := newObjectLayerFn()
|
|
||||||
if objAPI == nil {
|
|
||||||
// Server not initialized yet, exit happily.
|
|
||||||
runExitFn(nil)
|
|
||||||
} else {
|
|
||||||
runExitFn(objAPI.Shutdown())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+56
-21
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Minio Client, (C) 2015 Minio, Inc.
|
* Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -18,32 +18,67 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// signalTrap traps the registered signals and notifies the caller.
|
func handleSignals() {
|
||||||
func signalTrap(sig ...os.Signal) <-chan bool {
|
// Custom exit function
|
||||||
// channel to notify the caller.
|
exit := func(state bool) {
|
||||||
trapCh := make(chan bool, 1)
|
// If global profiler is set stop before we exit.
|
||||||
|
if globalProfiler != nil {
|
||||||
|
globalProfiler.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
go func(chan<- bool) {
|
if state {
|
||||||
// channel to receive signals.
|
os.Exit(0)
|
||||||
sigCh := make(chan os.Signal, 1)
|
}
|
||||||
defer close(sigCh)
|
|
||||||
|
|
||||||
// `signal.Notify` registers the given channel to
|
os.Exit(1)
|
||||||
// receive notifications of the specified signals.
|
}
|
||||||
signal.Notify(sigCh, sig...)
|
|
||||||
|
|
||||||
// Wait for the signal.
|
stopProcess := func() bool {
|
||||||
<-sigCh
|
var err, oerr error
|
||||||
|
|
||||||
// Once signal has been received stop signal Notify handler.
|
err = globalHTTPServer.Shutdown()
|
||||||
signal.Stop(sigCh)
|
errorIf(err, "Unable to shutdown http server")
|
||||||
|
|
||||||
// Notify the caller.
|
if objAPI := newObjectLayerFn(); objAPI != nil {
|
||||||
trapCh <- true
|
oerr = objAPI.Shutdown()
|
||||||
}(trapCh)
|
errorIf(oerr, "Unable to shutdown object layer")
|
||||||
|
}
|
||||||
|
|
||||||
return trapCh
|
return (err == nil && oerr == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-globalHTTPServerErrorCh:
|
||||||
|
errorIf(err, "http server exited abnormally")
|
||||||
|
var oerr error
|
||||||
|
if objAPI := newObjectLayerFn(); objAPI != nil {
|
||||||
|
oerr = objAPI.Shutdown()
|
||||||
|
errorIf(oerr, "Unable to shutdown object layer")
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(err == nil && oerr == nil)
|
||||||
|
case osSignal := <-globalOSSignalCh:
|
||||||
|
log.Printf("Exiting on signal %v\n", osSignal)
|
||||||
|
exit(stopProcess())
|
||||||
|
case signal := <-globalServiceSignalCh:
|
||||||
|
switch signal {
|
||||||
|
case serviceStatus:
|
||||||
|
// Ignore this at the moment.
|
||||||
|
case serviceRestart:
|
||||||
|
log.Println("Restarting on service signal")
|
||||||
|
err := globalHTTPServer.Shutdown()
|
||||||
|
errorIf(err, "Unable to shutdown http server")
|
||||||
|
rerr := restartProcess()
|
||||||
|
errorIf(rerr, "Unable to restart the server")
|
||||||
|
|
||||||
|
exit(err == nil && rerr == nil)
|
||||||
|
case serviceStop:
|
||||||
|
log.Println("Stopping on service signal")
|
||||||
|
exit(stopProcess())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-24
@@ -45,20 +45,20 @@ func (c credentialHeader) getScope() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parse credentialHeader string into its structured form.
|
// parse credentialHeader string into its structured form.
|
||||||
func parseCredentialHeader(credElement string) (credentialHeader, APIErrorCode) {
|
func parseCredentialHeader(credElement string) (ch credentialHeader, aec APIErrorCode) {
|
||||||
creds := strings.Split(strings.TrimSpace(credElement), "=")
|
creds := strings.Split(strings.TrimSpace(credElement), "=")
|
||||||
if len(creds) != 2 {
|
if len(creds) != 2 {
|
||||||
return credentialHeader{}, ErrMissingFields
|
return ch, ErrMissingFields
|
||||||
}
|
}
|
||||||
if creds[0] != "Credential" {
|
if creds[0] != "Credential" {
|
||||||
return credentialHeader{}, ErrMissingCredTag
|
return ch, ErrMissingCredTag
|
||||||
}
|
}
|
||||||
credElements := strings.Split(strings.TrimSpace(creds[1]), "/")
|
credElements := strings.Split(strings.TrimSpace(creds[1]), "/")
|
||||||
if len(credElements) != 5 {
|
if len(credElements) != 5 {
|
||||||
return credentialHeader{}, ErrCredMalformed
|
return ch, ErrCredMalformed
|
||||||
}
|
}
|
||||||
if !isAccessKeyValid(credElements[0]) {
|
if !isAccessKeyValid(credElements[0]) {
|
||||||
return credentialHeader{}, ErrInvalidAccessKeyID
|
return ch, ErrInvalidAccessKeyID
|
||||||
}
|
}
|
||||||
// Save access key id.
|
// Save access key id.
|
||||||
cred := credentialHeader{
|
cred := credentialHeader{
|
||||||
@@ -67,15 +67,15 @@ func parseCredentialHeader(credElement string) (credentialHeader, APIErrorCode)
|
|||||||
var e error
|
var e error
|
||||||
cred.scope.date, e = time.Parse(yyyymmdd, credElements[1])
|
cred.scope.date, e = time.Parse(yyyymmdd, credElements[1])
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return credentialHeader{}, ErrMalformedCredentialDate
|
return ch, ErrMalformedCredentialDate
|
||||||
}
|
}
|
||||||
cred.scope.region = credElements[2]
|
cred.scope.region = credElements[2]
|
||||||
if credElements[3] != "s3" {
|
if credElements[3] != "s3" {
|
||||||
return credentialHeader{}, ErrInvalidService
|
return ch, ErrInvalidService
|
||||||
}
|
}
|
||||||
cred.scope.service = credElements[3]
|
cred.scope.service = credElements[3]
|
||||||
if credElements[4] != "aws4_request" {
|
if credElements[4] != "aws4_request" {
|
||||||
return credentialHeader{}, ErrInvalidRequestVersion
|
return ch, ErrInvalidRequestVersion
|
||||||
}
|
}
|
||||||
cred.scope.request = credElements[4]
|
cred.scope.request = credElements[4]
|
||||||
return cred, ErrNone
|
return cred, ErrNone
|
||||||
@@ -148,17 +148,17 @@ func doesV4PresignParamsExist(query url.Values) APIErrorCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parses all the presigned signature values into separate elements.
|
// Parses all the presigned signature values into separate elements.
|
||||||
func parsePreSignV4(query url.Values) (preSignValues, APIErrorCode) {
|
func parsePreSignV4(query url.Values) (psv preSignValues, aec APIErrorCode) {
|
||||||
var err APIErrorCode
|
var err APIErrorCode
|
||||||
// verify whether the required query params exist.
|
// verify whether the required query params exist.
|
||||||
err = doesV4PresignParamsExist(query)
|
err = doesV4PresignParamsExist(query)
|
||||||
if err != ErrNone {
|
if err != ErrNone {
|
||||||
return preSignValues{}, err
|
return psv, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify if the query algorithm is supported or not.
|
// Verify if the query algorithm is supported or not.
|
||||||
if query.Get("X-Amz-Algorithm") != signV4Algorithm {
|
if query.Get("X-Amz-Algorithm") != signV4Algorithm {
|
||||||
return preSignValues{}, ErrInvalidQuerySignatureAlgo
|
return psv, ErrInvalidQuerySignatureAlgo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize signature version '4' structured header.
|
// Initialize signature version '4' structured header.
|
||||||
@@ -167,35 +167,35 @@ func parsePreSignV4(query url.Values) (preSignValues, APIErrorCode) {
|
|||||||
// Save credential.
|
// Save credential.
|
||||||
preSignV4Values.Credential, err = parseCredentialHeader("Credential=" + query.Get("X-Amz-Credential"))
|
preSignV4Values.Credential, err = parseCredentialHeader("Credential=" + query.Get("X-Amz-Credential"))
|
||||||
if err != ErrNone {
|
if err != ErrNone {
|
||||||
return preSignValues{}, err
|
return psv, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var e error
|
var e error
|
||||||
// Save date in native time.Time.
|
// Save date in native time.Time.
|
||||||
preSignV4Values.Date, e = time.Parse(iso8601Format, query.Get("X-Amz-Date"))
|
preSignV4Values.Date, e = time.Parse(iso8601Format, query.Get("X-Amz-Date"))
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return preSignValues{}, ErrMalformedPresignedDate
|
return psv, ErrMalformedPresignedDate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save expires in native time.Duration.
|
// Save expires in native time.Duration.
|
||||||
preSignV4Values.Expires, e = time.ParseDuration(query.Get("X-Amz-Expires") + "s")
|
preSignV4Values.Expires, e = time.ParseDuration(query.Get("X-Amz-Expires") + "s")
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return preSignValues{}, ErrMalformedExpires
|
return psv, ErrMalformedExpires
|
||||||
}
|
}
|
||||||
|
|
||||||
if preSignV4Values.Expires < 0 {
|
if preSignV4Values.Expires < 0 {
|
||||||
return preSignValues{}, ErrNegativeExpires
|
return psv, ErrNegativeExpires
|
||||||
}
|
}
|
||||||
// Save signed headers.
|
// Save signed headers.
|
||||||
preSignV4Values.SignedHeaders, err = parseSignedHeader("SignedHeaders=" + query.Get("X-Amz-SignedHeaders"))
|
preSignV4Values.SignedHeaders, err = parseSignedHeader("SignedHeaders=" + query.Get("X-Amz-SignedHeaders"))
|
||||||
if err != ErrNone {
|
if err != ErrNone {
|
||||||
return preSignValues{}, err
|
return psv, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save signature.
|
// Save signature.
|
||||||
preSignV4Values.Signature, err = parseSignature("Signature=" + query.Get("X-Amz-Signature"))
|
preSignV4Values.Signature, err = parseSignature("Signature=" + query.Get("X-Amz-Signature"))
|
||||||
if err != ErrNone {
|
if err != ErrNone {
|
||||||
return preSignValues{}, err
|
return psv, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return structed form of signature query string.
|
// Return structed form of signature query string.
|
||||||
@@ -207,25 +207,25 @@ func parsePreSignV4(query url.Values) (preSignValues, APIErrorCode) {
|
|||||||
// Authorization: algorithm Credential=accessKeyID/credScope, \
|
// Authorization: algorithm Credential=accessKeyID/credScope, \
|
||||||
// SignedHeaders=signedHeaders, Signature=signature
|
// SignedHeaders=signedHeaders, Signature=signature
|
||||||
//
|
//
|
||||||
func parseSignV4(v4Auth string) (signValues, APIErrorCode) {
|
func parseSignV4(v4Auth string) (sv signValues, aec APIErrorCode) {
|
||||||
// Replace all spaced strings, some clients can send spaced
|
// Replace all spaced strings, some clients can send spaced
|
||||||
// parameters and some won't. So we pro-actively remove any spaces
|
// parameters and some won't. So we pro-actively remove any spaces
|
||||||
// to make parsing easier.
|
// to make parsing easier.
|
||||||
v4Auth = strings.Replace(v4Auth, " ", "", -1)
|
v4Auth = strings.Replace(v4Auth, " ", "", -1)
|
||||||
if v4Auth == "" {
|
if v4Auth == "" {
|
||||||
return signValues{}, ErrAuthHeaderEmpty
|
return sv, ErrAuthHeaderEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify if the header algorithm is supported or not.
|
// Verify if the header algorithm is supported or not.
|
||||||
if !strings.HasPrefix(v4Auth, signV4Algorithm) {
|
if !strings.HasPrefix(v4Auth, signV4Algorithm) {
|
||||||
return signValues{}, ErrSignatureVersionNotSupported
|
return sv, ErrSignatureVersionNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip off the Algorithm prefix.
|
// Strip off the Algorithm prefix.
|
||||||
v4Auth = strings.TrimPrefix(v4Auth, signV4Algorithm)
|
v4Auth = strings.TrimPrefix(v4Auth, signV4Algorithm)
|
||||||
authFields := strings.Split(strings.TrimSpace(v4Auth), ",")
|
authFields := strings.Split(strings.TrimSpace(v4Auth), ",")
|
||||||
if len(authFields) != 3 {
|
if len(authFields) != 3 {
|
||||||
return signValues{}, ErrMissingFields
|
return sv, ErrMissingFields
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize signature version '4' structured header.
|
// Initialize signature version '4' structured header.
|
||||||
@@ -235,19 +235,19 @@ func parseSignV4(v4Auth string) (signValues, APIErrorCode) {
|
|||||||
// Save credentail values.
|
// Save credentail values.
|
||||||
signV4Values.Credential, err = parseCredentialHeader(authFields[0])
|
signV4Values.Credential, err = parseCredentialHeader(authFields[0])
|
||||||
if err != ErrNone {
|
if err != ErrNone {
|
||||||
return signValues{}, err
|
return sv, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save signed headers.
|
// Save signed headers.
|
||||||
signV4Values.SignedHeaders, err = parseSignedHeader(authFields[1])
|
signV4Values.SignedHeaders, err = parseSignedHeader(authFields[1])
|
||||||
if err != ErrNone {
|
if err != ErrNone {
|
||||||
return signValues{}, err
|
return sv, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save signature.
|
// Save signature.
|
||||||
signV4Values.Signature, err = parseSignature(authFields[2])
|
signV4Values.Signature, err = parseSignature(authFields[2])
|
||||||
if err != ErrNone {
|
if err != ErrNone {
|
||||||
return signValues{}, err
|
return sv, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the structure here.
|
// Return the structure here.
|
||||||
|
|||||||
+3
-4
@@ -39,10 +39,9 @@ import (
|
|||||||
|
|
||||||
// AWS Signature Version '4' constants.
|
// AWS Signature Version '4' constants.
|
||||||
const (
|
const (
|
||||||
signV4Algorithm = "AWS4-HMAC-SHA256"
|
signV4Algorithm = "AWS4-HMAC-SHA256"
|
||||||
iso8601Format = "20060102T150405Z"
|
iso8601Format = "20060102T150405Z"
|
||||||
yyyymmdd = "20060102"
|
yyyymmdd = "20060102"
|
||||||
presignedHostHeader = "host"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// getCanonicalHeaders generate a list of request headers with their values
|
// getCanonicalHeaders generate a list of request headers with their values
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ var errUnexpected = errors.New("Unexpected error, please report this issue at ht
|
|||||||
// errCorruptedFormat - corrupted backend format.
|
// errCorruptedFormat - corrupted backend format.
|
||||||
var errCorruptedFormat = errors.New("corrupted backend format, please join https://slack.minio.io for assistance")
|
var errCorruptedFormat = errors.New("corrupted backend format, please join https://slack.minio.io for assistance")
|
||||||
|
|
||||||
|
// errFormatNotSupported - returned when older minio tries to parse metadata
|
||||||
|
// created by newer minio.
|
||||||
|
var errFormatNotSupported = errors.New("format not supported")
|
||||||
|
|
||||||
// errUnformattedDisk - unformatted disk found.
|
// errUnformattedDisk - unformatted disk found.
|
||||||
var errUnformattedDisk = errors.New("unformatted disk found")
|
var errUnformattedDisk = errors.New("unformatted disk found")
|
||||||
|
|
||||||
@@ -51,9 +55,6 @@ var errFileNotFound = errors.New("file not found")
|
|||||||
// errFileNameTooLong - given file name is too long than supported length.
|
// errFileNameTooLong - given file name is too long than supported length.
|
||||||
var errFileNameTooLong = errors.New("file name too long")
|
var errFileNameTooLong = errors.New("file name too long")
|
||||||
|
|
||||||
// errFileComponentInvalid - given file name has invalid components.
|
|
||||||
var errFileComponentInvalid = errors.New("file name has invalid components")
|
|
||||||
|
|
||||||
// errVolumeExists - cannot create same volume again.
|
// errVolumeExists - cannot create same volume again.
|
||||||
var errVolumeExists = errors.New("volume already exists")
|
var errVolumeExists = errors.New("volume already exists")
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/rpc"
|
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -217,7 +216,7 @@ func (s *storageServer) RenameFileHandler(args *RenameFileArgs, reply *AuthRPCRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize new storage rpc.
|
// Initialize new storage rpc.
|
||||||
func newRPCServer(endpoints EndpointList) (servers []*storageServer, err error) {
|
func newStorageRPCServer(endpoints EndpointList) (servers []*storageServer, err error) {
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
if endpoint.IsLocal {
|
if endpoint.IsLocal {
|
||||||
storage, err := newPosix(endpoint.Path)
|
storage, err := newPosix(endpoint.Path)
|
||||||
@@ -238,14 +237,14 @@ func newRPCServer(endpoints EndpointList) (servers []*storageServer, err error)
|
|||||||
// registerStorageRPCRouter - register storage rpc router.
|
// registerStorageRPCRouter - register storage rpc router.
|
||||||
func registerStorageRPCRouters(mux *router.Router, endpoints EndpointList) error {
|
func registerStorageRPCRouters(mux *router.Router, endpoints EndpointList) error {
|
||||||
// Initialize storage rpc servers for every disk that is hosted on this node.
|
// Initialize storage rpc servers for every disk that is hosted on this node.
|
||||||
storageRPCs, err := newRPCServer(endpoints)
|
storageRPCs, err := newStorageRPCServer(endpoints)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return traceError(err)
|
return traceError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a unique route for each disk exported from this node.
|
// Create a unique route for each disk exported from this node.
|
||||||
for _, stServer := range storageRPCs {
|
for _, stServer := range storageRPCs {
|
||||||
storageRPCServer := rpc.NewServer()
|
storageRPCServer := newRPCServer()
|
||||||
err = storageRPCServer.RegisterName("Storage", stServer)
|
err = storageRPCServer.RegisterName("Storage", stServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return traceError(err)
|
return traceError(err)
|
||||||
|
|||||||
@@ -41,13 +41,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// getChunkSignature - get chunk signature.
|
// getChunkSignature - get chunk signature.
|
||||||
func getChunkSignature(seedSignature string, date time.Time, hashedChunk string) string {
|
func getChunkSignature(seedSignature string, region string, date time.Time, hashedChunk string) string {
|
||||||
// Access credentials.
|
// Access credentials.
|
||||||
cred := serverConfig.GetCredential()
|
cred := serverConfig.GetCredential()
|
||||||
|
|
||||||
// Server region.
|
|
||||||
region := serverConfig.GetRegion()
|
|
||||||
|
|
||||||
// Calculate string to sign.
|
// Calculate string to sign.
|
||||||
stringToSign := signV4ChunkedAlgorithm + "\n" +
|
stringToSign := signV4ChunkedAlgorithm + "\n" +
|
||||||
date.Format(iso8601Format) + "\n" +
|
date.Format(iso8601Format) + "\n" +
|
||||||
@@ -69,12 +66,12 @@ func getChunkSignature(seedSignature string, date time.Time, hashedChunk string)
|
|||||||
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
|
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
|
||||||
// returns signature, error otherwise if the signature mismatches or any other
|
// returns signature, error otherwise if the signature mismatches or any other
|
||||||
// error while parsing and validating.
|
// error while parsing and validating.
|
||||||
func calculateSeedSignature(r *http.Request) (signature string, date time.Time, errCode APIErrorCode) {
|
func calculateSeedSignature(r *http.Request) (signature string, region string, date time.Time, errCode APIErrorCode) {
|
||||||
// Access credentials.
|
// Access credentials.
|
||||||
cred := serverConfig.GetCredential()
|
cred := serverConfig.GetCredential()
|
||||||
|
|
||||||
// Server region.
|
// Configured region.
|
||||||
region := serverConfig.GetRegion()
|
confRegion := serverConfig.GetRegion()
|
||||||
|
|
||||||
// Copy request.
|
// Copy request.
|
||||||
req := *r
|
req := *r
|
||||||
@@ -85,7 +82,7 @@ func calculateSeedSignature(r *http.Request) (signature string, date time.Time,
|
|||||||
// Parse signature version '4' header.
|
// Parse signature version '4' header.
|
||||||
signV4Values, errCode := parseSignV4(v4Auth)
|
signV4Values, errCode := parseSignV4(v4Auth)
|
||||||
if errCode != ErrNone {
|
if errCode != ErrNone {
|
||||||
return "", time.Time{}, errCode
|
return "", "", time.Time{}, errCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payload streaming.
|
// Payload streaming.
|
||||||
@@ -93,32 +90,32 @@ func calculateSeedSignature(r *http.Request) (signature string, date time.Time,
|
|||||||
|
|
||||||
// Payload for STREAMING signature should be 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD'
|
// Payload for STREAMING signature should be 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD'
|
||||||
if payload != req.Header.Get("X-Amz-Content-Sha256") {
|
if payload != req.Header.Get("X-Amz-Content-Sha256") {
|
||||||
return "", time.Time{}, ErrContentSHA256Mismatch
|
return "", "", time.Time{}, ErrContentSHA256Mismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract all the signed headers along with its values.
|
// Extract all the signed headers along with its values.
|
||||||
extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r)
|
extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r)
|
||||||
if errCode != ErrNone {
|
if errCode != ErrNone {
|
||||||
return "", time.Time{}, errCode
|
return "", "", time.Time{}, errCode
|
||||||
}
|
}
|
||||||
// Verify if the access key id matches.
|
// Verify if the access key id matches.
|
||||||
if signV4Values.Credential.accessKey != cred.AccessKey {
|
if signV4Values.Credential.accessKey != cred.AccessKey {
|
||||||
return "", time.Time{}, ErrInvalidAccessKeyID
|
return "", "", time.Time{}, ErrInvalidAccessKeyID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify if region is valid.
|
// Verify if region is valid.
|
||||||
sRegion := signV4Values.Credential.scope.region
|
region = signV4Values.Credential.scope.region
|
||||||
// Should validate region, only if region is set. Some operations
|
// Should validate region, only if region is set. Some operations
|
||||||
// do not need region validated for example GetBucketLocation.
|
// do not need region validated for example GetBucketLocation.
|
||||||
if !isValidRegion(sRegion, region) {
|
if !isValidRegion(region, confRegion) {
|
||||||
return "", time.Time{}, ErrInvalidRegion
|
return "", "", time.Time{}, ErrInvalidRegion
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract date, if not present throw error.
|
// Extract date, if not present throw error.
|
||||||
var dateStr string
|
var dateStr string
|
||||||
if dateStr = req.Header.Get(http.CanonicalHeaderKey("x-amz-date")); dateStr == "" {
|
if dateStr = req.Header.Get(http.CanonicalHeaderKey("x-amz-date")); dateStr == "" {
|
||||||
if dateStr = r.Header.Get("Date"); dateStr == "" {
|
if dateStr = r.Header.Get("Date"); dateStr == "" {
|
||||||
return "", time.Time{}, ErrMissingDateHeader
|
return "", "", time.Time{}, ErrMissingDateHeader
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Parse date header.
|
// Parse date header.
|
||||||
@@ -126,7 +123,7 @@ func calculateSeedSignature(r *http.Request) (signature string, date time.Time,
|
|||||||
date, err = time.Parse(iso8601Format, dateStr)
|
date, err = time.Parse(iso8601Format, dateStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorIf(err, "Unable to parse date", dateStr)
|
errorIf(err, "Unable to parse date", dateStr)
|
||||||
return "", time.Time{}, ErrMalformedDate
|
return "", "", time.Time{}, ErrMalformedDate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query string.
|
// Query string.
|
||||||
@@ -146,11 +143,11 @@ func calculateSeedSignature(r *http.Request) (signature string, date time.Time,
|
|||||||
|
|
||||||
// Verify if signature match.
|
// Verify if signature match.
|
||||||
if newSignature != signV4Values.Signature {
|
if newSignature != signV4Values.Signature {
|
||||||
return "", time.Time{}, ErrSignatureDoesNotMatch
|
return "", "", time.Time{}, ErrSignatureDoesNotMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return caculated signature.
|
// Return caculated signature.
|
||||||
return newSignature, date, ErrNone
|
return newSignature, region, date, ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxLineLength = 4 * humanize.KiByte // assumed <= bufio.defaultBufSize 4KiB
|
const maxLineLength = 4 * humanize.KiByte // assumed <= bufio.defaultBufSize 4KiB
|
||||||
@@ -168,7 +165,7 @@ var errMalformedEncoding = errors.New("malformed chunked encoding")
|
|||||||
// NewChunkedReader is not needed by normal applications. The http package
|
// NewChunkedReader is not needed by normal applications. The http package
|
||||||
// automatically decodes chunking when reading response bodies.
|
// automatically decodes chunking when reading response bodies.
|
||||||
func newSignV4ChunkedReader(req *http.Request) (io.Reader, APIErrorCode) {
|
func newSignV4ChunkedReader(req *http.Request) (io.Reader, APIErrorCode) {
|
||||||
seedSignature, seedDate, errCode := calculateSeedSignature(req)
|
seedSignature, region, seedDate, errCode := calculateSeedSignature(req)
|
||||||
if errCode != ErrNone {
|
if errCode != ErrNone {
|
||||||
return nil, errCode
|
return nil, errCode
|
||||||
}
|
}
|
||||||
@@ -176,6 +173,7 @@ func newSignV4ChunkedReader(req *http.Request) (io.Reader, APIErrorCode) {
|
|||||||
reader: bufio.NewReader(req.Body),
|
reader: bufio.NewReader(req.Body),
|
||||||
seedSignature: seedSignature,
|
seedSignature: seedSignature,
|
||||||
seedDate: seedDate,
|
seedDate: seedDate,
|
||||||
|
region: region,
|
||||||
chunkSHA256Writer: sha256.New(),
|
chunkSHA256Writer: sha256.New(),
|
||||||
state: readChunkHeader,
|
state: readChunkHeader,
|
||||||
}, ErrNone
|
}, ErrNone
|
||||||
@@ -187,6 +185,7 @@ type s3ChunkedReader struct {
|
|||||||
reader *bufio.Reader
|
reader *bufio.Reader
|
||||||
seedSignature string
|
seedSignature string
|
||||||
seedDate time.Time
|
seedDate time.Time
|
||||||
|
region string
|
||||||
state chunkState
|
state chunkState
|
||||||
lastChunk bool
|
lastChunk bool
|
||||||
chunkSignature string
|
chunkSignature string
|
||||||
@@ -304,7 +303,7 @@ func (cr *s3ChunkedReader) Read(buf []byte) (n int, err error) {
|
|||||||
// Calculate the hashed chunk.
|
// Calculate the hashed chunk.
|
||||||
hashedChunk := hex.EncodeToString(cr.chunkSHA256Writer.Sum(nil))
|
hashedChunk := hex.EncodeToString(cr.chunkSHA256Writer.Sum(nil))
|
||||||
// Calculate the chunk signature.
|
// Calculate the chunk signature.
|
||||||
newSignature := getChunkSignature(cr.seedSignature, cr.seedDate, hashedChunk)
|
newSignature := getChunkSignature(cr.seedSignature, cr.region, cr.seedDate, hashedChunk)
|
||||||
if cr.chunkSignature != newSignature {
|
if cr.chunkSignature != newSignature {
|
||||||
// Chunk signature doesn't match we return signature does not match.
|
// Chunk signature doesn't match we return signature does not match.
|
||||||
cr.err = errSignatureMismatch
|
cr.err = errSignatureMismatch
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -140,6 +141,30 @@ func IsKubernetes() bool {
|
|||||||
return os.Getenv("KUBERNETES_SERVICE_HOST") != ""
|
return os.Getenv("KUBERNETES_SERVICE_HOST") != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Minio Helm chart uses DownwardAPIFile to write pod label info to /podinfo/labels
|
||||||
|
// More info: https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/#store-pod-fields
|
||||||
|
// Check if this is Helm package installation and report helm chart version
|
||||||
|
func getHelmVersion(helmInfoFilePath string) string {
|
||||||
|
// Read the file exists.
|
||||||
|
helmInfoFile, err := os.Open(helmInfoFilePath)
|
||||||
|
// Log errors and return "" as Minio can be deployed without Helm charts as well.
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
errorIf(err, "Unable to read %s", helmInfoFilePath)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(helmInfoFile)
|
||||||
|
for scanner.Scan() {
|
||||||
|
if strings.Contains(scanner.Text(), "chart=") {
|
||||||
|
helmChartVersion := strings.TrimPrefix(scanner.Text(), "chart=")
|
||||||
|
// remove quotes from the chart version
|
||||||
|
return strings.Trim(helmChartVersion, `"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func isSourceBuild(minioVersion string) bool {
|
func isSourceBuild(minioVersion string) bool {
|
||||||
_, err := time.Parse(time.RFC3339, minioVersion)
|
_, err := time.Parse(time.RFC3339, minioVersion)
|
||||||
return err != nil
|
return err != nil
|
||||||
@@ -182,6 +207,15 @@ func getUserAgent(mode string) string {
|
|||||||
userAgent += " Minio/" + "universe-" + universePkgVersion
|
userAgent += " Minio/" + "universe-" + universePkgVersion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if IsKubernetes() {
|
||||||
|
// In Kubernetes environment, try to fetch the helm package version
|
||||||
|
helmChartVersion := getHelmVersion("/podinfo/labels")
|
||||||
|
if helmChartVersion != "" {
|
||||||
|
userAgent += " Minio/" + "helm-" + helmChartVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return userAgent
|
return userAgent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -235,6 +235,48 @@ func TestIsKubernetes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests if the environment we are running is Helm chart.
|
||||||
|
func TestGetHelmVersion(t *testing.T) {
|
||||||
|
createTempFile := func(content string) string {
|
||||||
|
tmpfile, err := ioutil.TempFile("", "helm-testfile-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unable to create temporary file. %s", err)
|
||||||
|
}
|
||||||
|
if _, err = tmpfile.Write([]byte(content)); err != nil {
|
||||||
|
t.Fatalf("Unable to create temporary file. %s", err)
|
||||||
|
}
|
||||||
|
if err = tmpfile.Close(); err != nil {
|
||||||
|
t.Fatalf("Unable to create temporary file. %s", err)
|
||||||
|
}
|
||||||
|
return tmpfile.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := createTempFile(
|
||||||
|
`app="virtuous-rat-minio"
|
||||||
|
chart="minio-0.1.3"
|
||||||
|
heritage="Tiller"
|
||||||
|
pod-template-hash="818089471"`)
|
||||||
|
|
||||||
|
defer os.Remove(filename)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
filename string
|
||||||
|
expectedResult string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"/tmp/non-existing-file", ""},
|
||||||
|
{filename, "minio-0.1.3"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
result := getHelmVersion(testCase.filename)
|
||||||
|
|
||||||
|
if testCase.expectedResult != result {
|
||||||
|
t.Fatalf("result: expected: %v, got: %v", testCase.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tests if the environment we are running is in docker.
|
// Tests if the environment we are running is in docker.
|
||||||
func TestIsDocker(t *testing.T) {
|
func TestIsDocker(t *testing.T) {
|
||||||
createTempFile := func(content string) string {
|
createTempFile := func(content string) string {
|
||||||
|
|||||||
+16
-7
@@ -497,7 +497,12 @@ func (web *webAPIHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract incoming metadata if any.
|
// Extract incoming metadata if any.
|
||||||
metadata := extractMetadataFromHeader(r.Header)
|
metadata, err := extractMetadataFromHeader(r.Header)
|
||||||
|
if err != nil {
|
||||||
|
errorIf(err, "found invalid http request header")
|
||||||
|
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Lock the object.
|
// Lock the object.
|
||||||
objectLock := globalNSMutex.NewNSLock(bucket, object)
|
objectLock := globalNSMutex.NewNSLock(bucket, object)
|
||||||
@@ -690,13 +695,15 @@ func getBucketAccessPolicy(objAPI ObjectLayer, bucketName string) (policy.Bucket
|
|||||||
policyInfo, err = layer.GetBucketPolicies(bucketName)
|
policyInfo, err = layer.GetBucketPolicies(bucketName)
|
||||||
case *azureObjects:
|
case *azureObjects:
|
||||||
policyInfo, err = layer.GetBucketPolicies(bucketName)
|
policyInfo, err = layer.GetBucketPolicies(bucketName)
|
||||||
|
case *gcsGateway:
|
||||||
|
policyInfo, err = layer.GetBucketPolicies(bucketName)
|
||||||
default:
|
default:
|
||||||
policyInfo, err = readBucketAccessPolicy(objAPI, bucketName)
|
policyInfo, err = readBucketAccessPolicy(objAPI, bucketName)
|
||||||
}
|
}
|
||||||
return policyInfo, err
|
return policyInfo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketPolicy - get bucket policy.
|
// GetBucketPolicy - get bucket policy for the requested prefix.
|
||||||
func (web *webAPIHandlers) GetBucketPolicy(r *http.Request, args *GetBucketPolicyArgs, reply *GetBucketPolicyRep) error {
|
func (web *webAPIHandlers) GetBucketPolicy(r *http.Request, args *GetBucketPolicyArgs, reply *GetBucketPolicyRep) error {
|
||||||
objectAPI := web.ObjectAPI()
|
objectAPI := web.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
@@ -707,9 +714,12 @@ func (web *webAPIHandlers) GetBucketPolicy(r *http.Request, args *GetBucketPolic
|
|||||||
return toJSONError(errAuthentication)
|
return toJSONError(errAuthentication)
|
||||||
}
|
}
|
||||||
|
|
||||||
policyInfo, err := readBucketAccessPolicy(objectAPI, args.BucketName)
|
var policyInfo, err = getBucketAccessPolicy(objectAPI, args.BucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return toJSONError(err, args.BucketName)
|
_, ok := errorCause(err).(PolicyNotFound)
|
||||||
|
if !ok {
|
||||||
|
return toJSONError(err, args.BucketName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reply.UIVersion = browser.UIVersion
|
reply.UIVersion = browser.UIVersion
|
||||||
@@ -745,8 +755,8 @@ func (web *webAPIHandlers) ListAllBucketPolicies(r *http.Request, args *ListAllB
|
|||||||
if !isHTTPRequestValid(r) {
|
if !isHTTPRequestValid(r) {
|
||||||
return toJSONError(errAuthentication)
|
return toJSONError(errAuthentication)
|
||||||
}
|
}
|
||||||
var policyInfo, err = getBucketAccessPolicy(objectAPI, args.BucketName)
|
|
||||||
|
|
||||||
|
var policyInfo, err = getBucketAccessPolicy(objectAPI, args.BucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, ok := errorCause(err).(PolicyNotFound)
|
_, ok := errorCause(err).(PolicyNotFound)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -791,7 +801,6 @@ func (web *webAPIHandlers) SetBucketPolicy(r *http.Request, args *SetBucketPolic
|
|||||||
}
|
}
|
||||||
|
|
||||||
var policyInfo, err = getBucketAccessPolicy(objectAPI, args.BucketName)
|
var policyInfo, err = getBucketAccessPolicy(objectAPI, args.BucketName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := errorCause(err).(PolicyNotFound); !ok {
|
if _, ok := errorCause(err).(PolicyNotFound); !ok {
|
||||||
return toJSONError(err, args.BucketName)
|
return toJSONError(err, args.BucketName)
|
||||||
@@ -915,7 +924,7 @@ func presignedGet(host, bucket, object string, expiry int64) string {
|
|||||||
signature := getSignature(signingKey, stringToSign)
|
signature := getSignature(signingKey, stringToSign)
|
||||||
|
|
||||||
// Construct the final presigned URL.
|
// Construct the final presigned URL.
|
||||||
return host + path + "?" + query + "&" + "X-Amz-Signature=" + signature
|
return host + getURLEncodedName(path) + "?" + query + "&" + "X-Amz-Signature=" + signature
|
||||||
}
|
}
|
||||||
|
|
||||||
// toJSONError converts regular errors into more user friendly
|
// toJSONError converts regular errors into more user friendly
|
||||||
|
|||||||
+3
-3
@@ -144,15 +144,15 @@ func (xl xlObjects) getBucketInfo(bucketName string) (bucketInfo BucketInfo, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketInfo - returns BucketInfo for a bucket.
|
// GetBucketInfo - returns BucketInfo for a bucket.
|
||||||
func (xl xlObjects) GetBucketInfo(bucket string) (BucketInfo, error) {
|
func (xl xlObjects) GetBucketInfo(bucket string) (bi BucketInfo, e error) {
|
||||||
// Verify if bucket is valid.
|
// Verify if bucket is valid.
|
||||||
if !IsValidBucketName(bucket) {
|
if !IsValidBucketName(bucket) {
|
||||||
return BucketInfo{}, BucketNameInvalid{Bucket: bucket}
|
return bi, BucketNameInvalid{Bucket: bucket}
|
||||||
}
|
}
|
||||||
|
|
||||||
bucketInfo, err := xl.getBucketInfo(bucket)
|
bucketInfo, err := xl.getBucketInfo(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return BucketInfo{}, toObjectErr(err, bucket)
|
return bi, toObjectErr(err, bucket)
|
||||||
}
|
}
|
||||||
return bucketInfo, nil
|
return bucketInfo, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user