mirror of
https://github.com/pgsty/minio.git
synced 2026-08-11 00:33:28 +03:00
Compare commits
332 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7881791a91 | |||
| d2f8f8c7ee | |||
| 8c32311b80 | |||
| 9bb88e610e | |||
| d1e41695fe | |||
| 2aeb3fbe86 | |||
| 99b843a64e | |||
| 4f31a9a33b | |||
| 65ddff8899 | |||
| e7c902bbbc | |||
| 5a5895203b | |||
| 7da0336ac8 | |||
| 3be616de3f | |||
| c5bf22fd90 | |||
| 7c9f934875 | |||
| b6f9b24b30 | |||
| 13cb814a0e | |||
| d264d2c899 | |||
| bebaff269c | |||
| 52b159b1db | |||
| 324834e4da | |||
| c2ed1347d9 | |||
| 50f6f9fe58 | |||
| 48cb0ea34b | |||
| 6f7c99a333 | |||
| 3498f5b0ec | |||
| 21d8c0fd13 | |||
| 79b9a9ce46 | |||
| b9b353db4b | |||
| 11a9b317a3 | |||
| 9af7d627ac | |||
| 76d9d54603 | |||
| 4c7c571875 | |||
| 313ba74b09 | |||
| 3e124315c8 | |||
| 78a0fd951e | |||
| 40852801ea | |||
| f6980c4630 | |||
| 8fcc787cba | |||
| 4e6d3c093f | |||
| 61145361fd | |||
| 950b4ad9af | |||
| 6add646130 | |||
| 20e61fb362 | |||
| 18ced1102c | |||
| d6af3c1237 | |||
| 5549a44566 | |||
| e7af31c2ff | |||
| e7971b1d55 | |||
| 26120d7838 | |||
| bef7c01c58 | |||
| 6a8ccc5925 | |||
| d85199e9de | |||
| 8608a84c23 | |||
| b7226f4c82 | |||
| f930ffe9e2 | |||
| b50a245208 | |||
| 45bb11e020 | |||
| b65cf281fd | |||
| f781548b0c | |||
| 25ee8e74f7 | |||
| c975d2cc7e | |||
| ea66528739 | |||
| e1164103d4 | |||
| 83fe70f710 | |||
| 12a6523fb2 | |||
| dba61867e8 | |||
| dd092f6c2b | |||
| 2a810c7da2 | |||
| dd8c2aa5c6 | |||
| 9e3fce441e | |||
| d4265f9a13 | |||
| 2fc024e880 | |||
| eddf468aef | |||
| b0d04b9a81 | |||
| a9de303d8b | |||
| 69bd6df464 | |||
| bfb505aa8e | |||
| d732b1ff9d | |||
| ef517bd0da | |||
| 32d837cf88 | |||
| 7b579caf68 | |||
| 272b8003d6 | |||
| 1c24c93f73 | |||
| 712abc7958 | |||
| 5f6d717b7a | |||
| 7e1661f4fa | |||
| f9779b24ad | |||
| f1f23f6f11 | |||
| cf26c937e4 | |||
| f19f957668 | |||
| d6572879a8 | |||
| b6ab8f50fa | |||
| c82acc599a | |||
| 2447bb58dd | |||
| a55a298e00 | |||
| 2929c1832d | |||
| aa2d8583ad | |||
| df2d75a2a3 | |||
| b24b320807 | |||
| c872c1f1dc | |||
| a40610d331 | |||
| 38978eb2aa | |||
| ca7c3a3278 | |||
| d58fc68137 | |||
| 71c66464c1 | |||
| bf414068a3 | |||
| 88959ce600 | |||
| 572719872d | |||
| bdea19b583 | |||
| eb1f9c9916 | |||
| d07fb41fe8 | |||
| a9cda850ca | |||
| bef0318c36 | |||
| 3f19ea98bb | |||
| c96073f985 | |||
| d2f240c791 | |||
| 6491dfbbd6 | |||
| d9cfa5fcd3 | |||
| 89b14639a9 | |||
| 3f744c0361 | |||
| 9ed7fb4916 | |||
| 4280e68de3 | |||
| f162d7bd97 | |||
| 36990aeafd | |||
| 9fe51e392b | |||
| 7e879a45d5 | |||
| 1c911c5f40 | |||
| bd8dc17b7a | |||
| 8491a29ec3 | |||
| 81d21850ec | |||
| c6ec3fdfba | |||
| 88c3dd49c6 | |||
| 6869f6d9dd | |||
| 3f643acb99 | |||
| ea73accefd | |||
| 555d54371c | |||
| bab4c90c45 | |||
| a2fc0b14d6 | |||
| bf66e9a529 | |||
| fde8c38638 | |||
| e6252dee5a | |||
| ecb042aa1c | |||
| e29009d347 | |||
| 9631d65552 | |||
| 7b7be66fa1 | |||
| b99aaab42e | |||
| 32bd1b31e9 | |||
| f287b15e71 | |||
| 586466584f | |||
| c0b4bf0a3e | |||
| acf46cc3b5 | |||
| 06ef8248c3 | |||
| 7c2ae4eaf7 | |||
| 989d7af9ac | |||
| 8a6c3aa3cd | |||
| 62b560510b | |||
| 0edfb32621 | |||
| 7e0f1eb8b5 | |||
| b43e8337b1 | |||
| 30040fba45 | |||
| 44cf9ac62f | |||
| 3b55357045 | |||
| 18d9a20ff6 | |||
| 6590aba6d2 | |||
| 2e81f27d27 | |||
| ae3c05aa37 | |||
| cef044178c | |||
| c998d1ac8c | |||
| 3457e504cf | |||
| 7f0cca075a | |||
| 088c595e01 | |||
| 26b4b466df | |||
| fdf691fdcc | |||
| 88c8c2d6cd | |||
| ef585037a0 | |||
| 362ebdcbab | |||
| b251454dd6 | |||
| 21c8693d9c | |||
| 1e7e5e297c | |||
| c7f180ffa9 | |||
| b8bd8d6a03 | |||
| baec331e84 | |||
| 557f382477 | |||
| 23b166b318 | |||
| 81a481e098 | |||
| 5b3090dffc | |||
| 3ef3fefd54 | |||
| 28e25eac78 | |||
| b0c9ae7490 | |||
| 143e7fe300 | |||
| f09e7ca764 | |||
| fae284d6b9 | |||
| 83d8e01c81 | |||
| 110458cd10 | |||
| e3eec89d24 | |||
| 54ae364def | |||
| 16a100b597 | |||
| cbc5d78a09 | |||
| d8a2975a68 | |||
| 66d911653f | |||
| ca6f795504 | |||
| 2af0f11731 | |||
| c3408f4f04 | |||
| b92c324254 | |||
| 81bee93b8d | |||
| 670f9788e3 | |||
| f187a16962 | |||
| e031f2b614 | |||
| c2b7b82ef4 | |||
| 02ad9d6072 | |||
| ea9408ccbb | |||
| 307765591d | |||
| 5d859b2178 | |||
| 223967fd32 | |||
| 274b35154c | |||
| c05ced08bb | |||
| c7722fbb1b | |||
| f163bed40d | |||
| b4772849f9 | |||
| 839a758a36 | |||
| aebfceeafb | |||
| 83d7ec09c1 | |||
| 8c29f69b00 | |||
| ce9d36d954 | |||
| 5c765bc63e | |||
| 6c7c6bec91 | |||
| ed703c065d | |||
| 387584356f | |||
| 1111419d4a | |||
| 20378821cf | |||
| ce1bfa6de8 | |||
| 6c26227081 | |||
| aa4e2b1542 | |||
| 1e5ac39ff3 | |||
| ec2295c3dc | |||
| 8cf7b88cc5 | |||
| 48bfebe442 | |||
| df60b3c733 | |||
| 584cb61bb8 | |||
| c1798cc89a | |||
| 3c8fabd116 | |||
| 36e51d0cee | |||
| 7d0645fb3a | |||
| 7c339e248a | |||
| b62ed5dc90 | |||
| f0641a0406 | |||
| 3d060f8b64 | |||
| 052a7b8eec | |||
| 9531cddb06 | |||
| 6fe9a613c0 | |||
| b729a4e83c | |||
| a0683d3c1f | |||
| 66fda7a37f | |||
| a63bc9254d | |||
| 14fa0097b0 | |||
| d99c397746 | |||
| e3777b1dd9 | |||
| 63c03758e6 | |||
| ce419c9835 | |||
| 0c2b708484 | |||
| 166e998788 | |||
| 267a0a3dfa | |||
| 985fd7d4e7 | |||
| 5479a6e33e | |||
| fb4186f6b9 | |||
| 7571582000 | |||
| 12b4971b70 | |||
| 5c0b98abf0 | |||
| 30d4a2cf53 | |||
| 92bc7caf7a | |||
| 19202bae81 | |||
| e7a4512a90 | |||
| ff822e64ca | |||
| 7cb87f863e | |||
| 67d8396af4 | |||
| 8b0cc376f4 | |||
| 9e5c4df106 | |||
| fd8749f42a | |||
| 5c13765168 | |||
| 3099af70a3 | |||
| fd1b8491db | |||
| 1961f2ef54 | |||
| 631c78e655 | |||
| e0f8b767ba | |||
| d0d015361c | |||
| 81b7e5c7a8 | |||
| 9e32cc283f | |||
| 1126410e62 | |||
| 2c46214291 | |||
| d13bd5b9b5 | |||
| c8c70a3750 | |||
| 882a1a1ccc | |||
| 8690d62146 | |||
| 85117d554f | |||
| 4487f70f08 | |||
| fb27388101 | |||
| 5e7ccc983d | |||
| 72fa2b4537 | |||
| 5399d91965 | |||
| 53a0bbeb5b | |||
| 384a862940 | |||
| 029f52880b | |||
| 5b05df215a | |||
| a13cd7b7c4 | |||
| d524924b80 | |||
| e6d740ce09 | |||
| cea4f82586 | |||
| 1d6ce115da | |||
| 06d2dfa31c | |||
| d547873b17 | |||
| 01721a840a | |||
| 52f6d5aafc | |||
| 2211a5f1b8 | |||
| 1ffa6adcd4 | |||
| add57a6938 | |||
| dafa5073cb | |||
| 65e05a06fb | |||
| 5c9354894b | |||
| b01e69e08f | |||
| 8601f29d95 | |||
| 0aee722e3f | |||
| beb6d40ce6 | |||
| 19db921555 | |||
| 68b9e9e7e7 | |||
| 2d84b02bc4 | |||
| 8b2801bd46 | |||
| 7d7e21aebb | |||
| bf14e5ce1b | |||
| d531080b7e | |||
| a6b8a5487a | |||
| 6c0d53a1c5 |
@@ -24,6 +24,10 @@
|
|||||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||||
|
|
||||||
|
## Regression
|
||||||
|
<!-- Is this issue a regression? (Yes / No) -->
|
||||||
|
<!-- If Yes, optionally please include minio version or commit id or PR# that caused this regression, if you have these details. -->
|
||||||
|
|
||||||
## Your Environment
|
## Your Environment
|
||||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||||
* Version used (`minio version`):
|
* Version used (`minio version`):
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
<!--- Why is this change required? What problem does it solve? -->
|
<!--- Why is this change required? What problem does it solve? -->
|
||||||
<!--- If it fixes an open issue, please link to the issue here. -->
|
<!--- If it fixes an open issue, please link to the issue here. -->
|
||||||
|
|
||||||
|
## Regression
|
||||||
|
<!-- Is this PR fixing a regression? (Yes / No) -->
|
||||||
|
<!-- If Yes, optionally please include minio version or commit id or PR# that caused this regression, if you have these details. -->
|
||||||
|
|
||||||
## How Has This Been Tested?
|
## How Has This Been Tested?
|
||||||
<!--- Please describe in detail how you tested your changes. -->
|
<!--- Please describe in detail how you tested your changes. -->
|
||||||
<!--- Include details of your testing environment, and the tests you ran to -->
|
<!--- Include details of your testing environment, and the tests you ran to -->
|
||||||
|
|||||||
+25
-9
@@ -1,30 +1,46 @@
|
|||||||
go_import_path: github.com/minio/minio
|
go_import_path: github.com/minio/minio
|
||||||
sudo: required
|
|
||||||
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
dist: trusty
|
|
||||||
|
|
||||||
language: go
|
language: go
|
||||||
|
|
||||||
|
# this ensures PRs based on a local branch are not built twice
|
||||||
|
# the downside is that a PR targeting a different branch is not built
|
||||||
|
# but as a workaround you can add the branch to this list
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: linux
|
- os: linux
|
||||||
|
dist: trusty
|
||||||
|
sudo: required
|
||||||
env:
|
env:
|
||||||
- ARCH=x86_64
|
- ARCH=x86_64
|
||||||
go: 1.10.1
|
go: 1.10.4
|
||||||
script:
|
script:
|
||||||
- make
|
- make
|
||||||
- diff -au <(gofmt -s -d cmd) <(printf "")
|
- diff -au <(gofmt -s -d cmd) <(printf "")
|
||||||
- diff -au <(gofmt -s -d pkg) <(printf "")
|
- diff -au <(gofmt -s -d pkg) <(printf "")
|
||||||
- make test GOFLAGS="-timeout 15m -race -v"
|
- make test GOFLAGS="-timeout 15m -race -v"
|
||||||
|
- make verify
|
||||||
- make coverage
|
- make coverage
|
||||||
- node --version
|
|
||||||
- cd browser && yarn && yarn test && cd ..
|
- cd browser && yarn && yarn test && cd ..
|
||||||
|
- os: windows
|
||||||
|
env:
|
||||||
|
- ARCH=x86_64
|
||||||
|
go: 1.10.4
|
||||||
|
script:
|
||||||
|
- go build --ldflags="$(go run buildscripts/gen-ldflags.go)" -o %GOPATH%\bin\minio.exe
|
||||||
|
- for d in $(go list ./... | grep -v browser); do go test -v -race --timeout 20m "$d"; done
|
||||||
|
- bash buildscripts/go-coverage.sh
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
# Add an IPv6 config - see the corresponding Travis issue
|
||||||
|
# https://github.com/travis-ci/travis-ci/issues/8361
|
||||||
|
- if [[ "${TRAVIS_OS_NAME}" == "linux" ]]; then sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; fi
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- nvm install stable
|
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then nvm install stable ; fi
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ WORKDIR /go/src/github.com/minio/
|
|||||||
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
apk add --no-cache ca-certificates curl && \
|
apk add --no-cache ca-certificates 'curl>7.61.0' && \
|
||||||
apk add --no-cache --virtual .build-deps git && \
|
apk add --no-cache --virtual .build-deps git && \
|
||||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||||
go get -v -d github.com/minio/minio && \
|
go get -v -d github.com/minio/minio && \
|
||||||
|
|||||||
+9
-16
@@ -1,27 +1,20 @@
|
|||||||
FROM golang:1.10.1-alpine3.7
|
FROM alpine:3.7
|
||||||
|
|
||||||
LABEL maintainer="Minio Inc <dev@minio.io>"
|
LABEL maintainer="Minio Inc <dev@minio.io>"
|
||||||
|
|
||||||
ENV GOPATH /go
|
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||||
ENV PATH $PATH:$GOPATH/bin
|
COPY minio /usr/bin/
|
||||||
ENV CGO_ENABLED 0
|
|
||||||
ENV MINIO_UPDATE off
|
ENV MINIO_UPDATE off
|
||||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||||
MINIO_SECRET_KEY_FILE=secret_key
|
MINIO_SECRET_KEY_FILE=secret_key
|
||||||
|
|
||||||
WORKDIR /go/src/github.com/minio/
|
RUN \
|
||||||
|
apk add --no-cache ca-certificates 'curl>7.61.0' && \
|
||||||
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
|
||||||
|
|
||||||
COPY . /go/src/github.com/minio/minio
|
|
||||||
|
|
||||||
RUN \
|
|
||||||
apk add --no-cache ca-certificates curl && \
|
|
||||||
apk add --no-cache --virtual .build-deps git && \
|
|
||||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||||
cd /go/src/github.com/minio/minio && \
|
chmod +x /usr/bin/minio && \
|
||||||
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
|
chmod +x /usr/bin/docker-entrypoint.sh && \
|
||||||
rm -rf /go/pkg /go/src /usr/local/go && apk del .build-deps
|
chmod +x /usr/bin/healthcheck.sh
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -9,8 +9,7 @@ ENV MINIO_ACCESS_KEY_FILE=access_key \
|
|||||||
MINIO_SECRET_KEY_FILE=secret_key
|
MINIO_SECRET_KEY_FILE=secret_key
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
apk add --no-cache ca-certificates && \
|
apk add --no-cache ca-certificates 'curl>7.61.0' && \
|
||||||
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 && \
|
chmod +x /usr/bin/minio && \
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ checks:
|
|||||||
@(env bash $(PWD)/buildscripts/checkgopath.sh)
|
@(env bash $(PWD)/buildscripts/checkgopath.sh)
|
||||||
|
|
||||||
getdeps:
|
getdeps:
|
||||||
@echo "Installing golint" && go get -u github.com/golang/lint/golint
|
@echo "Installing golint" && go get -u golang.org/x/lint/golint
|
||||||
@echo "Installing gocyclo" && go get -u github.com/fzipp/gocyclo
|
@echo "Installing gocyclo" && go get -u github.com/fzipp/gocyclo
|
||||||
@echo "Installing deadcode" && go get -u github.com/remyoudompheng/go-misc/deadcode
|
@echo "Installing deadcode" && go get -u github.com/remyoudompheng/go-misc/deadcode
|
||||||
@echo "Installing misspell" && go get -u github.com/client9/misspell/cmd/misspell
|
@echo "Installing misspell" && go get -u github.com/client9/misspell/cmd/misspell
|
||||||
@@ -23,8 +23,8 @@ verifiers: getdeps vet fmt lint cyclo deadcode spelling
|
|||||||
|
|
||||||
vet:
|
vet:
|
||||||
@echo "Running $@"
|
@echo "Running $@"
|
||||||
@go tool vet -atomic -bool -copylocks -nilfunc -printf -shadow -rangeloops -unreachable -unsafeptr -unusedresult cmd
|
@go tool vet cmd
|
||||||
@go tool vet -atomic -bool -copylocks -nilfunc -printf -shadow -rangeloops -unreachable -unsafeptr -unusedresult pkg
|
@go tool vet pkg
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
@echo "Running $@"
|
@echo "Running $@"
|
||||||
@@ -61,6 +61,8 @@ check: test
|
|||||||
test: verifiers build
|
test: verifiers build
|
||||||
@echo "Running unit tests"
|
@echo "Running unit tests"
|
||||||
@go test $(GOFLAGS) -tags kqueue ./...
|
@go test $(GOFLAGS) -tags kqueue ./...
|
||||||
|
|
||||||
|
verify: build
|
||||||
@echo "Verifying build"
|
@echo "Verifying build"
|
||||||
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
||||||
|
|
||||||
@@ -73,6 +75,9 @@ build: checks
|
|||||||
@echo "Building minio binary to './minio'"
|
@echo "Building minio binary to './minio'"
|
||||||
@CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
|
@CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
|
||||||
|
|
||||||
|
docker: build
|
||||||
|
@docker build -t $(TAG) . -f Dockerfile.dev
|
||||||
|
|
||||||
pkg-add:
|
pkg-add:
|
||||||
@echo "Adding new package $(PKG)"
|
@echo "Adding new package $(PKG)"
|
||||||
@${GOPATH}/bin/govendor add $(PKG)
|
@${GOPATH}/bin/govendor add $(PKG)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ docker run -p 9000:9000 minio/minio server /data
|
|||||||
docker pull minio/minio:edge
|
docker pull minio/minio:edge
|
||||||
docker run -p 9000:9000 minio/minio:edge server /data
|
docker run -p 9000:9000 minio/minio:edge server /data
|
||||||
```
|
```
|
||||||
Please visit Minio Docker quickstart guide for more [here](https://docs.minio.io/docs/minio-docker-quickstart-guide)
|
Note: Docker will not display the autogenerated keys unless you start the container with the `-it`(interactive TTY) argument. Generally, it is not recommended to use autogenerated keys with containers. Please visit Minio Docker quickstart guide for more information [here](https://docs.minio.io/docs/minio-docker-quickstart-guide)
|
||||||
|
|
||||||
## macOS
|
## macOS
|
||||||
### Homebrew
|
### Homebrew
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
# version format
|
|
||||||
version: "{build}"
|
|
||||||
|
|
||||||
# Operating system (build VM template)
|
|
||||||
os: Windows Server 2012 R2
|
|
||||||
|
|
||||||
# Platform.
|
|
||||||
platform: x64
|
|
||||||
|
|
||||||
clone_folder: c:\gopath\src\github.com\minio\minio
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
environment:
|
|
||||||
GOPATH: c:\gopath
|
|
||||||
GOROOT: c:\go
|
|
||||||
|
|
||||||
# scripts that run after cloning repository
|
|
||||||
install:
|
|
||||||
- set PATH=%GOPATH%\bin;%GOROOT%\bin;%PATH%
|
|
||||||
- go version
|
|
||||||
- go env
|
|
||||||
- python --version
|
|
||||||
|
|
||||||
# To run your custom scripts instead of automatic MSBuild
|
|
||||||
build_script:
|
|
||||||
# Compile
|
|
||||||
# We need to disable firewall - https://github.com/appveyor/ci/issues/1579#issuecomment-309830648
|
|
||||||
- ps: Disable-NetFirewallRule -DisplayName 'File and Printer Sharing (SMB-Out)'
|
|
||||||
- appveyor AddCompilationMessage "Starting Compile"
|
|
||||||
- cd c:\gopath\src\github.com\minio\minio
|
|
||||||
- go run buildscripts/gen-ldflags.go > temp.txt
|
|
||||||
- set /p BUILD_LDFLAGS=<temp.txt
|
|
||||||
- go build -ldflags="%BUILD_LDFLAGS%" -o %GOPATH%\bin\minio.exe
|
|
||||||
- appveyor AddCompilationMessage "Compile Success"
|
|
||||||
|
|
||||||
# To run your custom scripts instead of automatic tests
|
|
||||||
test_script:
|
|
||||||
# Unit tests
|
|
||||||
- ps: Add-AppveyorTest "Unit Tests" -Outcome Running
|
|
||||||
- mkdir build\coverage
|
|
||||||
- for /f "" %%G in ('go list github.com/minio/minio/... ^| find /i /v "browser/"') do ( go test -v -timeout 20m -race %%G )
|
|
||||||
- go test -v -timeout 20m -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd
|
|
||||||
- ps: Update-AppveyorTest "Unit Tests" -Outcome Passed
|
|
||||||
|
|
||||||
after_test:
|
|
||||||
- go tool cover -html=build\coverage\coverage.txt -o build\coverage\coverage.html
|
|
||||||
- ps: Push-AppveyorArtifact build\coverage\coverage.txt
|
|
||||||
- ps: Push-AppveyorArtifact build\coverage\coverage.html
|
|
||||||
# Upload coverage report.
|
|
||||||
- "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%"
|
|
||||||
- pip install codecov
|
|
||||||
- codecov -X gcov -f "build\coverage\coverage.txt"
|
|
||||||
|
|
||||||
# to disable deployment
|
|
||||||
deploy: off
|
|
||||||
@@ -22,7 +22,7 @@ export let alertId = 0
|
|||||||
export const set = alert => {
|
export const set = alert => {
|
||||||
const id = alertId++
|
const id = alertId++
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (alert.type !== "danger") {
|
if (alert.type !== "danger" || alert.autoClear) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: CLEAR,
|
type: CLEAR,
|
||||||
|
|||||||
@@ -67,8 +67,12 @@ const mapDispatchToProps = dispatch => {
|
|||||||
return {
|
return {
|
||||||
fetchBuckets: () => dispatch(actionsBuckets.fetchBuckets()),
|
fetchBuckets: () => dispatch(actionsBuckets.fetchBuckets()),
|
||||||
setBucketList: buckets => dispatch(actionsBuckets.setList(buckets)),
|
setBucketList: buckets => dispatch(actionsBuckets.setList(buckets)),
|
||||||
selectBucket: bucket => dispatch(actionsBuckets.selectBucket(bucket))
|
selectBucket: (bucket, prefix) =>
|
||||||
|
dispatch(actionsBuckets.selectBucket(bucket, prefix))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(BucketList)
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(BucketList)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { READ_ONLY, WRITE_ONLY, READ_WRITE } from '../constants'
|
import { READ_ONLY, WRITE_ONLY, READ_WRITE, NONE } from '../constants'
|
||||||
|
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { connect } from "react-redux"
|
import { connect } from "react-redux"
|
||||||
@@ -42,37 +42,40 @@ export class Policy extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const {policy, prefix} = this.props
|
const {policy, prefix} = this.props
|
||||||
let newPrefix = prefix
|
let newPrefix = prefix
|
||||||
|
|
||||||
if (newPrefix === '')
|
if (newPrefix === '')
|
||||||
newPrefix = '*'
|
newPrefix = '*'
|
||||||
|
|
||||||
return (
|
if (policy === NONE) {
|
||||||
<div className="pmb-list">
|
return <noscript />
|
||||||
<div className="pmbl-item">
|
} else {
|
||||||
{ newPrefix }
|
return (
|
||||||
|
<div className="pmb-list">
|
||||||
|
<div className="pmbl-item">
|
||||||
|
{ newPrefix }
|
||||||
|
</div>
|
||||||
|
<div className="pmbl-item">
|
||||||
|
<select className="form-control"
|
||||||
|
disabled
|
||||||
|
value={ policy }>
|
||||||
|
<option value={ READ_ONLY }>
|
||||||
|
Read Only
|
||||||
|
</option>
|
||||||
|
<option value={ WRITE_ONLY }>
|
||||||
|
Write Only
|
||||||
|
</option>
|
||||||
|
<option value={ READ_WRITE }>
|
||||||
|
Read and Write
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="pmbl-item">
|
||||||
|
<button className="btn btn-block btn-danger" onClick={ this.removePolicy.bind(this) }>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pmbl-item">
|
)
|
||||||
<select className="form-control"
|
}
|
||||||
disabled
|
|
||||||
value={ policy }>
|
|
||||||
<option value={ READ_ONLY }>
|
|
||||||
Read Only
|
|
||||||
</option>
|
|
||||||
<option value={ WRITE_ONLY }>
|
|
||||||
Write Only
|
|
||||||
</option>
|
|
||||||
<option value={ READ_WRITE }>
|
|
||||||
Read and Write
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="pmbl-item">
|
|
||||||
<button className="btn btn-block btn-danger" onClick={ this.removePolicy.bind(this) }>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { shallow, mount } from "enzyme"
|
import { shallow, mount } from "enzyme"
|
||||||
import { Policy } from "../Policy"
|
import { Policy } from "../Policy"
|
||||||
import { READ_ONLY, WRITE_ONLY, READ_WRITE } from "../../constants"
|
import { READ_ONLY, WRITE_ONLY, READ_WRITE, NONE } from "../../constants"
|
||||||
import web from "../../web"
|
import web from "../../web"
|
||||||
|
|
||||||
jest.mock("../../web", () => ({
|
jest.mock("../../web", () => ({
|
||||||
@@ -31,6 +31,11 @@ describe("Policy", () => {
|
|||||||
shallow(<Policy currentBucket={"bucket"} prefix={"foo"} policy={READ_ONLY} />)
|
shallow(<Policy currentBucket={"bucket"} prefix={"foo"} policy={READ_ONLY} />)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should not render when policy is listed as 'none'", () => {
|
||||||
|
const wrapper = shallow(<Policy currentBucket={"bucket"} prefix={"foo"} policy={NONE} />)
|
||||||
|
expect(wrapper.find(".pmb-list").length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
it("should call web.setBucketPolicy and fetchPolicies on submit", () => {
|
it("should call web.setBucketPolicy and fetchPolicies on submit", () => {
|
||||||
const fetchPolicies = jest.fn()
|
const fetchPolicies = jest.fn()
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const minioBrowserPrefix = p.slice(0, p.indexOf("/", 1))
|
|||||||
export const READ_ONLY = "readonly"
|
export const READ_ONLY = "readonly"
|
||||||
export const WRITE_ONLY = "writeonly"
|
export const WRITE_ONLY = "writeonly"
|
||||||
export const READ_WRITE = "readwrite"
|
export const READ_WRITE = "readwrite"
|
||||||
|
export const NONE = "none"
|
||||||
|
|
||||||
export const SHARE_OBJECT_EXPIRY_DAYS = 5
|
export const SHARE_OBJECT_EXPIRY_DAYS = 5
|
||||||
export const SHARE_OBJECT_EXPIRY_HOURS = 0
|
export const SHARE_OBJECT_EXPIRY_HOURS = 0
|
||||||
|
|||||||
@@ -19,16 +19,29 @@ import thunk from "redux-thunk"
|
|||||||
import * as actionsObjects from "../actions"
|
import * as actionsObjects from "../actions"
|
||||||
import * as alertActions from "../../alert/actions"
|
import * as alertActions from "../../alert/actions"
|
||||||
import { minioBrowserPrefix } from "../../constants"
|
import { minioBrowserPrefix } from "../../constants"
|
||||||
|
import history from "../../history"
|
||||||
|
|
||||||
jest.mock("../../web", () => ({
|
jest.mock("../../web", () => ({
|
||||||
LoggedIn: jest.fn(() => true).mockReturnValueOnce(false),
|
LoggedIn: jest
|
||||||
ListObjects: jest.fn(() => {
|
.fn(() => true)
|
||||||
return Promise.resolve({
|
.mockReturnValueOnce(true)
|
||||||
objects: [{ name: "test1" }, { name: "test2" }],
|
.mockReturnValueOnce(false)
|
||||||
istruncated: false,
|
.mockReturnValueOnce(true)
|
||||||
nextmarker: "test2",
|
.mockReturnValueOnce(true)
|
||||||
writable: false
|
.mockReturnValueOnce(false),
|
||||||
})
|
ListObjects: jest.fn(({ bucketName }) => {
|
||||||
|
if (bucketName === "test-deny") {
|
||||||
|
return Promise.reject({
|
||||||
|
message: "listobjects is denied"
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({
|
||||||
|
objects: [{ name: "test1" }, { name: "test2" }],
|
||||||
|
istruncated: false,
|
||||||
|
nextmarker: "test2",
|
||||||
|
writable: false
|
||||||
|
})
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
RemoveObject: jest.fn(({ bucketName, objects }) => {
|
RemoveObject: jest.fn(({ bucketName, objects }) => {
|
||||||
if (!bucketName) {
|
if (!bucketName) {
|
||||||
@@ -160,6 +173,41 @@ describe("Objects actions", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("creates objects/RESET_LIST after failing to fetch the objects from bucket with ListObjects denied for LoggedIn users", () => {
|
||||||
|
const store = mockStore({
|
||||||
|
buckets: { currentBucket: "test-deny" },
|
||||||
|
objects: { currentPrefix: "" }
|
||||||
|
})
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
type: "alert/SET",
|
||||||
|
alert: {
|
||||||
|
type: "danger",
|
||||||
|
message: "listobjects is denied",
|
||||||
|
id: alertActions.alertId,
|
||||||
|
autoClear: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "object/RESET_LIST"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return store.dispatch(actionsObjects.fetchObjects()).then(() => {
|
||||||
|
const actions = store.getActions()
|
||||||
|
expect(actions).toEqual(expectedActions)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("redirect to login after failing to fetch the objects from bucket for non-LoggedIn users", () => {
|
||||||
|
const store = mockStore({
|
||||||
|
buckets: { currentBucket: "test-deny" },
|
||||||
|
objects: { currentPrefix: "" }
|
||||||
|
})
|
||||||
|
return store.dispatch(actionsObjects.fetchObjects()).then(() => {
|
||||||
|
expect(history.location.pathname.endsWith("/login")).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("creates objects/SET_SORT_BY and objects/SET_SORT_ORDER when sortObjects is called", () => {
|
it("creates objects/SET_SORT_BY and objects/SET_SORT_ORDER when sortObjects is called", () => {
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
objects: {
|
objects: {
|
||||||
@@ -244,7 +292,11 @@ describe("Objects actions", () => {
|
|||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{
|
{
|
||||||
type: "alert/SET",
|
type: "alert/SET",
|
||||||
alert: { type: "danger", message: "Invalid bucket", id: 0 }
|
alert: {
|
||||||
|
type: "danger",
|
||||||
|
message: "Invalid bucket",
|
||||||
|
id: alertActions.alertId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
return store.dispatch(actionsObjects.deleteObject("obj1")).then(() => {
|
return store.dispatch(actionsObjects.deleteObject("obj1")).then(() => {
|
||||||
@@ -355,7 +407,7 @@ describe("Objects actions", () => {
|
|||||||
store.dispatch(actionsObjects.downloadObject("obj1"))
|
store.dispatch(actionsObjects.downloadObject("obj1"))
|
||||||
const url = `${
|
const url = `${
|
||||||
window.location.origin
|
window.location.origin
|
||||||
}${minioBrowserPrefix}/download/bk1/${encodeURI("pre1/obj1")}?token=''`
|
}${minioBrowserPrefix}/download/bk1/${encodeURI("pre1/obj1")}?token=`
|
||||||
expect(setLocation).toHaveBeenCalledWith(url)
|
expect(setLocation).toHaveBeenCalledWith(url)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -16,17 +16,15 @@
|
|||||||
|
|
||||||
import web from "../web"
|
import web from "../web"
|
||||||
import history from "../history"
|
import history from "../history"
|
||||||
import {
|
import { sortObjectsByName, sortObjectsBySize, sortObjectsByDate } from "../utils"
|
||||||
sortObjectsByName,
|
|
||||||
sortObjectsBySize,
|
|
||||||
sortObjectsByDate
|
|
||||||
} from "../utils"
|
|
||||||
import { getCurrentBucket } from "../buckets/selectors"
|
import { getCurrentBucket } from "../buckets/selectors"
|
||||||
import { getCurrentPrefix, getCheckedList } from "./selectors"
|
import { getCurrentPrefix, getCheckedList } from "./selectors"
|
||||||
import * as alertActions from "../alert/actions"
|
import * as alertActions from "../alert/actions"
|
||||||
|
import * as bucketActions from "../buckets/actions"
|
||||||
import { minioBrowserPrefix } from "../constants"
|
import { minioBrowserPrefix } from "../constants"
|
||||||
|
|
||||||
export const SET_LIST = "objects/SET_LIST"
|
export const SET_LIST = "objects/SET_LIST"
|
||||||
|
export const RESET_LIST = "object/RESET_LIST"
|
||||||
export const APPEND_LIST = "objects/APPEND_LIST"
|
export const APPEND_LIST = "objects/APPEND_LIST"
|
||||||
export const REMOVE = "objects/REMOVE"
|
export const REMOVE = "objects/REMOVE"
|
||||||
export const SET_SORT_BY = "objects/SET_SORT_BY"
|
export const SET_SORT_BY = "objects/SET_SORT_BY"
|
||||||
@@ -45,6 +43,10 @@ export const setList = (objects, marker, isTruncated) => ({
|
|||||||
isTruncated
|
isTruncated
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const resetList = () => ({
|
||||||
|
type: RESET_LIST
|
||||||
|
})
|
||||||
|
|
||||||
export const appendList = (objects, marker, isTruncated) => ({
|
export const appendList = (objects, marker, isTruncated) => ({
|
||||||
type: APPEND_LIST,
|
type: APPEND_LIST,
|
||||||
objects,
|
objects,
|
||||||
@@ -54,47 +56,54 @@ export const appendList = (objects, marker, isTruncated) => ({
|
|||||||
|
|
||||||
export const fetchObjects = append => {
|
export const fetchObjects = append => {
|
||||||
return function(dispatch, getState) {
|
return function(dispatch, getState) {
|
||||||
const {
|
const {buckets: {currentBucket}, objects: {currentPrefix, marker}} = getState()
|
||||||
buckets: { currentBucket },
|
|
||||||
objects: { currentPrefix, marker }
|
|
||||||
} = getState()
|
|
||||||
if (currentBucket) {
|
if (currentBucket) {
|
||||||
return web
|
return web
|
||||||
.ListObjects({
|
.ListObjects({
|
||||||
bucketName: currentBucket,
|
bucketName: currentBucket,
|
||||||
prefix: currentPrefix,
|
prefix: currentPrefix,
|
||||||
marker: append ? marker : ""
|
marker: append ? marker : ""
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
let objects = []
|
let objects = []
|
||||||
if (res.objects) {
|
if (res.objects) {
|
||||||
objects = res.objects.map(object => {
|
objects = res.objects.map(object => {
|
||||||
return {
|
return {
|
||||||
...object,
|
...object,
|
||||||
name: object.name.replace(currentPrefix, "")
|
name: object.name.replace(currentPrefix, "")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (append) {
|
if (append) {
|
||||||
dispatch(appendList(objects, res.nextmarker, res.istruncated))
|
dispatch(appendList(objects, res.nextmarker, res.istruncated))
|
||||||
} else {
|
} else {
|
||||||
dispatch(setList(objects, res.nextmarker, res.istruncated))
|
dispatch(setList(objects, res.nextmarker, res.istruncated))
|
||||||
dispatch(setSortBy(""))
|
dispatch(setSortBy(""))
|
||||||
dispatch(setSortOrder(false))
|
dispatch(setSortOrder(false))
|
||||||
}
|
}
|
||||||
dispatch(setPrefixWritable(res.writable))
|
dispatch(setPrefixWritable(res.writable))
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
dispatch(alertActions.set({ type: "danger", message: err.message }))
|
if (web.LoggedIn()) {
|
||||||
history.push("/login")
|
dispatch(
|
||||||
})
|
alertActions.set({
|
||||||
}
|
type: "danger",
|
||||||
|
message: err.message,
|
||||||
|
autoClear: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
dispatch(resetList())
|
||||||
|
} else {
|
||||||
|
history.push("/login")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sortObjects = sortBy => {
|
export const sortObjects = sortBy => {
|
||||||
return function(dispatch, getState) {
|
return function(dispatch, getState) {
|
||||||
const { objects } = getState()
|
const {objects} = getState()
|
||||||
const sortOrder = objects.sortBy == sortBy ? !objects.sortOrder : true
|
const sortOrder = objects.sortBy == sortBy ? !objects.sortOrder : true
|
||||||
dispatch(setSortBy(sortBy))
|
dispatch(setSortBy(sortBy))
|
||||||
dispatch(setSortOrder(sortOrder))
|
dispatch(setSortOrder(sortOrder))
|
||||||
@@ -194,30 +203,39 @@ export const shareObject = (object, days, hours, minutes) => {
|
|||||||
const currentPrefix = getCurrentPrefix(getState())
|
const currentPrefix = getCurrentPrefix(getState())
|
||||||
const objectName = `${currentPrefix}${object}`
|
const objectName = `${currentPrefix}${object}`
|
||||||
const expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60
|
const expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60
|
||||||
return web
|
if (web.LoggedIn()) {
|
||||||
.PresignedGet({
|
return web
|
||||||
host: location.host,
|
.PresignedGet({
|
||||||
bucket: currentBucket,
|
host: location.host,
|
||||||
object: objectName,
|
bucket: currentBucket,
|
||||||
expiry
|
object: objectName
|
||||||
})
|
})
|
||||||
.then(obj => {
|
.then(obj => {
|
||||||
dispatch(showShareObject(object, obj.url))
|
dispatch(showShareObject(object, obj.url))
|
||||||
dispatch(
|
dispatch(
|
||||||
alertActions.set({
|
alertActions.set({
|
||||||
type: "success",
|
type: "success",
|
||||||
message: `Object shared. Expires in ${days} days ${hours} hours ${minutes} minutes`
|
message: `Object shared. Expires in ${days} days ${hours} hours ${minutes} minutes`
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
dispatch(
|
dispatch(
|
||||||
alertActions.set({
|
alertActions.set({
|
||||||
type: "danger",
|
type: "danger",
|
||||||
message: err.message
|
message: err.message
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
dispatch(showShareObject(object, `${location.host}` + '/' + `${currentBucket}` + '/' + encodeURI(objectName)))
|
||||||
|
dispatch(
|
||||||
|
alertActions.set({
|
||||||
|
type: "success",
|
||||||
|
message: `Object shared.`
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +281,7 @@ export const downloadObject = object => {
|
|||||||
} else {
|
} else {
|
||||||
const url = `${
|
const url = `${
|
||||||
window.location.origin
|
window.location.origin
|
||||||
}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=''`
|
}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=`
|
||||||
window.location = url
|
window.location = url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,7 +310,7 @@ export const downloadCheckedObjects = () => {
|
|||||||
objects: getCheckedList(state)
|
objects: getCheckedList(state)
|
||||||
}
|
}
|
||||||
if (!web.LoggedIn()) {
|
if (!web.LoggedIn()) {
|
||||||
const requestUrl = location.origin + "/minio/zip?token=''"
|
const requestUrl = location.origin + "/minio/zip?token="
|
||||||
downloadZip(requestUrl, req, dispatch)
|
downloadZip(requestUrl, req, dispatch)
|
||||||
} else {
|
} else {
|
||||||
return web
|
return web
|
||||||
@@ -303,14 +321,13 @@ export const downloadCheckedObjects = () => {
|
|||||||
}${minioBrowserPrefix}/zip?token=${res.token}`
|
}${minioBrowserPrefix}/zip?token=${res.token}`
|
||||||
downloadZip(requestUrl, req, dispatch)
|
downloadZip(requestUrl, req, dispatch)
|
||||||
})
|
})
|
||||||
.catch(err =>
|
.catch(err => dispatch(
|
||||||
dispatch(
|
alertActions.set({
|
||||||
alertActions.set({
|
type: "danger",
|
||||||
type: "danger",
|
message: err.message
|
||||||
message: err.message
|
})
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,8 +350,7 @@ const downloadZip = (url, req, dispatch) => {
|
|||||||
var separator = req.prefix.length > 1 ? "-" : ""
|
var separator = req.prefix.length > 1 ? "-" : ""
|
||||||
|
|
||||||
anchor.href = blobUrl
|
anchor.href = blobUrl
|
||||||
anchor.download =
|
anchor.download = req.bucketName + separator + req.prefix.slice(0, -1) + ".zip"
|
||||||
req.bucketName + separator + req.prefix.slice(0, -1) + ".zip"
|
|
||||||
|
|
||||||
anchor.click()
|
anchor.click()
|
||||||
window.URL.revokeObjectURL(blobUrl)
|
window.URL.revokeObjectURL(blobUrl)
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ export default (
|
|||||||
marker: action.marker,
|
marker: action.marker,
|
||||||
isTruncated: action.isTruncated
|
isTruncated: action.isTruncated
|
||||||
}
|
}
|
||||||
|
case actionsObjects.RESET_LIST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
list: [],
|
||||||
|
marker: "",
|
||||||
|
isTruncated: false
|
||||||
|
}
|
||||||
case actionsObjects.APPEND_LIST:
|
case actionsObjects.APPEND_LIST:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -109,7 +109,15 @@ div.fesl-row {
|
|||||||
/*--------------------------
|
/*--------------------------
|
||||||
Icons
|
Icons
|
||||||
----------------------------*/
|
----------------------------*/
|
||||||
&[data-type=folder] { .list-type(#a1d6dd, '\f114'); }
|
&[data-type=folder] {
|
||||||
|
.list-type(#a1d6dd, '\f114');
|
||||||
|
|
||||||
|
.fesl-item-name {
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
&[data-type=pdf] {.list-type(#fa7775, '\f1c1'); }
|
&[data-type=pdf] {.list-type(#fa7775, '\f1c1'); }
|
||||||
&[data-type=zip] { .list-type(#427089, '\f1c6'); }
|
&[data-type=zip] { .list-type(#427089, '\f1c6'); }
|
||||||
&[data-type=audio] { .list-type(#009688, '\f1c7'); }
|
&[data-type=audio] { .list-type(#009688, '\f1c7'); }
|
||||||
|
|||||||
+26
-37
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@ set -e
|
|||||||
echo "" > coverage.txt
|
echo "" > coverage.txt
|
||||||
|
|
||||||
for d in $(go list ./... | grep -v browser); do
|
for d in $(go list ./... | grep -v browser); do
|
||||||
go test -coverprofile=profile.out -covermode=atomic "$d"
|
go test -v -coverprofile=profile.out -covermode=atomic "$d"
|
||||||
if [ -f profile.out ]; then
|
if [ -f profile.out ]; then
|
||||||
cat profile.out >> coverage.txt
|
cat profile.out >> coverage.txt
|
||||||
rm profile.out
|
rm profile.out
|
||||||
|
|||||||
@@ -68,6 +68,36 @@ function start_minio_erasure_sets()
|
|||||||
echo "$minio_pid"
|
echo "$minio_pid"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function start_minio_dist_erasure_sets_ipv6()
|
||||||
|
{
|
||||||
|
declare -a minio_pids
|
||||||
|
export MINIO_ACCESS_KEY=$ACCESS_KEY
|
||||||
|
export MINIO_SECRET_KEY=$SECRET_KEY
|
||||||
|
"${MINIO[@]}" server --address="[::1]:9000" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9000.log" 2>&1 &
|
||||||
|
minio_pids[0]=$!
|
||||||
|
"${MINIO[@]}" server --address="[::1]:9001" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9001.log" 2>&1 &
|
||||||
|
minio_pids[1]=$!
|
||||||
|
"${MINIO[@]}" server --address="[::1]:9002" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9002.log" 2>&1 &
|
||||||
|
minio_pids[2]=$!
|
||||||
|
"${MINIO[@]}" server --address="[::1]:9003" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9003.log" 2>&1 &
|
||||||
|
minio_pids[3]=$!
|
||||||
|
"${MINIO[@]}" server --address="[::1]:9004" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9004.log" 2>&1 &
|
||||||
|
minio_pids[4]=$!
|
||||||
|
"${MINIO[@]}" server --address="[::1]:9005" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9005.log" 2>&1 &
|
||||||
|
minio_pids[5]=$!
|
||||||
|
"${MINIO[@]}" server --address="[::1]:9006" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9006.log" 2>&1 &
|
||||||
|
minio_pids[6]=$!
|
||||||
|
"${MINIO[@]}" server --address="[::1]:9007" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9007.log" 2>&1 &
|
||||||
|
minio_pids[7]=$!
|
||||||
|
"${MINIO[@]}" server --address="[::1]:9008" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9008.log" 2>&1 &
|
||||||
|
minio_pids[8]=$!
|
||||||
|
"${MINIO[@]}" server --address="[::1]:9009" "http://[::1]:9000${WORK_DIR}/dist-disk-sets1" "http://[::1]:9001${WORK_DIR}/dist-disk-sets2" "http://[::1]:9002${WORK_DIR}/dist-disk-sets3" "http://[::1]:9003${WORK_DIR}/dist-disk-sets4" "http://[::1]:9004${WORK_DIR}/dist-disk-sets5" "http://[::1]:9005${WORK_DIR}/dist-disk-sets6" "http://[::1]:9006${WORK_DIR}/dist-disk-sets7" "http://[::1]:9007${WORK_DIR}/dist-disk-sets8" "http://[::1]:9008${WORK_DIR}/dist-disk-sets9" "http://[::1]:9009${WORK_DIR}/dist-disk-sets10" "http://[::1]:9000${WORK_DIR}/dist-disk-sets11" "http://[::1]:9001${WORK_DIR}/dist-disk-sets12" "http://[::1]:9002${WORK_DIR}/dist-disk-sets13" "http://[::1]:9003${WORK_DIR}/dist-disk-sets14" "http://[::1]:9004${WORK_DIR}/dist-disk-sets15" "http://[::1]:9005${WORK_DIR}/dist-disk-sets16" "http://[::1]:9006${WORK_DIR}/dist-disk-sets17" "http://[::1]:9007${WORK_DIR}/dist-disk-sets18" "http://[::1]:9008${WORK_DIR}/dist-disk-sets19" "http://[::1]:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-v6-9009.log" 2>&1 &
|
||||||
|
minio_pids[9]=$!
|
||||||
|
|
||||||
|
sleep 35
|
||||||
|
echo "${minio_pids[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
function start_minio_dist_erasure_sets()
|
function start_minio_dist_erasure_sets()
|
||||||
{
|
{
|
||||||
declare -a minio_pids
|
declare -a minio_pids
|
||||||
@@ -161,6 +191,34 @@ function run_test_erasure_sets() {
|
|||||||
return "$rv"
|
return "$rv"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function run_test_dist_erasure_sets_ipv6()
|
||||||
|
{
|
||||||
|
minio_pids=( $(start_minio_dist_erasure_sets_ipv6) )
|
||||||
|
|
||||||
|
export SERVER_ENDPOINT="[::1]:9000"
|
||||||
|
|
||||||
|
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||||
|
rv=$?
|
||||||
|
|
||||||
|
for pid in "${minio_pids[@]}"; do
|
||||||
|
kill "$pid"
|
||||||
|
done
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
if [ "$rv" -ne 0 ]; then
|
||||||
|
for i in $(seq 0 9); do
|
||||||
|
echo "server$i log:"
|
||||||
|
cat "$WORK_DIR/dist-minio-v6-900$i.log"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
for i in $(seq 0 9); do
|
||||||
|
rm -f "$WORK_DIR/dist-minio-v6-900$i.log"
|
||||||
|
done
|
||||||
|
|
||||||
|
return "$rv"
|
||||||
|
}
|
||||||
|
|
||||||
function run_test_dist_erasure_sets()
|
function run_test_dist_erasure_sets()
|
||||||
{
|
{
|
||||||
minio_pids=( $(start_minio_dist_erasure_sets) )
|
minio_pids=( $(start_minio_dist_erasure_sets) )
|
||||||
@@ -237,6 +295,7 @@ function run_test_gateway_s3()
|
|||||||
{
|
{
|
||||||
minio_pid="$(start_minio_gateway_s3)"
|
minio_pid="$(start_minio_gateway_s3)"
|
||||||
|
|
||||||
|
export SERVER_ENDPOINT="127.0.0.1:9000"
|
||||||
export ACCESS_KEY=Q3AM3UQ867SPQQA43P2F
|
export ACCESS_KEY=Q3AM3UQ867SPQQA43P2F
|
||||||
export SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
export SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
||||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||||
@@ -279,6 +338,7 @@ function __init__()
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
sed -i 's|-sS|-sSg|g' "$FUNCTIONAL_TESTS"
|
||||||
chmod a+x "$FUNCTIONAL_TESTS"
|
chmod a+x "$FUNCTIONAL_TESTS"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,6 +379,13 @@ function main()
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Testing in Distributed Erasure setup as sets with ipv6"
|
||||||
|
if ! run_test_dist_erasure_sets_ipv6; then
|
||||||
|
echo "FAILED"
|
||||||
|
rm -fr "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Testing in Gateway S3 setup"
|
echo "Testing in Gateway S3 setup"
|
||||||
if ! run_test_gateway_s3; then
|
if ! run_test_gateway_s3; then
|
||||||
echo "FAILED"
|
echo "FAILED"
|
||||||
|
|||||||
+14
-9
@@ -21,6 +21,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,26 +57,28 @@ type accessControlPolicy struct {
|
|||||||
func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetBucketACL")
|
ctx := newContext(r, w, "GetBucketACL")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "GetBucketACL", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow getBucketACL if policy action is set, since this is a dummy call
|
// Allow getBucketACL if policy action is set, since this is a dummy call
|
||||||
// we are simply re-purposing the bucketPolicyAction.
|
// we are simply re-purposing the bucketPolicyAction.
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before proceeding validate if bucket exists.
|
// Before proceeding validate if bucket exists.
|
||||||
_, err := objAPI.GetBucketInfo(ctx, bucket)
|
_, err := objAPI.GetBucketInfo(ctx, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +92,7 @@ func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.
|
|||||||
Permission: "FULL_CONTROL",
|
Permission: "FULL_CONTROL",
|
||||||
})
|
})
|
||||||
if err := xml.NewEncoder(w).Encode(acl); err != nil {
|
if err := xml.NewEncoder(w).Encode(acl); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,27 +106,29 @@ func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.
|
|||||||
func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetObjectACL")
|
ctx := newContext(r, w, "GetObjectACL")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "GetObjectACL", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
object := vars["object"]
|
object := vars["object"]
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow getObjectACL if policy action is set, since this is a dummy call
|
// Allow getObjectACL if policy action is set, since this is a dummy call
|
||||||
// we are simply re-purposing the bucketPolicyAction.
|
// we are simply re-purposing the bucketPolicyAction.
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before proceeding validate if object exists.
|
// Before proceeding validate if object exists.
|
||||||
_, err := objAPI.GetObjectInfo(ctx, bucket, object)
|
_, err := objAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +142,7 @@ func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.
|
|||||||
Permission: "FULL_CONTROL",
|
Permission: "FULL_CONTROL",
|
||||||
})
|
})
|
||||||
if err := xml.NewEncoder(w).Encode(acl); err != nil {
|
if err := xml.NewEncoder(w).Encode(acl); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+863
-97
File diff suppressed because it is too large
Load Diff
+213
-171
@@ -27,6 +27,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -37,160 +38,194 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
configJSON = []byte(`{
|
configJSON = []byte(`{
|
||||||
"version": "28",
|
"version": "33",
|
||||||
"credential": {
|
"credential": {
|
||||||
"accessKey": "minio",
|
"accessKey": "minio",
|
||||||
"secretKey": "minio123"
|
"secretKey": "minio123"
|
||||||
|
},
|
||||||
|
"region": "us-east-1",
|
||||||
|
"worm": "off",
|
||||||
|
"storageclass": {
|
||||||
|
"standard": "",
|
||||||
|
"rrs": ""
|
||||||
|
},
|
||||||
|
"cache": {
|
||||||
|
"drives": [],
|
||||||
|
"expiry": 90,
|
||||||
|
"maxuse": 80,
|
||||||
|
"exclude": []
|
||||||
|
},
|
||||||
|
"kms": {
|
||||||
|
"vault": {
|
||||||
|
"endpoint": "",
|
||||||
|
"auth": {
|
||||||
|
"type": "",
|
||||||
|
"approle": {
|
||||||
|
"id": "",
|
||||||
|
"secret": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"key-id": {
|
||||||
|
"name": "",
|
||||||
|
"version": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notify": {
|
||||||
|
"amqp": {
|
||||||
|
"1": {
|
||||||
|
"enable": false,
|
||||||
|
"url": "",
|
||||||
|
"exchange": "",
|
||||||
|
"routingKey": "",
|
||||||
|
"exchangeType": "",
|
||||||
|
"deliveryMode": 0,
|
||||||
|
"mandatory": false,
|
||||||
|
"immediate": false,
|
||||||
|
"durable": false,
|
||||||
|
"internal": false,
|
||||||
|
"noWait": false,
|
||||||
|
"autoDeleted": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"elasticsearch": {
|
||||||
|
"1": {
|
||||||
|
"enable": false,
|
||||||
|
"format": "namespace",
|
||||||
|
"url": "",
|
||||||
|
"index": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"kafka": {
|
||||||
|
"1": {
|
||||||
|
"enable": false,
|
||||||
|
"brokers": null,
|
||||||
|
"topic": "",
|
||||||
|
"tls": {
|
||||||
|
"enable": false,
|
||||||
|
"skipVerify": false,
|
||||||
|
"clientAuth": 0
|
||||||
|
},
|
||||||
|
"sasl": {
|
||||||
|
"enable": false,
|
||||||
|
"username": "",
|
||||||
|
"password": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mqtt": {
|
||||||
|
"1": {
|
||||||
|
"enable": false,
|
||||||
|
"broker": "",
|
||||||
|
"topic": "",
|
||||||
|
"qos": 0,
|
||||||
|
"clientId": "",
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"reconnectInterval": 0,
|
||||||
|
"keepAliveInterval": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mysql": {
|
||||||
|
"1": {
|
||||||
|
"enable": false,
|
||||||
|
"format": "namespace",
|
||||||
|
"dsnString": "",
|
||||||
|
"table": "",
|
||||||
|
"host": "",
|
||||||
|
"port": "",
|
||||||
|
"user": "",
|
||||||
|
"password": "",
|
||||||
|
"database": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nats": {
|
||||||
|
"1": {
|
||||||
|
"enable": false,
|
||||||
|
"address": "",
|
||||||
|
"subject": "",
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"token": "",
|
||||||
|
"secure": false,
|
||||||
|
"pingInterval": 0,
|
||||||
|
"streaming": {
|
||||||
|
"enable": false,
|
||||||
|
"clusterID": "",
|
||||||
|
"async": false,
|
||||||
|
"maxPubAcksInflight": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"region": "",
|
"nsq": {
|
||||||
"browser": "on",
|
"1": {
|
||||||
"worm": "off",
|
"enable": false,
|
||||||
"domain": "",
|
"nsqdAddress": "",
|
||||||
"storageclass": {
|
"topic": "",
|
||||||
"standard": "",
|
"tls": {
|
||||||
"rrs": ""
|
"enable": false,
|
||||||
},
|
"skipVerify": false
|
||||||
"cache": {
|
|
||||||
"drives": [],
|
|
||||||
"expiry": 90,
|
|
||||||
"maxuse": 80,
|
|
||||||
"exclude": []
|
|
||||||
},
|
|
||||||
"kms": {
|
|
||||||
"vault": {
|
|
||||||
"endpoint": "",
|
|
||||||
"auth": {
|
|
||||||
"type": "",
|
|
||||||
"approle": {
|
|
||||||
"id": "",
|
|
||||||
"secret": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"key-id": {
|
|
||||||
"name": "",
|
|
||||||
"version": 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"notify": {
|
},
|
||||||
"amqp": {
|
"postgresql": {
|
||||||
"1": {
|
"1": {
|
||||||
"enable": false,
|
"enable": false,
|
||||||
"url": "",
|
"format": "namespace",
|
||||||
"exchange": "",
|
"connectionString": "",
|
||||||
"routingKey": "",
|
"table": "",
|
||||||
"exchangeType": "",
|
"host": "",
|
||||||
"deliveryMode": 0,
|
"port": "",
|
||||||
"mandatory": false,
|
"user": "",
|
||||||
"immediate": false,
|
"password": "",
|
||||||
"durable": false,
|
"database": ""
|
||||||
"internal": false,
|
}
|
||||||
"noWait": false,
|
},
|
||||||
"autoDeleted": false
|
"redis": {
|
||||||
}
|
"1": {
|
||||||
},
|
"enable": false,
|
||||||
"elasticsearch": {
|
"format": "namespace",
|
||||||
"1": {
|
"address": "",
|
||||||
"enable": false,
|
"password": "",
|
||||||
"format": "",
|
"key": ""
|
||||||
"url": "",
|
}
|
||||||
"index": ""
|
},
|
||||||
}
|
"webhook": {
|
||||||
},
|
"1": {
|
||||||
"kafka": {
|
"enable": false,
|
||||||
"1": {
|
"endpoint": ""
|
||||||
"enable": false,
|
}
|
||||||
"brokers": null,
|
}
|
||||||
"topic": ""
|
},
|
||||||
}
|
"logger": {
|
||||||
},
|
"console": {
|
||||||
"mqtt": {
|
"enabled": true
|
||||||
"1": {
|
},
|
||||||
"enable": false,
|
"http": {
|
||||||
"broker": "",
|
"1": {
|
||||||
"topic": "",
|
"enabled": false,
|
||||||
"qos": 0,
|
"endpoint": "https://username:password@example.com/api"
|
||||||
"clientId": "",
|
}
|
||||||
"username": "",
|
}
|
||||||
"password": "",
|
},
|
||||||
"reconnectInterval": 0,
|
"compress": {
|
||||||
"keepAliveInterval": 0
|
"enabled": false,
|
||||||
}
|
"extensions":[".txt",".log",".csv",".json"],
|
||||||
},
|
"mime-types":["text/csv","text/plain","application/json"]
|
||||||
"mysql": {
|
},
|
||||||
"1": {
|
"openid": {
|
||||||
"enable": false,
|
"jwks": {
|
||||||
"format": "",
|
"url": ""
|
||||||
"dsnString": "",
|
}
|
||||||
"table": "",
|
},
|
||||||
"host": "",
|
"policy": {
|
||||||
"port": "",
|
"opa": {
|
||||||
"user": "",
|
"url": "",
|
||||||
"password": "",
|
"authToken": ""
|
||||||
"database": ""
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"nats": {
|
`)
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"address": "",
|
|
||||||
"subject": "",
|
|
||||||
"username": "",
|
|
||||||
"password": "",
|
|
||||||
"token": "",
|
|
||||||
"secure": false,
|
|
||||||
"pingInterval": 0,
|
|
||||||
"streaming": {
|
|
||||||
"enable": false,
|
|
||||||
"clusterID": "",
|
|
||||||
"clientID": "",
|
|
||||||
"async": false,
|
|
||||||
"maxPubAcksInflight": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"postgresql": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"format": "",
|
|
||||||
"connectionString": "",
|
|
||||||
"table": "",
|
|
||||||
"host": "",
|
|
||||||
"port": "",
|
|
||||||
"user": "",
|
|
||||||
"password": "",
|
|
||||||
"database": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"redis": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"format": "",
|
|
||||||
"address": "",
|
|
||||||
"password": "",
|
|
||||||
"key": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"webhook": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"endpoint": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"logger": {
|
|
||||||
"console": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"http": {
|
|
||||||
"1": {
|
|
||||||
"enabled": false,
|
|
||||||
"endpoint": "http://user:example@localhost:9001/api/endpoint"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}`)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// adminXLTestBed - encapsulates subsystems that need to be setup for
|
// adminXLTestBed - encapsulates subsystems that need to be setup for
|
||||||
@@ -242,7 +277,7 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
|||||||
|
|
||||||
// Setup admin mgmt REST API handlers.
|
// Setup admin mgmt REST API handlers.
|
||||||
adminRouter := mux.NewRouter()
|
adminRouter := mux.NewRouter()
|
||||||
registerAdminRouter(adminRouter)
|
registerAdminRouter(adminRouter, true)
|
||||||
|
|
||||||
return &adminXLTestBed{
|
return &adminXLTestBed{
|
||||||
xlDirs: xlDirs,
|
xlDirs: xlDirs,
|
||||||
@@ -273,8 +308,8 @@ func (atb *adminXLTestBed) GenerateHealTestData(t *testing.T) {
|
|||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
objectName := fmt.Sprintf("%s-%d", objName, i)
|
objectName := fmt.Sprintf("%s-%d", objName, i)
|
||||||
_, err = atb.objLayer.PutObject(context.Background(), bucketName, objectName,
|
_, err = atb.objLayer.PutObject(context.Background(), bucketName, objectName,
|
||||||
mustGetHashReader(t, bytes.NewReader([]byte("hello")),
|
mustGetPutObjReader(t, bytes.NewReader([]byte("hello")),
|
||||||
int64(len("hello")), "", ""), nil)
|
int64(len("hello")), "", ""), nil, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create %s - %v", objectName,
|
t.Fatalf("Failed to create %s - %v", objectName,
|
||||||
err)
|
err)
|
||||||
@@ -286,14 +321,14 @@ func (atb *adminXLTestBed) GenerateHealTestData(t *testing.T) {
|
|||||||
{
|
{
|
||||||
objName := "mpObject"
|
objName := "mpObject"
|
||||||
uploadID, err := atb.objLayer.NewMultipartUpload(context.Background(), bucketName,
|
uploadID, err := atb.objLayer.NewMultipartUpload(context.Background(), bucketName,
|
||||||
objName, nil)
|
objName, nil, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("mp new error: %v", err)
|
t.Fatalf("mp new error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = atb.objLayer.PutObjectPart(context.Background(), bucketName, objName,
|
_, err = atb.objLayer.PutObjectPart(context.Background(), bucketName, objName,
|
||||||
uploadID, 3, mustGetHashReader(t, bytes.NewReader(
|
uploadID, 3, mustGetPutObjReader(t, bytes.NewReader(
|
||||||
[]byte("hello")), int64(len("hello")), "", ""))
|
[]byte("hello")), int64(len("hello")), "", ""), ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("mp put error: %v", err)
|
t.Fatalf("mp put error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -472,6 +507,8 @@ func getServiceCmdRequest(cmd cmdType, cred auth.Credentials, body []byte) (*htt
|
|||||||
|
|
||||||
// Set body
|
// Set body
|
||||||
req.Body = ioutil.NopCloser(bytes.NewReader(body))
|
req.Body = ioutil.NopCloser(bytes.NewReader(body))
|
||||||
|
req.ContentLength = int64(len(body))
|
||||||
|
|
||||||
// Set sha-sum header
|
// Set sha-sum header
|
||||||
req.Header.Set("X-Amz-Content-Sha256", getSHA256Hash(body))
|
req.Header.Set("X-Amz-Content-Sha256", getSHA256Hash(body))
|
||||||
|
|
||||||
@@ -498,15 +535,21 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
|||||||
globalMinioAddr = "127.0.0.1:9000"
|
globalMinioAddr = "127.0.0.1:9000"
|
||||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
// Setting up a go routine to simulate ServerRouter's
|
// Setting up a go routine to simulate ServerRouter's
|
||||||
// handleServiceSignals for stop and restart commands.
|
// handleServiceSignals for stop and restart commands.
|
||||||
if cmd == restartCmd {
|
if cmd == restartCmd {
|
||||||
go testServiceSignalReceiver(cmd, t)
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
testServiceSignalReceiver(cmd, t)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
credentials := globalServerConfig.GetCredential()
|
credentials := globalServerConfig.GetCredential()
|
||||||
|
|
||||||
body, err := json.Marshal(madmin.ServiceAction{
|
body, err := json.Marshal(madmin.ServiceAction{
|
||||||
cmd.toServiceActionValue()})
|
Action: cmd.toServiceActionValue()})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("JSONify error: %v", err)
|
t.Fatalf("JSONify error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -537,6 +580,9 @@ func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
|||||||
t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
|
t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
|
||||||
http.StatusOK, rec.Code, string(resp))
|
http.StatusOK, rec.Code, string(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait until testServiceSignalReceiver() called in a goroutine quits.
|
||||||
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test for service status management REST API.
|
// Test for service status management REST API.
|
||||||
@@ -593,7 +639,7 @@ func TestServiceSetCreds(t *testing.T) {
|
|||||||
t.Fatalf("JSONify err: %v", err)
|
t.Fatalf("JSONify err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ebody, err := madmin.EncryptServerConfigData(credentials.SecretKey, body)
|
ebody, err := madmin.EncryptData(credentials.SecretKey, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -691,16 +737,12 @@ func TestSetConfigHandler(t *testing.T) {
|
|||||||
globalMinioAddr = "127.0.0.1:9000"
|
globalMinioAddr = "127.0.0.1:9000"
|
||||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||||
|
|
||||||
// SetConfigHandler restarts minio setup - need to start a
|
|
||||||
// signal receiver to receive on globalServiceSignalCh.
|
|
||||||
go testServiceSignalReceiver(restartCmd, t)
|
|
||||||
|
|
||||||
// Prepare query params for set-config mgmt REST API.
|
// Prepare query params for set-config mgmt REST API.
|
||||||
queryVal := url.Values{}
|
queryVal := url.Values{}
|
||||||
queryVal.Set("config", "")
|
queryVal.Set("config", "")
|
||||||
|
|
||||||
password := globalServerConfig.GetCredential().SecretKey
|
password := globalServerConfig.GetCredential().SecretKey
|
||||||
econfigJSON, err := madmin.EncryptServerConfigData(password, configJSON)
|
econfigJSON, err := madmin.EncryptData(password, configJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -714,13 +756,13 @@ func TestSetConfigHandler(t *testing.T) {
|
|||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
adminTestBed.router.ServeHTTP(rec, req)
|
adminTestBed.router.ServeHTTP(rec, req)
|
||||||
if rec.Code != http.StatusOK {
|
if rec.Code != http.StatusOK {
|
||||||
t.Errorf("Expected to succeed but failed with %d", rec.Code)
|
t.Errorf("Expected to succeed but failed with %d, body: %s", rec.Code, rec.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that a very large config file returns an error.
|
// Check that a very large config file returns an error.
|
||||||
{
|
{
|
||||||
// Make a large enough config string
|
// Make a large enough config string
|
||||||
invalidCfg := []byte(strings.Repeat("A", maxConfigJSONSize+1))
|
invalidCfg := []byte(strings.Repeat("A", maxEConfigJSONSize+1))
|
||||||
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
||||||
int64(len(invalidCfg)), bytes.NewReader(invalidCfg))
|
int64(len(invalidCfg)), bytes.NewReader(invalidCfg))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -750,7 +792,7 @@ func TestSetConfigHandler(t *testing.T) {
|
|||||||
adminTestBed.router.ServeHTTP(rec, req)
|
adminTestBed.router.ServeHTTP(rec, req)
|
||||||
respBody := string(rec.Body.Bytes())
|
respBody := string(rec.Body.Bytes())
|
||||||
if rec.Code != http.StatusBadRequest ||
|
if rec.Code != http.StatusBadRequest ||
|
||||||
!strings.Contains(respBody, "JSON configuration provided has objects with duplicate keys") {
|
!strings.Contains(respBody, "JSON configuration provided is of incorrect format") {
|
||||||
t.Errorf("Got unexpected response code or body %d - %s", rec.Code, respBody)
|
t.Errorf("Got unexpected response code or body %d - %s", rec.Code, respBody)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -824,12 +866,12 @@ func TestToAdminAPIErr(t *testing.T) {
|
|||||||
// 3. Non-admin API specific error.
|
// 3. Non-admin API specific error.
|
||||||
{
|
{
|
||||||
err: errDiskNotFound,
|
err: errDiskNotFound,
|
||||||
expectedAPIErr: toAPIErrorCode(errDiskNotFound),
|
expectedAPIErr: toAPIErrorCode(context.Background(), errDiskNotFound),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range testCases {
|
for i, test := range testCases {
|
||||||
actualErr := toAdminAPIErrCode(test.err)
|
actualErr := toAdminAPIErrCode(context.Background(), test.err)
|
||||||
if actualErr != test.expectedAPIErr {
|
if actualErr != test.expectedAPIErr {
|
||||||
t.Errorf("Test %d: Expected %v but received %v",
|
t.Errorf("Test %d: Expected %v but received %v",
|
||||||
i+1, test.expectedAPIErr, actualErr)
|
i+1, test.expectedAPIErr, actualErr)
|
||||||
|
|||||||
+186
-63
@@ -20,12 +20,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
|
"github.com/minio/minio/pkg/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// healStatusSummary - overall short summary of a healing sequence
|
// healStatusSummary - overall short summary of a healing sequence
|
||||||
@@ -59,8 +62,8 @@ var (
|
|||||||
errHealPushStopNDiscard = fmt.Errorf("heal push stopped due to heal stop signal")
|
errHealPushStopNDiscard = fmt.Errorf("heal push stopped due to heal stop signal")
|
||||||
errHealStopSignalled = fmt.Errorf("heal stop signaled")
|
errHealStopSignalled = fmt.Errorf("heal stop signaled")
|
||||||
|
|
||||||
errFnHealFromAPIErr = func(err error) error {
|
errFnHealFromAPIErr = func(ctx context.Context, err error) error {
|
||||||
errCode := toAPIErrorCode(err)
|
errCode := toAPIErrorCode(ctx, err)
|
||||||
apiErr := getAPIError(errCode)
|
apiErr := getAPIError(errCode)
|
||||||
return fmt.Errorf("Heal internal error: %s: %s",
|
return fmt.Errorf("Heal internal error: %s: %s",
|
||||||
apiErr.Code, apiErr.Description)
|
apiErr.Code, apiErr.Description)
|
||||||
@@ -110,6 +113,32 @@ func initAllHealState(isErasureMode bool) {
|
|||||||
globalAllHealState = allHealState{
|
globalAllHealState = allHealState{
|
||||||
healSeqMap: make(map[string]*healSequence),
|
healSeqMap: make(map[string]*healSequence),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go globalAllHealState.periodicHealSeqsClean()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ahs *allHealState) periodicHealSeqsClean() {
|
||||||
|
// Launch clean-up routine to remove this heal sequence (after
|
||||||
|
// it ends) from the global state after timeout has elapsed.
|
||||||
|
ticker := time.NewTicker(time.Minute * 5)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
now := UTCNow()
|
||||||
|
ahs.Lock()
|
||||||
|
for path, h := range ahs.healSeqMap {
|
||||||
|
if h.hasEnded() && h.endTime.Add(keepHealSeqStateDuration).Before(now) {
|
||||||
|
delete(ahs.healSeqMap, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ahs.Unlock()
|
||||||
|
case <-globalServiceDoneCh:
|
||||||
|
// server could be restarting - need
|
||||||
|
// to exit immediately
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getHealSequence - Retrieve a heal sequence by path. The second
|
// getHealSequence - Retrieve a heal sequence by path. The second
|
||||||
@@ -121,6 +150,35 @@ func (ahs *allHealState) getHealSequence(path string) (h *healSequence, exists b
|
|||||||
return h, exists
|
return h, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ahs *allHealState) stopHealSequence(path string) ([]byte, APIErrorCode) {
|
||||||
|
var hsp madmin.HealStopSuccess
|
||||||
|
he, exists := ahs.getHealSequence(path)
|
||||||
|
if !exists {
|
||||||
|
hsp = madmin.HealStopSuccess{
|
||||||
|
ClientToken: "invalid",
|
||||||
|
StartTime: UTCNow(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hsp = madmin.HealStopSuccess{
|
||||||
|
ClientToken: he.clientToken,
|
||||||
|
ClientAddress: he.clientAddress,
|
||||||
|
StartTime: he.startTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
he.stop()
|
||||||
|
for !he.hasEnded() {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
ahs.Lock()
|
||||||
|
defer ahs.Unlock()
|
||||||
|
// Heal sequence explicitly stopped, remove it.
|
||||||
|
delete(ahs.healSeqMap, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(&hsp)
|
||||||
|
return b, toAdminAPIErrCode(context.Background(), err)
|
||||||
|
}
|
||||||
|
|
||||||
// LaunchNewHealSequence - launches a background routine that performs
|
// LaunchNewHealSequence - launches a background routine that performs
|
||||||
// healing according to the healSequence argument. For each heal
|
// healing according to the healSequence argument. For each heal
|
||||||
// sequence, state is stored in the `globalAllHealState`, which is a
|
// sequence, state is stored in the `globalAllHealState`, which is a
|
||||||
@@ -141,20 +199,20 @@ func (ahs *allHealState) LaunchNewHealSequence(h *healSequence) (
|
|||||||
existsAndLive = true
|
existsAndLive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if existsAndLive {
|
if existsAndLive {
|
||||||
// A heal sequence exists on the given path.
|
// A heal sequence exists on the given path.
|
||||||
if h.forceStarted {
|
if h.forceStarted {
|
||||||
// stop the running heal sequence - wait for
|
// stop the running heal sequence - wait for it to finish.
|
||||||
// it to finish.
|
|
||||||
he.stop()
|
he.stop()
|
||||||
for !he.hasEnded() {
|
for !he.hasEnded() {
|
||||||
time.Sleep(10 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errMsg = "Heal is already running on the given path " +
|
errMsg = "Heal is already running on the given path " +
|
||||||
"(use force-start option to stop and start afresh). " +
|
"(use force-start option to stop and start afresh). " +
|
||||||
fmt.Sprintf("The heal was started by IP %s at %s",
|
fmt.Sprintf("The heal was started by IP %s at %s, token is %s",
|
||||||
h.clientAddress, h.startTime)
|
h.clientAddress, h.startTime.Format(http.TimeFormat), h.clientToken)
|
||||||
|
|
||||||
return nil, ErrHealAlreadyRunning, errMsg
|
return nil, ErrHealAlreadyRunning, errMsg
|
||||||
}
|
}
|
||||||
@@ -181,48 +239,13 @@ func (ahs *allHealState) LaunchNewHealSequence(h *healSequence) (
|
|||||||
// Launch top-level background heal go-routine
|
// Launch top-level background heal go-routine
|
||||||
go h.healSequenceStart()
|
go h.healSequenceStart()
|
||||||
|
|
||||||
// Launch clean-up routine to remove this heal sequence (after
|
|
||||||
// it ends) from the global state after timeout has elapsed.
|
|
||||||
go func() {
|
|
||||||
var keepStateTimeout <-chan time.Time
|
|
||||||
ticker := time.NewTicker(time.Minute)
|
|
||||||
defer ticker.Stop()
|
|
||||||
everyMinute := ticker.C
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
// Check every minute if heal sequence has ended.
|
|
||||||
case <-everyMinute:
|
|
||||||
if h.hasEnded() {
|
|
||||||
keepStateTimeout = time.After(keepHealSeqStateDuration)
|
|
||||||
everyMinute = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This case does not fire until the heal
|
|
||||||
// sequence completes.
|
|
||||||
case <-keepStateTimeout:
|
|
||||||
// Heal sequence has ended, keep
|
|
||||||
// results state duration has elapsed,
|
|
||||||
// so purge state.
|
|
||||||
ahs.Lock()
|
|
||||||
defer ahs.Unlock()
|
|
||||||
delete(ahs.healSeqMap, h.path)
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-globalServiceDoneCh:
|
|
||||||
// server could be restarting - need
|
|
||||||
// to exit immediately
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
b, err := json.Marshal(madmin.HealStartSuccess{
|
b, err := json.Marshal(madmin.HealStartSuccess{
|
||||||
ClientToken: h.clientToken,
|
ClientToken: h.clientToken,
|
||||||
ClientAddress: h.clientAddress,
|
ClientAddress: h.clientAddress,
|
||||||
StartTime: h.startTime,
|
StartTime: h.startTime,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(h.ctx, err)
|
||||||
return nil, ErrInternalError, ""
|
return nil, ErrInternalError, ""
|
||||||
}
|
}
|
||||||
return b, ErrNone, ""
|
return b, ErrNone, ""
|
||||||
@@ -270,7 +293,7 @@ func (ahs *allHealState) PopHealStatusJSON(path string,
|
|||||||
|
|
||||||
jbytes, err := json.Marshal(h.currentStatus)
|
jbytes, err := json.Marshal(h.currentStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(h.ctx, err)
|
||||||
return nil, ErrInternalError
|
return nil, ErrInternalError
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,12 +306,15 @@ type healSequence struct {
|
|||||||
// bucket, and prefix on which heal seq. was initiated
|
// bucket, and prefix on which heal seq. was initiated
|
||||||
bucket, objPrefix string
|
bucket, objPrefix string
|
||||||
|
|
||||||
// path is just bucket + "/" + objPrefix
|
// path is just pathJoin(bucket, objPrefix)
|
||||||
path string
|
path string
|
||||||
|
|
||||||
// time at which heal sequence was started
|
// time at which heal sequence was started
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
|
|
||||||
|
// time at which heal sequence has ended
|
||||||
|
endTime time.Time
|
||||||
|
|
||||||
// Heal client info
|
// Heal client info
|
||||||
clientToken, clientAddress string
|
clientToken, clientAddress string
|
||||||
|
|
||||||
@@ -328,7 +354,7 @@ func newHealSequence(bucket, objPrefix, clientAddr string,
|
|||||||
return &healSequence{
|
return &healSequence{
|
||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
objPrefix: objPrefix,
|
objPrefix: objPrefix,
|
||||||
path: bucket + "/" + objPrefix,
|
path: pathJoin(bucket, objPrefix),
|
||||||
startTime: UTCNow(),
|
startTime: UTCNow(),
|
||||||
clientToken: mustGetUUID(),
|
clientToken: mustGetUUID(),
|
||||||
clientAddress: clientAddr,
|
clientAddress: clientAddr,
|
||||||
@@ -383,7 +409,6 @@ func (h *healSequence) stop() {
|
|||||||
// sequence automatically resumes. The return value indicates if the
|
// sequence automatically resumes. The return value indicates if the
|
||||||
// operation succeeded.
|
// operation succeeded.
|
||||||
func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
||||||
|
|
||||||
// start a timer to keep an upper time limit to find an empty
|
// start a timer to keep an upper time limit to find an empty
|
||||||
// slot to add the given heal result - if no slot is found it
|
// slot to add the given heal result - if no slot is found it
|
||||||
// means that the server is holding the maximum amount of
|
// means that the server is holding the maximum amount of
|
||||||
@@ -467,6 +492,7 @@ func (h *healSequence) healSequenceStart() {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case err, ok := <-h.traverseAndHealDoneCh:
|
case err, ok := <-h.traverseAndHealDoneCh:
|
||||||
|
h.endTime = UTCNow()
|
||||||
h.currentStatus.updateLock.Lock()
|
h.currentStatus.updateLock.Lock()
|
||||||
defer h.currentStatus.updateLock.Unlock()
|
defer h.currentStatus.updateLock.Unlock()
|
||||||
// Heal traversal is complete.
|
// Heal traversal is complete.
|
||||||
@@ -480,6 +506,7 @@ func (h *healSequence) healSequenceStart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case <-h.stopSignalCh:
|
case <-h.stopSignalCh:
|
||||||
|
h.endTime = UTCNow()
|
||||||
h.currentStatus.updateLock.Lock()
|
h.currentStatus.updateLock.Lock()
|
||||||
h.currentStatus.Summary = healStoppedStatus
|
h.currentStatus.Summary = healStoppedStatus
|
||||||
h.currentStatus.FailureDetail = errHealStopSignalled.Error()
|
h.currentStatus.FailureDetail = errHealStopSignalled.Error()
|
||||||
@@ -519,6 +546,9 @@ func (h *healSequence) traverseAndHeal() {
|
|||||||
// Start with format healing
|
// Start with format healing
|
||||||
checkErr(h.healDiskFormat)
|
checkErr(h.healDiskFormat)
|
||||||
|
|
||||||
|
// Start healing the config.
|
||||||
|
checkErr(h.healConfig)
|
||||||
|
|
||||||
// Heal buckets and objects
|
// Heal buckets and objects
|
||||||
checkErr(h.healBuckets)
|
checkErr(h.healBuckets)
|
||||||
|
|
||||||
@@ -529,9 +559,72 @@ func (h *healSequence) traverseAndHeal() {
|
|||||||
close(h.traverseAndHealDoneCh)
|
close(h.traverseAndHealDoneCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// healConfig - heals config.json, retrun value indicates if a failure occurred.
|
||||||
|
func (h *healSequence) healConfig() error {
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Healing on configs is run regardless
|
||||||
|
// of any bucket being selected, this is to ensure that
|
||||||
|
// configs are always uptodate and correct.
|
||||||
|
marker := ""
|
||||||
|
isTruncated := true
|
||||||
|
for isTruncated {
|
||||||
|
if globalHTTPServer != nil {
|
||||||
|
// Wait at max 1 minute for an inprogress request
|
||||||
|
// before proceeding to heal
|
||||||
|
waitCount := 60
|
||||||
|
// Any requests in progress, delay the heal.
|
||||||
|
for globalHTTPServer.GetRequestCount() > 2 && waitCount > 0 {
|
||||||
|
waitCount--
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lists all objects under `config` prefix.
|
||||||
|
objectInfos, err := objectAPI.ListObjectsHeal(h.ctx, minioMetaBucket, minioConfigPrefix,
|
||||||
|
marker, "", 1000)
|
||||||
|
if err != nil {
|
||||||
|
return errFnHealFromAPIErr(h.ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := range objectInfos.Objects {
|
||||||
|
if h.isQuitting() {
|
||||||
|
return errHealStopSignalled
|
||||||
|
}
|
||||||
|
o := objectInfos.Objects[index]
|
||||||
|
res, herr := objectAPI.HealObject(h.ctx, o.Bucket, o.Name, h.settings.DryRun)
|
||||||
|
// Object might have been deleted, by the time heal
|
||||||
|
// was attempted we ignore this file an move on.
|
||||||
|
if isErrObjectNotFound(herr) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if herr != nil {
|
||||||
|
return herr
|
||||||
|
}
|
||||||
|
res.Type = madmin.HealItemBucketMetadata
|
||||||
|
if err = h.pushHealResultItem(res); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isTruncated = objectInfos.IsTruncated
|
||||||
|
marker = objectInfos.NextMarker
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// healDiskFormat - heals format.json, return value indicates if a
|
// healDiskFormat - heals format.json, return value indicates if a
|
||||||
// failure error occurred.
|
// failure error occurred.
|
||||||
func (h *healSequence) healDiskFormat() error {
|
func (h *healSequence) healDiskFormat() error {
|
||||||
|
if h.isQuitting() {
|
||||||
|
return errHealStopSignalled
|
||||||
|
}
|
||||||
|
|
||||||
// Get current object layer instance.
|
// Get current object layer instance.
|
||||||
objectAPI := newObjectLayerFn()
|
objectAPI := newObjectLayerFn()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
@@ -542,13 +635,18 @@ func (h *healSequence) healDiskFormat() error {
|
|||||||
// return any error, ignore error returned when disks have
|
// return any error, ignore error returned when disks have
|
||||||
// already healed.
|
// already healed.
|
||||||
if err != nil && err != errNoHealRequired {
|
if err != nil && err != errNoHealRequired {
|
||||||
return errFnHealFromAPIErr(err)
|
return errFnHealFromAPIErr(h.ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Healing succeeded notify the peers to reload format and re-initialize disks.
|
// Healing succeeded notify the peers to reload format and re-initialize disks.
|
||||||
// We will not notify peers only if healing succeeded.
|
// We will not notify peers only if healing succeeded.
|
||||||
if err == nil {
|
if err == nil {
|
||||||
peersReInitFormat(globalAdminPeers, h.settings.DryRun)
|
for host, rerr := range globalNotificationSys.ReloadFormat(h.settings.DryRun) {
|
||||||
|
if rerr != nil {
|
||||||
|
logger.GetReqInfo(h.ctx).SetTags("peerAddress", host.String())
|
||||||
|
logger.LogIf(h.ctx, rerr)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push format heal result
|
// Push format heal result
|
||||||
@@ -557,6 +655,10 @@ func (h *healSequence) healDiskFormat() error {
|
|||||||
|
|
||||||
// healBuckets - check for all buckets heal or just particular bucket.
|
// healBuckets - check for all buckets heal or just particular bucket.
|
||||||
func (h *healSequence) healBuckets() error {
|
func (h *healSequence) healBuckets() error {
|
||||||
|
if h.isQuitting() {
|
||||||
|
return errHealStopSignalled
|
||||||
|
}
|
||||||
|
|
||||||
// 1. If a bucket was specified, heal only the bucket.
|
// 1. If a bucket was specified, heal only the bucket.
|
||||||
if h.bucket != "" {
|
if h.bucket != "" {
|
||||||
return h.healBucket(h.bucket)
|
return h.healBucket(h.bucket)
|
||||||
@@ -570,7 +672,7 @@ func (h *healSequence) healBuckets() error {
|
|||||||
|
|
||||||
buckets, err := objectAPI.ListBucketsHeal(h.ctx)
|
buckets, err := objectAPI.ListBucketsHeal(h.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errFnHealFromAPIErr(err)
|
return errFnHealFromAPIErr(h.ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, bucket := range buckets {
|
for _, bucket := range buckets {
|
||||||
@@ -584,10 +686,6 @@ func (h *healSequence) healBuckets() error {
|
|||||||
|
|
||||||
// healBucket - traverses and heals given bucket
|
// healBucket - traverses and heals given bucket
|
||||||
func (h *healSequence) healBucket(bucket string) error {
|
func (h *healSequence) healBucket(bucket string) error {
|
||||||
if h.isQuitting() {
|
|
||||||
return errHealStopSignalled
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current object layer instance.
|
// Get current object layer instance.
|
||||||
objectAPI := newObjectLayerFn()
|
objectAPI := newObjectLayerFn()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
@@ -610,10 +708,9 @@ func (h *healSequence) healBucket(bucket string) error {
|
|||||||
if h.objPrefix != "" {
|
if h.objPrefix != "" {
|
||||||
// Check if an object named as the objPrefix exists,
|
// Check if an object named as the objPrefix exists,
|
||||||
// and if so heal it.
|
// and if so heal it.
|
||||||
_, err = objectAPI.GetObjectInfo(h.ctx, bucket, h.objPrefix)
|
_, err = objectAPI.GetObjectInfo(h.ctx, bucket, h.objPrefix, ObjectOptions{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = h.healObject(bucket, h.objPrefix)
|
if err = h.healObject(bucket, h.objPrefix); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -622,17 +719,40 @@ func (h *healSequence) healBucket(bucket string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entries := runtime.NumCPU() * globalEndpoints.Nodes()
|
||||||
|
|
||||||
marker := ""
|
marker := ""
|
||||||
isTruncated := true
|
isTruncated := true
|
||||||
for isTruncated {
|
for isTruncated {
|
||||||
objectInfos, err := objectAPI.ListObjectsHeal(h.ctx, bucket,
|
if globalHTTPServer != nil {
|
||||||
h.objPrefix, marker, "", 1000)
|
// Wait at max 1 minute for an inprogress request
|
||||||
if err != nil {
|
// before proceeding to heal
|
||||||
return errFnHealFromAPIErr(err)
|
waitCount := 60
|
||||||
|
// Any requests in progress, delay the heal.
|
||||||
|
for globalHTTPServer.GetRequestCount() > 2 && waitCount > 0 {
|
||||||
|
waitCount--
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range objectInfos.Objects {
|
// Heal numCPU * nodes objects at a time.
|
||||||
if err := h.healObject(o.Bucket, o.Name); err != nil {
|
objectInfos, err := objectAPI.ListObjectsHeal(h.ctx, bucket,
|
||||||
|
h.objPrefix, marker, "", entries)
|
||||||
|
if err != nil {
|
||||||
|
return errFnHealFromAPIErr(h.ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
g := errgroup.WithNErrs(len(objectInfos.Objects))
|
||||||
|
for index := range objectInfos.Objects {
|
||||||
|
index := index
|
||||||
|
g.Go(func() error {
|
||||||
|
o := objectInfos.Objects[index]
|
||||||
|
return h.healObject(o.Bucket, o.Name)
|
||||||
|
}, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, err := range g.Wait() {
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -656,6 +776,9 @@ func (h *healSequence) healObject(bucket, object string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hri, err := objectAPI.HealObject(h.ctx, bucket, object, h.settings.DryRun)
|
hri, err := objectAPI.HealObject(h.ctx, bucket, object, h.settings.DryRun)
|
||||||
|
if isErrObjectNotFound(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hri.Detail = err.Error()
|
hri.Detail = err.Error()
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-12
@@ -31,7 +31,7 @@ type adminAPIHandlers struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// registerAdminRouter - Add handler functions for each service REST API routes.
|
// registerAdminRouter - Add handler functions for each service REST API routes.
|
||||||
func registerAdminRouter(router *mux.Router) {
|
func registerAdminRouter(router *mux.Router, enableIAM bool) {
|
||||||
|
|
||||||
adminAPI := adminAPIHandlers{}
|
adminAPI := adminAPIHandlers{}
|
||||||
// Admin router
|
// Admin router
|
||||||
@@ -53,19 +53,60 @@ func registerAdminRouter(router *mux.Router) {
|
|||||||
// Info operations
|
// Info operations
|
||||||
adminV1Router.Methods(http.MethodGet).Path("/info").HandlerFunc(httpTraceAll(adminAPI.ServerInfoHandler))
|
adminV1Router.Methods(http.MethodGet).Path("/info").HandlerFunc(httpTraceAll(adminAPI.ServerInfoHandler))
|
||||||
|
|
||||||
/// Heal operations
|
if globalIsDistXL || globalIsXL {
|
||||||
|
/// Heal operations
|
||||||
|
|
||||||
// Heal processing endpoint.
|
// Heal processing endpoint.
|
||||||
adminV1Router.Methods(http.MethodPost).Path("/heal/").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
adminV1Router.Methods(http.MethodPost).Path("/heal/").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||||
adminV1Router.Methods(http.MethodPost).Path("/heal/{bucket}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
adminV1Router.Methods(http.MethodPost).Path("/heal/{bucket}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||||
adminV1Router.Methods(http.MethodPost).Path("/heal/{bucket}/{prefix:.*}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
adminV1Router.Methods(http.MethodPost).Path("/heal/{bucket}/{prefix:.*}").HandlerFunc(httpTraceAll(adminAPI.HealHandler))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profiling operations
|
||||||
|
adminV1Router.Methods(http.MethodPost).Path("/profiling/start").HandlerFunc(httpTraceAll(adminAPI.StartProfilingHandler)).
|
||||||
|
Queries("profilerType", "{profilerType:.*}")
|
||||||
|
adminV1Router.Methods(http.MethodGet).Path("/profiling/download").HandlerFunc(httpTraceAll(adminAPI.DownloadProfilingHandler))
|
||||||
|
|
||||||
/// Config operations
|
/// Config operations
|
||||||
|
|
||||||
// Update credentials
|
if enableIAM {
|
||||||
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateCredentialsHandler))
|
// Update credentials
|
||||||
// Get config
|
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateAdminCredentialsHandler))
|
||||||
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
// Get config
|
||||||
// Set config
|
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
||||||
adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
|
// Set config
|
||||||
|
adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
|
||||||
|
|
||||||
|
// Get config keys/values
|
||||||
|
adminV1Router.Methods(http.MethodGet).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigKeysHandler))
|
||||||
|
// Set config keys/values
|
||||||
|
adminV1Router.Methods(http.MethodPut).Path("/config-keys").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigKeysHandler))
|
||||||
|
|
||||||
|
// -- IAM APIs --
|
||||||
|
|
||||||
|
// Add policy IAM
|
||||||
|
adminV1Router.Methods(http.MethodPut).Path("/add-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.AddCannedPolicy)).Queries("name", "{name:.*}")
|
||||||
|
|
||||||
|
// Add user IAM
|
||||||
|
adminV1Router.Methods(http.MethodPut).Path("/add-user").HandlerFunc(httpTraceHdrs(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
|
||||||
|
adminV1Router.Methods(http.MethodPut).Path("/set-user-policy").HandlerFunc(httpTraceHdrs(adminAPI.SetUserPolicy)).
|
||||||
|
Queries("accessKey", "{accessKey:.*}").Queries("name", "{name:.*}")
|
||||||
|
adminV1Router.Methods(http.MethodPut).Path("/set-user-status").HandlerFunc(httpTraceHdrs(adminAPI.SetUserStatus)).
|
||||||
|
Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")
|
||||||
|
|
||||||
|
// Remove policy IAM
|
||||||
|
adminV1Router.Methods(http.MethodDelete).Path("/remove-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")
|
||||||
|
|
||||||
|
// Remove user IAM
|
||||||
|
adminV1Router.Methods(http.MethodDelete).Path("/remove-user").HandlerFunc(httpTraceHdrs(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
|
||||||
|
|
||||||
|
// List users
|
||||||
|
adminV1Router.Methods(http.MethodGet).Path("/list-users").HandlerFunc(httpTraceHdrs(adminAPI.ListUsers))
|
||||||
|
|
||||||
|
// List policies
|
||||||
|
adminV1Router.Methods(http.MethodGet).Path("/list-canned-policies").HandlerFunc(httpTraceHdrs(adminAPI.ListCannedPolicies))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If none of the routes match, return error.
|
||||||
|
adminV1Router.NotFoundHandler = http.HandlerFunc(httpTraceHdrs(notFoundHandlerJSON))
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-35
@@ -45,26 +45,25 @@ func (rpcClient *AdminRPCClient) SignalService(signal serviceSignal) (err error)
|
|||||||
return rpcClient.Call(adminServiceName+".SignalService", &args, &reply)
|
return rpcClient.Call(adminServiceName+".SignalService", &args, &reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReInitFormat - re-initialize disk format, remotely.
|
|
||||||
func (rpcClient *AdminRPCClient) ReInitFormat(dryRun bool) error {
|
|
||||||
args := ReInitFormatArgs{DryRun: dryRun}
|
|
||||||
reply := VoidReply{}
|
|
||||||
|
|
||||||
return rpcClient.Call(adminServiceName+".ReInitFormat", &args, &reply)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (rpcClient *AdminRPCClient) ServerInfo() (sid ServerInfoData, err error) {
|
func (rpcClient *AdminRPCClient) ServerInfo() (sid ServerInfoData, err error) {
|
||||||
err = rpcClient.Call(adminServiceName+".ServerInfo", &AuthArgs{}, &sid)
|
err = rpcClient.Call(adminServiceName+".ServerInfo", &AuthArgs{}, &sid)
|
||||||
return sid, err
|
return sid, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfig - returns config.json of the remote server.
|
// StartProfiling - starts profiling in the remote server.
|
||||||
func (rpcClient *AdminRPCClient) GetConfig() ([]byte, error) {
|
func (rpcClient *AdminRPCClient) StartProfiling(profiler string) error {
|
||||||
|
args := StartProfilingArgs{Profiler: profiler}
|
||||||
|
reply := VoidReply{}
|
||||||
|
return rpcClient.Call(adminServiceName+".StartProfiling", &args, &reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadProfilingData - returns profiling data of the remote server.
|
||||||
|
func (rpcClient *AdminRPCClient) DownloadProfilingData() ([]byte, error) {
|
||||||
args := AuthArgs{}
|
args := AuthArgs{}
|
||||||
var reply []byte
|
var reply []byte
|
||||||
|
|
||||||
err := rpcClient.Call(adminServiceName+".GetConfig", &args, &reply)
|
err := rpcClient.Call(adminServiceName+".DownloadProfilingData", &args, &reply)
|
||||||
return reply, err
|
return reply, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,9 +108,9 @@ func NewAdminRPCClient(host *xnet.Host) (*AdminRPCClient, error) {
|
|||||||
// commands like service stop and service restart.
|
// commands like service stop and service restart.
|
||||||
type adminCmdRunner interface {
|
type adminCmdRunner interface {
|
||||||
SignalService(s serviceSignal) error
|
SignalService(s serviceSignal) error
|
||||||
ReInitFormat(dryRun bool) error
|
|
||||||
ServerInfo() (ServerInfoData, error)
|
ServerInfo() (ServerInfoData, error)
|
||||||
GetConfig() ([]byte, error)
|
StartProfiling(string) error
|
||||||
|
DownloadProfilingData() ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// adminPeer - represents an entity that implements admin API RPCs.
|
// adminPeer - represents an entity that implements admin API RPCs.
|
||||||
@@ -131,6 +130,11 @@ func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
|
|||||||
// Use first IPv4 instead of loopback address.
|
// Use first IPv4 instead of loopback address.
|
||||||
localAddr = net.JoinHostPort(sortIPs(localIP4.ToSlice())[0], globalMinioPort)
|
localAddr = net.JoinHostPort(sortIPs(localIP4.ToSlice())[0], globalMinioPort)
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(localAddr, "[::1]:") {
|
||||||
|
// Use first IPv4 instead of loopback address.
|
||||||
|
localAddr = net.JoinHostPort(localIP6.ToSlice()[0], globalMinioPort)
|
||||||
|
}
|
||||||
|
|
||||||
adminPeerList = append(adminPeerList, adminPeer{
|
adminPeerList = append(adminPeerList, adminPeer{
|
||||||
addr: localAddr,
|
addr: localAddr,
|
||||||
cmdRunner: localAdminClient{},
|
cmdRunner: localAdminClient{},
|
||||||
@@ -139,9 +143,9 @@ func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
|
|||||||
|
|
||||||
for _, hostStr := range GetRemotePeers(endpoints) {
|
for _, hostStr := range GetRemotePeers(endpoints) {
|
||||||
host, err := xnet.ParseHost(hostStr)
|
host, err := xnet.ParseHost(hostStr)
|
||||||
logger.FatalIf(err, "Unable to parse Admin RPC Host", context.Background())
|
logger.FatalIf(err, "Unable to parse Admin RPC Host")
|
||||||
rpcClient, err := NewAdminRPCClient(host)
|
rpcClient, err := NewAdminRPCClient(host)
|
||||||
logger.FatalIf(err, "Unable to initialize Admin RPC Client", context.Background())
|
logger.FatalIf(err, "Unable to initialize Admin RPC Client")
|
||||||
adminPeerList = append(adminPeerList, adminPeer{
|
adminPeerList = append(adminPeerList, adminPeer{
|
||||||
addr: hostStr,
|
addr: hostStr,
|
||||||
cmdRunner: rpcClient,
|
cmdRunner: rpcClient,
|
||||||
@@ -151,26 +155,6 @@ func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
|
|||||||
return adminPeerList
|
return adminPeerList
|
||||||
}
|
}
|
||||||
|
|
||||||
// peersReInitFormat - reinitialize remote object layers to new format.
|
|
||||||
func peersReInitFormat(peers adminPeers, dryRun bool) error {
|
|
||||||
errs := make([]error, len(peers))
|
|
||||||
|
|
||||||
// Send ReInitFormat RPC call to all nodes.
|
|
||||||
// for local adminPeer this is a no-op.
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
for i, peer := range peers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int, peer adminPeer) {
|
|
||||||
defer wg.Done()
|
|
||||||
if !peer.isLocal {
|
|
||||||
errs[idx] = peer.cmdRunner.ReInitFormat(dryRun)
|
|
||||||
}
|
|
||||||
}(i, peer)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize global adminPeer collection.
|
// Initialize global adminPeer collection.
|
||||||
func initGlobalAdminPeers(endpoints EndpointList) {
|
func initGlobalAdminPeers(endpoints EndpointList) {
|
||||||
globalAdminPeers = makeAdminPeers(endpoints)
|
globalAdminPeers = makeAdminPeers(endpoints)
|
||||||
|
|||||||
+13
-14
@@ -17,7 +17,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@@ -52,21 +51,21 @@ func (receiver *adminRPCReceiver) ServerInfo(args *AuthArgs, reply *ServerInfoDa
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfig - returns the config.json of this server.
|
// StartProfilingArgs - holds the RPC argument for StartingProfiling RPC call
|
||||||
func (receiver *adminRPCReceiver) GetConfig(args *AuthArgs, reply *[]byte) (err error) {
|
type StartProfilingArgs struct {
|
||||||
*reply, err = receiver.local.GetConfig()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReInitFormatArgs - provides dry-run information to re-initialize format.json
|
|
||||||
type ReInitFormatArgs struct {
|
|
||||||
AuthArgs
|
AuthArgs
|
||||||
DryRun bool
|
Profiler string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReInitFormat - re-init 'format.json'
|
// StartProfiling - starts profiling of this server
|
||||||
func (receiver *adminRPCReceiver) ReInitFormat(args *ReInitFormatArgs, reply *VoidReply) error {
|
func (receiver *adminRPCReceiver) StartProfiling(args *StartProfilingArgs, reply *VoidReply) error {
|
||||||
return receiver.local.ReInitFormat(args.DryRun)
|
return receiver.local.StartProfiling(args.Profiler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadProfilingData - stops and returns profiling data of this server
|
||||||
|
func (receiver *adminRPCReceiver) DownloadProfilingData(args *AuthArgs, reply *[]byte) (err error) {
|
||||||
|
*reply, err = receiver.local.DownloadProfilingData()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAdminRPCServer - returns new admin RPC server.
|
// NewAdminRPCServer - returns new admin RPC server.
|
||||||
@@ -81,7 +80,7 @@ func NewAdminRPCServer() (*xrpc.Server, error) {
|
|||||||
// registerAdminRPCRouter - creates and registers Admin RPC server and its router.
|
// registerAdminRPCRouter - creates and registers Admin RPC server and its router.
|
||||||
func registerAdminRPCRouter(router *mux.Router) {
|
func registerAdminRPCRouter(router *mux.Router) {
|
||||||
rpcServer, err := NewAdminRPCServer()
|
rpcServer, err := NewAdminRPCServer()
|
||||||
logger.FatalIf(err, "Unable to initialize Lock RPC Server", context.Background())
|
logger.FatalIf(err, "Unable to initialize Lock RPC Server")
|
||||||
subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter()
|
subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter()
|
||||||
subrouter.Path(adminServiceSubPath).HandlerFunc(httpTraceHdrs(rpcServer.ServeHTTP))
|
subrouter.Path(adminServiceSubPath).HandlerFunc(httpTraceHdrs(rpcServer.ServeHTTP))
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-79
@@ -40,11 +40,8 @@ import (
|
|||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
func testAdminCmdRunnerSignalService(t *testing.T, client adminCmdRunner) {
|
func testAdminCmdRunnerSignalService(t *testing.T, client adminCmdRunner) {
|
||||||
tmpGlobalServiceSignalCh := globalServiceSignalCh
|
defer func(sigChan chan serviceSignal) { globalServiceSignalCh = sigChan }(globalServiceSignalCh)
|
||||||
globalServiceSignalCh = make(chan serviceSignal, 10)
|
globalServiceSignalCh = make(chan serviceSignal, 10)
|
||||||
defer func() {
|
|
||||||
globalServiceSignalCh = tmpGlobalServiceSignalCh
|
|
||||||
}()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
signal serviceSignal
|
signal serviceSignal
|
||||||
@@ -66,34 +63,6 @@ func testAdminCmdRunnerSignalService(t *testing.T, client adminCmdRunner) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAdminCmdRunnerReInitFormat(t *testing.T, client adminCmdRunner) {
|
|
||||||
tmpGlobalObjectAPI := globalObjectAPI
|
|
||||||
defer func() {
|
|
||||||
globalObjectAPI = tmpGlobalObjectAPI
|
|
||||||
}()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
objectAPI ObjectLayer
|
|
||||||
dryRun bool
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{&DummyObjectLayer{}, true, false},
|
|
||||||
{&DummyObjectLayer{}, false, false},
|
|
||||||
{nil, true, true},
|
|
||||||
{nil, false, true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
globalObjectAPI = testCase.objectAPI
|
|
||||||
err := client.ReInitFormat(testCase.dryRun)
|
|
||||||
expectErr := (err != nil)
|
|
||||||
|
|
||||||
if expectErr != testCase.expectErr {
|
|
||||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
|
func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
|
||||||
tmpGlobalBootTime := globalBootTime
|
tmpGlobalBootTime := globalBootTime
|
||||||
tmpGlobalObjectAPI := globalObjectAPI
|
tmpGlobalObjectAPI := globalObjectAPI
|
||||||
@@ -140,33 +109,6 @@ func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAdminCmdRunnerGetConfig(t *testing.T, client adminCmdRunner) {
|
|
||||||
tmpGlobalServerConfig := globalServerConfig
|
|
||||||
defer func() {
|
|
||||||
globalServerConfig = tmpGlobalServerConfig
|
|
||||||
}()
|
|
||||||
|
|
||||||
config := newServerConfig()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
config *serverConfig
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{globalServerConfig, false},
|
|
||||||
{config, false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
globalServerConfig = testCase.config
|
|
||||||
_, err := client.GetConfig()
|
|
||||||
expectErr := (err != nil)
|
|
||||||
|
|
||||||
if expectErr != testCase.expectErr {
|
|
||||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAdminRPCHTTPServerClient(t *testing.T) (*httptest.Server, *AdminRPCClient, *serverConfig) {
|
func newAdminRPCHTTPServerClient(t *testing.T) (*httptest.Server, *AdminRPCClient, *serverConfig) {
|
||||||
rpcServer, err := NewAdminRPCServer()
|
rpcServer, err := NewAdminRPCServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -208,16 +150,6 @@ func TestAdminRPCClientSignalService(t *testing.T) {
|
|||||||
testAdminCmdRunnerSignalService(t, rpcClient)
|
testAdminCmdRunnerSignalService(t, rpcClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminRPCClientReInitFormat(t *testing.T) {
|
|
||||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
|
||||||
defer httpServer.Close()
|
|
||||||
defer func() {
|
|
||||||
globalServerConfig = prevGlobalServerConfig
|
|
||||||
}()
|
|
||||||
|
|
||||||
testAdminCmdRunnerReInitFormat(t, rpcClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdminRPCClientServerInfo(t *testing.T) {
|
func TestAdminRPCClientServerInfo(t *testing.T) {
|
||||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
||||||
defer httpServer.Close()
|
defer httpServer.Close()
|
||||||
@@ -227,13 +159,3 @@ func TestAdminRPCClientServerInfo(t *testing.T) {
|
|||||||
|
|
||||||
testAdminCmdRunnerServerInfo(t, rpcClient)
|
testAdminCmdRunnerServerInfo(t, rpcClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminRPCClientGetConfig(t *testing.T) {
|
|
||||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
|
||||||
defer httpServer.Close()
|
|
||||||
defer func() {
|
|
||||||
globalServerConfig = prevGlobalServerConfig
|
|
||||||
}()
|
|
||||||
|
|
||||||
testAdminCmdRunnerGetConfig(t, rpcClient)
|
|
||||||
}
|
|
||||||
|
|||||||
+2
-81
@@ -23,89 +23,10 @@ import (
|
|||||||
const (
|
const (
|
||||||
// Response request id.
|
// Response request id.
|
||||||
responseRequestIDKey = "x-amz-request-id"
|
responseRequestIDKey = "x-amz-request-id"
|
||||||
|
// Deployment id.
|
||||||
|
responseDeploymentIDKey = "x-minio-deployment-id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CSVFileHeaderInfo -Can be either USE IGNORE OR NONE, defines what to do with
|
|
||||||
// the first row
|
|
||||||
type CSVFileHeaderInfo string
|
|
||||||
|
|
||||||
// Constants for file header info.
|
|
||||||
const (
|
|
||||||
CSVFileHeaderInfoNone CSVFileHeaderInfo = "NONE"
|
|
||||||
CSVFileHeaderInfoIgnore = "IGNORE"
|
|
||||||
CSVFileHeaderInfoUse = "USE"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SelectCompressionType - ONLY GZIP is supported
|
|
||||||
type SelectCompressionType string
|
|
||||||
|
|
||||||
// Constants for compression types under select API.
|
|
||||||
const (
|
|
||||||
SelectCompressionNONE SelectCompressionType = "NONE"
|
|
||||||
SelectCompressionGZIP = "GZIP"
|
|
||||||
SelectCompressionBZIP = "BZIP2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CSVQuoteFields - Can be either Always or AsNeeded
|
|
||||||
type CSVQuoteFields string
|
|
||||||
|
|
||||||
// Constants for csv quote styles.
|
|
||||||
const (
|
|
||||||
CSVQuoteFieldsAlways CSVQuoteFields = "Always"
|
|
||||||
CSVQuoteFieldsAsNeeded = "AsNeeded"
|
|
||||||
)
|
|
||||||
|
|
||||||
// QueryExpressionType - Currently can only be SQL
|
|
||||||
type QueryExpressionType string
|
|
||||||
|
|
||||||
// Constants for expression type.
|
|
||||||
const (
|
|
||||||
QueryExpressionTypeSQL QueryExpressionType = "SQL"
|
|
||||||
)
|
|
||||||
|
|
||||||
// JSONType determines json input serialization type.
|
|
||||||
type JSONType string
|
|
||||||
|
|
||||||
// Constants for JSONTypes.
|
|
||||||
const (
|
|
||||||
JSONDocumentType JSONType = "Document"
|
|
||||||
JSONStreamType = "Stream"
|
|
||||||
JSONLinesType = "Lines"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ObjectSelectRequest - represents the input select body
|
|
||||||
type ObjectSelectRequest struct {
|
|
||||||
XMLName xml.Name `xml:"SelectObjectContentRequest" json:"-"`
|
|
||||||
Expression string
|
|
||||||
ExpressionType QueryExpressionType
|
|
||||||
InputSerialization struct {
|
|
||||||
CompressionType SelectCompressionType
|
|
||||||
CSV *struct {
|
|
||||||
FileHeaderInfo CSVFileHeaderInfo
|
|
||||||
RecordDelimiter string
|
|
||||||
FieldDelimiter string
|
|
||||||
QuoteCharacter string
|
|
||||||
QuoteEscapeCharacter string
|
|
||||||
Comments string
|
|
||||||
}
|
|
||||||
JSON *struct {
|
|
||||||
Type JSONType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutputSerialization struct {
|
|
||||||
CSV *struct {
|
|
||||||
QuoteFields CSVQuoteFields
|
|
||||||
RecordDelimiter string
|
|
||||||
FieldDelimiter string
|
|
||||||
QuoteCharacter string
|
|
||||||
QuoteEscapeCharacter string
|
|
||||||
}
|
|
||||||
JSON *struct {
|
|
||||||
RecordDelimiter string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectIdentifier carries key name for the object to delete.
|
// ObjectIdentifier carries key name for the object to delete.
|
||||||
type ObjectIdentifier struct {
|
type ObjectIdentifier struct {
|
||||||
ObjectName string `xml:"Key"`
|
ObjectName string `xml:"Key"`
|
||||||
|
|||||||
+107
-23
@@ -23,11 +23,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/dns"
|
"github.com/minio/minio/pkg/dns"
|
||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
"github.com/minio/minio/pkg/s3select"
|
"github.com/minio/minio/pkg/s3select"
|
||||||
|
"github.com/minio/minio/pkg/s3select/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
// APIError structure
|
// APIError structure
|
||||||
@@ -85,6 +87,7 @@ const (
|
|||||||
ErrNoSuchBucketPolicy
|
ErrNoSuchBucketPolicy
|
||||||
ErrNoSuchKey
|
ErrNoSuchKey
|
||||||
ErrNoSuchUpload
|
ErrNoSuchUpload
|
||||||
|
ErrNoSuchVersion
|
||||||
ErrNotImplemented
|
ErrNotImplemented
|
||||||
ErrPreconditionFailed
|
ErrPreconditionFailed
|
||||||
ErrRequestTimeTooSkewed
|
ErrRequestTimeTooSkewed
|
||||||
@@ -129,6 +132,7 @@ const (
|
|||||||
ErrMaximumExpires
|
ErrMaximumExpires
|
||||||
ErrSlowDown
|
ErrSlowDown
|
||||||
ErrInvalidPrefixMarker
|
ErrInvalidPrefixMarker
|
||||||
|
ErrBadRequest
|
||||||
// Add new error codes here.
|
// Add new error codes here.
|
||||||
|
|
||||||
// SSE-S3 related API errors
|
// SSE-S3 related API errors
|
||||||
@@ -149,6 +153,9 @@ const (
|
|||||||
ErrKMSNotConfigured
|
ErrKMSNotConfigured
|
||||||
ErrKMSAuthFailure
|
ErrKMSAuthFailure
|
||||||
|
|
||||||
|
ErrNoAccessKey
|
||||||
|
ErrInvalidToken
|
||||||
|
|
||||||
// Bucket notification related errors.
|
// Bucket notification related errors.
|
||||||
ErrEventNotification
|
ErrEventNotification
|
||||||
ErrARNNotification
|
ErrARNNotification
|
||||||
@@ -186,11 +193,15 @@ const (
|
|||||||
// new error codes here.
|
// new error codes here.
|
||||||
|
|
||||||
ErrMalformedJSON
|
ErrMalformedJSON
|
||||||
|
ErrAdminNoSuchUser
|
||||||
|
ErrAdminNoSuchPolicy
|
||||||
|
ErrAdminInvalidArgument
|
||||||
ErrAdminInvalidAccessKey
|
ErrAdminInvalidAccessKey
|
||||||
ErrAdminInvalidSecretKey
|
ErrAdminInvalidSecretKey
|
||||||
ErrAdminConfigNoQuorum
|
ErrAdminConfigNoQuorum
|
||||||
ErrAdminConfigTooLarge
|
ErrAdminConfigTooLarge
|
||||||
ErrAdminConfigBadJSON
|
ErrAdminConfigBadJSON
|
||||||
|
ErrAdminConfigDuplicateKeys
|
||||||
ErrAdminCredentialsMismatch
|
ErrAdminCredentialsMismatch
|
||||||
ErrInsecureClientRequest
|
ErrInsecureClientRequest
|
||||||
ErrObjectTampered
|
ErrObjectTampered
|
||||||
@@ -248,7 +259,6 @@ const (
|
|||||||
ErrParseUnsupportedAlias
|
ErrParseUnsupportedAlias
|
||||||
ErrParseUnsupportedSyntax
|
ErrParseUnsupportedSyntax
|
||||||
ErrParseUnknownOperator
|
ErrParseUnknownOperator
|
||||||
ErrParseInvalidPathComponent
|
|
||||||
ErrParseMissingIdentAfterAt
|
ErrParseMissingIdentAfterAt
|
||||||
ErrParseUnexpectedOperator
|
ErrParseUnexpectedOperator
|
||||||
ErrParseUnexpectedTerm
|
ErrParseUnexpectedTerm
|
||||||
@@ -287,8 +297,12 @@ const (
|
|||||||
ErrEvaluatorInvalidTimestampFormatPatternToken
|
ErrEvaluatorInvalidTimestampFormatPatternToken
|
||||||
ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
||||||
ErrEvaluatorBindingDoesNotExist
|
ErrEvaluatorBindingDoesNotExist
|
||||||
ErrInvalidColumnIndex
|
|
||||||
ErrMissingHeaders
|
ErrMissingHeaders
|
||||||
|
ErrInvalidColumnIndex
|
||||||
|
|
||||||
|
ErrAdminConfigNotificationTargetsFailed
|
||||||
|
ErrAdminProfilerNotEnabled
|
||||||
|
ErrInvalidDecompressedSize
|
||||||
)
|
)
|
||||||
|
|
||||||
// error code to APIError structure, these fields carry respective
|
// error code to APIError structure, these fields carry respective
|
||||||
@@ -434,6 +448,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "The specified multipart upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.",
|
Description: "The specified multipart upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.",
|
||||||
HTTPStatusCode: http.StatusNotFound,
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
|
ErrNoSuchVersion: {
|
||||||
|
Code: "NoSuchVersion",
|
||||||
|
Description: "Indicates that the version ID specified in the request does not match an existing version.",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
ErrNotImplemented: {
|
ErrNotImplemented: {
|
||||||
Code: "NotImplemented",
|
Code: "NotImplemented",
|
||||||
Description: "A header you provided implies functionality that is not implemented",
|
Description: "A header you provided implies functionality that is not implemented",
|
||||||
@@ -636,6 +655,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "Invalid marker prefix combination",
|
Description: "Invalid marker prefix combination",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrBadRequest: {
|
||||||
|
Code: "BadRequest",
|
||||||
|
Description: "400 BadRequest",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
|
||||||
// FIXME: Actual XML error response also contains the header which missed in list of signed header parameters.
|
// FIXME: Actual XML error response also contains the header which missed in list of signed header parameters.
|
||||||
ErrUnsignedHeaders: {
|
ErrUnsignedHeaders: {
|
||||||
@@ -795,6 +819,16 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "Server side encryption specified but KMS authorization failed",
|
Description: "Server side encryption specified but KMS authorization failed",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrNoAccessKey: {
|
||||||
|
Code: "AccessDenied",
|
||||||
|
Description: "No AWSAccessKey was presented",
|
||||||
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
ErrInvalidToken: {
|
||||||
|
Code: "InvalidTokenId",
|
||||||
|
Description: "The security token included in the request is invalid",
|
||||||
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
|
||||||
/// S3 extensions.
|
/// S3 extensions.
|
||||||
ErrContentSHA256Mismatch: {
|
ErrContentSHA256Mismatch: {
|
||||||
@@ -854,6 +888,21 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "The JSON you provided was not well-formed or did not validate against our published format.",
|
Description: "The JSON you provided was not well-formed or did not validate against our published format.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrAdminNoSuchUser: {
|
||||||
|
Code: "XMinioAdminNoSuchUser",
|
||||||
|
Description: "The specified user does not exist.",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
ErrAdminNoSuchPolicy: {
|
||||||
|
Code: "XMinioAdminNoSuchPolicy",
|
||||||
|
Description: "The canned policy does not exist.",
|
||||||
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
ErrAdminInvalidArgument: {
|
||||||
|
Code: "XMinioAdminInvalidArgument",
|
||||||
|
Description: "Invalid arguments specified.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrAdminInvalidAccessKey: {
|
ErrAdminInvalidAccessKey: {
|
||||||
Code: "XMinioAdminInvalidAccessKey",
|
Code: "XMinioAdminInvalidAccessKey",
|
||||||
Description: "The access key is invalid.",
|
Description: "The access key is invalid.",
|
||||||
@@ -872,14 +921,29 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
ErrAdminConfigTooLarge: {
|
ErrAdminConfigTooLarge: {
|
||||||
Code: "XMinioAdminConfigTooLarge",
|
Code: "XMinioAdminConfigTooLarge",
|
||||||
Description: fmt.Sprintf("Configuration data provided exceeds the allowed maximum of %d bytes",
|
Description: fmt.Sprintf("Configuration data provided exceeds the allowed maximum of %d bytes",
|
||||||
maxConfigJSONSize),
|
maxEConfigJSONSize),
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrAdminConfigBadJSON: {
|
ErrAdminConfigBadJSON: {
|
||||||
Code: "XMinioAdminConfigBadJSON",
|
Code: "XMinioAdminConfigBadJSON",
|
||||||
|
Description: "JSON configuration provided is of incorrect format",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrAdminConfigDuplicateKeys: {
|
||||||
|
Code: "XMinioAdminConfigDuplicateKeys",
|
||||||
Description: "JSON configuration provided has objects with duplicate keys",
|
Description: "JSON configuration provided has objects with duplicate keys",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrAdminConfigNotificationTargetsFailed: {
|
||||||
|
Code: "XMinioAdminNotificationTargetsTestFailed",
|
||||||
|
Description: "Configuration update failed due an unsuccessful attempt to connect to one or more notification servers",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrAdminProfilerNotEnabled: {
|
||||||
|
Code: "XMinioAdminProfilerNotEnabled",
|
||||||
|
Description: "Unable to perform the requested operation because profiling is not enabled",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrAdminCredentialsMismatch: {
|
ErrAdminCredentialsMismatch: {
|
||||||
Code: "XMinioAdminCredentialsMismatch",
|
Code: "XMinioAdminCredentialsMismatch",
|
||||||
Description: "Credentials in config mismatch with server environment variables",
|
Description: "Credentials in config mismatch with server environment variables",
|
||||||
@@ -1180,11 +1244,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "The SQL expression contains an invalid operator.",
|
Description: "The SQL expression contains an invalid operator.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrParseInvalidPathComponent: {
|
|
||||||
Code: "ParseInvalidPathComponent",
|
|
||||||
Description: "The SQL expression contains an invalid path component.",
|
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
ErrParseMissingIdentAfterAt: {
|
ErrParseMissingIdentAfterAt: {
|
||||||
Code: "ParseMissingIdentAfterAt",
|
Code: "ParseMissingIdentAfterAt",
|
||||||
Description: "Did not find the expected identifier after the @ symbol in the SQL expression.",
|
Description: "Did not find the expected identifier after the @ symbol in the SQL expression.",
|
||||||
@@ -1370,11 +1429,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "Time stamp format pattern contains an invalid symbol in the SQL expression.",
|
Description: "Time stamp format pattern contains an invalid symbol in the SQL expression.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrInvalidColumnIndex: {
|
|
||||||
Code: "InvalidColumnIndex",
|
|
||||||
Description: "Column index in the SQL expression is invalid.",
|
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
ErrEvaluatorBindingDoesNotExist: {
|
ErrEvaluatorBindingDoesNotExist: {
|
||||||
Code: "ErrEvaluatorBindingDoesNotExist",
|
Code: "ErrEvaluatorBindingDoesNotExist",
|
||||||
Description: "A column name or a path provided does not exist in the SQL expression",
|
Description: "A column name or a path provided does not exist in the SQL expression",
|
||||||
@@ -1385,21 +1439,39 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "Some headers in the query are missing from the file. Check the file and try again.",
|
Description: "Some headers in the query are missing from the file. Check the file and try again.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrInvalidColumnIndex: {
|
||||||
|
Code: "InvalidColumnIndex",
|
||||||
|
Description: "The column index is invalid. Please check the service documentation and try again.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidDecompressedSize: {
|
||||||
|
Code: "XMinioInvalidDecompressedSize",
|
||||||
|
Description: "The data provided is unfit for decompression",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
// Add your error structure here.
|
// Add your error structure here.
|
||||||
}
|
}
|
||||||
|
|
||||||
// toAPIErrorCode - Converts embedded errors. Convenience
|
// toAPIErrorCode - Converts embedded errors. Convenience
|
||||||
// function written to handle all cases where we have known types of
|
// function written to handle all cases where we have known types of
|
||||||
// errors returned by underlying layers.
|
// errors returned by underlying layers.
|
||||||
func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return ErrNone
|
return ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify if the underlying error is signature mismatch.
|
// Verify if the underlying error is signature mismatch.
|
||||||
switch err {
|
switch err {
|
||||||
|
case errInvalidArgument:
|
||||||
|
apiErr = ErrAdminInvalidArgument
|
||||||
|
case errNoSuchUser:
|
||||||
|
apiErr = ErrAdminNoSuchUser
|
||||||
|
case errNoSuchPolicy:
|
||||||
|
apiErr = ErrAdminNoSuchPolicy
|
||||||
case errSignatureMismatch:
|
case errSignatureMismatch:
|
||||||
apiErr = ErrSignatureDoesNotMatch
|
apiErr = ErrSignatureDoesNotMatch
|
||||||
|
case errInvalidRange:
|
||||||
|
apiErr = ErrInvalidRange
|
||||||
case errDataTooLarge:
|
case errDataTooLarge:
|
||||||
apiErr = ErrEntityTooLarge
|
apiErr = ErrEntityTooLarge
|
||||||
case errDataTooSmall:
|
case errDataTooSmall:
|
||||||
@@ -1409,10 +1481,10 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
case auth.ErrInvalidSecretKeyLength:
|
case auth.ErrInvalidSecretKeyLength:
|
||||||
apiErr = ErrAdminInvalidSecretKey
|
apiErr = ErrAdminInvalidSecretKey
|
||||||
// SSE errors
|
// SSE errors
|
||||||
|
case errInvalidEncryptionParameters:
|
||||||
|
apiErr = ErrInvalidEncryptionParameters
|
||||||
case crypto.ErrInvalidEncryptionMethod:
|
case crypto.ErrInvalidEncryptionMethod:
|
||||||
apiErr = ErrInvalidEncryptionMethod
|
apiErr = ErrInvalidEncryptionMethod
|
||||||
case errInsecureSSERequest:
|
|
||||||
apiErr = ErrInsecureSSECustomerRequest
|
|
||||||
case crypto.ErrInvalidCustomerAlgorithm:
|
case crypto.ErrInvalidCustomerAlgorithm:
|
||||||
apiErr = ErrInvalidSSECustomerAlgorithm
|
apiErr = ErrInvalidSSECustomerAlgorithm
|
||||||
case crypto.ErrInvalidCustomerKey:
|
case crypto.ErrInvalidCustomerKey:
|
||||||
@@ -1429,7 +1501,7 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrSSEEncryptedObject
|
apiErr = ErrSSEEncryptedObject
|
||||||
case errInvalidSSEParameters:
|
case errInvalidSSEParameters:
|
||||||
apiErr = ErrInvalidSSECustomerParameters
|
apiErr = ErrInvalidSSECustomerParameters
|
||||||
case crypto.ErrInvalidCustomerKey:
|
case crypto.ErrInvalidCustomerKey, crypto.ErrSecretKeyMismatch:
|
||||||
apiErr = ErrAccessDenied // no access without correct key
|
apiErr = ErrAccessDenied // no access without correct key
|
||||||
case crypto.ErrIncompatibleEncryptionMethod:
|
case crypto.ErrIncompatibleEncryptionMethod:
|
||||||
apiErr = ErrIncompatibleEncryptionMethod
|
apiErr = ErrIncompatibleEncryptionMethod
|
||||||
@@ -1437,7 +1509,7 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrKMSNotConfigured
|
apiErr = ErrKMSNotConfigured
|
||||||
case crypto.ErrKMSAuthLogin:
|
case crypto.ErrKMSAuthLogin:
|
||||||
apiErr = ErrKMSAuthFailure
|
apiErr = ErrKMSAuthFailure
|
||||||
case context.Canceled, context.DeadlineExceeded:
|
case errOperationTimedOut, context.Canceled, context.DeadlineExceeded:
|
||||||
apiErr = ErrOperationTimedOut
|
apiErr = ErrOperationTimedOut
|
||||||
}
|
}
|
||||||
switch err {
|
switch err {
|
||||||
@@ -1521,8 +1593,6 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrParseUnsupportedSyntax
|
apiErr = ErrParseUnsupportedSyntax
|
||||||
case s3select.ErrParseUnknownOperator:
|
case s3select.ErrParseUnknownOperator:
|
||||||
apiErr = ErrParseUnknownOperator
|
apiErr = ErrParseUnknownOperator
|
||||||
case s3select.ErrParseInvalidPathComponent:
|
|
||||||
apiErr = ErrParseInvalidPathComponent
|
|
||||||
case s3select.ErrParseMissingIdentAfterAt:
|
case s3select.ErrParseMissingIdentAfterAt:
|
||||||
apiErr = ErrParseMissingIdentAfterAt
|
apiErr = ErrParseMissingIdentAfterAt
|
||||||
case s3select.ErrParseUnexpectedOperator:
|
case s3select.ErrParseUnexpectedOperator:
|
||||||
@@ -1595,13 +1665,20 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrEvaluatorInvalidTimestampFormatPatternToken
|
apiErr = ErrEvaluatorInvalidTimestampFormatPatternToken
|
||||||
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbol:
|
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbol:
|
||||||
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
||||||
case s3select.ErrInvalidColumnIndex:
|
|
||||||
apiErr = ErrInvalidColumnIndex
|
|
||||||
case s3select.ErrEvaluatorBindingDoesNotExist:
|
case s3select.ErrEvaluatorBindingDoesNotExist:
|
||||||
apiErr = ErrEvaluatorBindingDoesNotExist
|
apiErr = ErrEvaluatorBindingDoesNotExist
|
||||||
case s3select.ErrMissingHeaders:
|
case s3select.ErrMissingHeaders:
|
||||||
apiErr = ErrMissingHeaders
|
apiErr = ErrMissingHeaders
|
||||||
|
case format.ErrParseInvalidPathComponent:
|
||||||
|
apiErr = ErrMissingHeaders
|
||||||
|
case format.ErrInvalidColumnIndex:
|
||||||
|
apiErr = ErrInvalidColumnIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compression errors
|
||||||
|
switch err {
|
||||||
|
case errInvalidDecompressedSize:
|
||||||
|
apiErr = ErrInvalidDecompressedSize
|
||||||
}
|
}
|
||||||
|
|
||||||
if apiErr != ErrNone {
|
if apiErr != ErrNone {
|
||||||
@@ -1708,6 +1785,10 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrObjectTampered
|
apiErr = ErrObjectTampered
|
||||||
default:
|
default:
|
||||||
apiErr = ErrInternalError
|
apiErr = ErrInternalError
|
||||||
|
// Make sure to log the errors which we cannot translate
|
||||||
|
// to a meaningful S3 API errors. This is added to aid in
|
||||||
|
// debugging unexpected/unhandled errors.
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiErr
|
return apiErr
|
||||||
@@ -1715,7 +1796,10 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
|
|
||||||
// getAPIError provides API Error for input API error code.
|
// getAPIError provides API Error for input API error code.
|
||||||
func getAPIError(code APIErrorCode) APIError {
|
func getAPIError(code APIErrorCode) APIError {
|
||||||
return errorCodeResponse[code]
|
if apiErr, ok := errorCodeResponse[code]; ok {
|
||||||
|
return apiErr
|
||||||
|
}
|
||||||
|
return errorCodeResponse[ErrInternalError]
|
||||||
}
|
}
|
||||||
|
|
||||||
// getErrorResponse gets in standard error and resource value and
|
// getErrorResponse gets in standard error and resource value and
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -52,7 +53,6 @@ var toAPIErrorCodeTests = []struct {
|
|||||||
{err: errSignatureMismatch, errCode: ErrSignatureDoesNotMatch},
|
{err: errSignatureMismatch, errCode: ErrSignatureDoesNotMatch},
|
||||||
|
|
||||||
// SSE-C errors
|
// SSE-C errors
|
||||||
{err: errInsecureSSERequest, errCode: ErrInsecureSSECustomerRequest},
|
|
||||||
{err: crypto.ErrInvalidCustomerAlgorithm, errCode: ErrInvalidSSECustomerAlgorithm},
|
{err: crypto.ErrInvalidCustomerAlgorithm, errCode: ErrInvalidSSECustomerAlgorithm},
|
||||||
{err: crypto.ErrMissingCustomerKey, errCode: ErrMissingSSECustomerKey},
|
{err: crypto.ErrMissingCustomerKey, errCode: ErrMissingSSECustomerKey},
|
||||||
{err: crypto.ErrInvalidCustomerKey, errCode: ErrInvalidSSECustomerKey},
|
{err: crypto.ErrInvalidCustomerKey, errCode: ErrInvalidSSECustomerKey},
|
||||||
@@ -65,8 +65,9 @@ var toAPIErrorCodeTests = []struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAPIErrCode(t *testing.T) {
|
func TestAPIErrCode(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
for i, testCase := range toAPIErrorCodeTests {
|
for i, testCase := range toAPIErrorCodeTests {
|
||||||
errCode := toAPIErrorCode(testCase.err)
|
errCode := toAPIErrorCode(ctx, testCase.err)
|
||||||
if errCode != testCase.errCode {
|
if errCode != testCase.errCode {
|
||||||
t.Errorf("Test %d: Expected error code %d, got %d", i+1, testCase.errCode, errCode)
|
t.Errorf("Test %d: Expected error code %d, got %d", i+1, testCase.errCode, errCode)
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-9
@@ -24,6 +24,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns a hexadecimal representation of time at the
|
// Returns a hexadecimal representation of time at the
|
||||||
@@ -41,6 +43,9 @@ func setCommonHeaders(w http.ResponseWriter) {
|
|||||||
w.Header().Set("X-Amz-Bucket-Region", region)
|
w.Header().Set("X-Amz-Bucket-Region", region)
|
||||||
}
|
}
|
||||||
w.Header().Set("Accept-Ranges", "bytes")
|
w.Header().Set("Accept-Ranges", "bytes")
|
||||||
|
|
||||||
|
// Remove sensitive information
|
||||||
|
crypto.RemoveSensitiveHeaders(w.Header())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encodes the response headers into XML format.
|
// Encodes the response headers into XML format.
|
||||||
@@ -61,13 +66,10 @@ func encodeResponseJSON(response interface{}) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write object header
|
// Write object header
|
||||||
func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *httpRange) {
|
func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSpec) (err error) {
|
||||||
// set common headers
|
// set common headers
|
||||||
setCommonHeaders(w)
|
setCommonHeaders(w)
|
||||||
|
|
||||||
// Set content length.
|
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(objInfo.Size, 10))
|
|
||||||
|
|
||||||
// Set last modified time.
|
// Set last modified time.
|
||||||
lastModified := objInfo.ModTime.UTC().Format(http.TimeFormat)
|
lastModified := objInfo.ModTime.UTC().Format(http.TimeFormat)
|
||||||
w.Header().Set("Last-Modified", lastModified)
|
w.Header().Set("Last-Modified", lastModified)
|
||||||
@@ -95,10 +97,34 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *h
|
|||||||
w.Header().Set(k, v)
|
w.Header().Set(k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// for providing ranged content
|
var totalObjectSize int64
|
||||||
if contentRange != nil && contentRange.offsetBegin > -1 {
|
switch {
|
||||||
// Override content-length
|
case crypto.IsEncrypted(objInfo.UserDefined):
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(contentRange.getLength(), 10))
|
totalObjectSize, err = objInfo.DecryptedSize()
|
||||||
w.Header().Set("Content-Range", contentRange.String())
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case objInfo.IsCompressed():
|
||||||
|
totalObjectSize = objInfo.GetActualSize()
|
||||||
|
if totalObjectSize < 0 {
|
||||||
|
return errInvalidDecompressedSize
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
totalObjectSize = objInfo.Size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for providing ranged content
|
||||||
|
start, rangeLen, err := rs.GetOffsetLength(totalObjectSize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set content length.
|
||||||
|
w.Header().Set("Content-Length", strconv.FormatInt(rangeLen, 10))
|
||||||
|
if rs != nil {
|
||||||
|
contentRange := fmt.Sprintf("bytes %d-%d/%d", start, start+rangeLen-1, totalObjectSize)
|
||||||
|
w.Header().Set("Content-Range", contentRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-20
@@ -22,15 +22,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Parse bucket url queries
|
// Parse bucket url queries
|
||||||
func getListObjectsV1Args(values url.Values) (prefix, marker, delimiter string, maxkeys int, encodingType string) {
|
func getListObjectsV1Args(values url.Values) (prefix, marker, delimiter string, maxkeys int, encodingType string, errCode APIErrorCode) {
|
||||||
prefix = values.Get("prefix")
|
errCode = ErrNone
|
||||||
marker = values.Get("marker")
|
|
||||||
delimiter = values.Get("delimiter")
|
|
||||||
if values.Get("max-keys") != "" {
|
if values.Get("max-keys") != "" {
|
||||||
maxkeys, _ = strconv.Atoi(values.Get("max-keys"))
|
var err error
|
||||||
|
if maxkeys, err = strconv.Atoi(values.Get("max-keys")); err != nil {
|
||||||
|
errCode = ErrInvalidMaxKeys
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
maxkeys = maxObjectList
|
maxkeys = maxObjectList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prefix = values.Get("prefix")
|
||||||
|
marker = values.Get("marker")
|
||||||
|
delimiter = values.Get("delimiter")
|
||||||
encodingType = values.Get("encoding-type")
|
encodingType = values.Get("encoding-type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -47,44 +54,69 @@ func getListObjectsV2Args(values url.Values) (prefix, token, startAfter, delimit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if values.Get("max-keys") != "" {
|
||||||
|
var err error
|
||||||
|
if maxkeys, err = strconv.Atoi(values.Get("max-keys")); err != nil {
|
||||||
|
errCode = ErrInvalidMaxKeys
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
maxkeys = maxObjectList
|
||||||
|
}
|
||||||
|
|
||||||
prefix = values.Get("prefix")
|
prefix = values.Get("prefix")
|
||||||
token = values.Get("continuation-token")
|
token = values.Get("continuation-token")
|
||||||
startAfter = values.Get("start-after")
|
startAfter = values.Get("start-after")
|
||||||
delimiter = values.Get("delimiter")
|
delimiter = values.Get("delimiter")
|
||||||
if values.Get("max-keys") != "" {
|
|
||||||
maxkeys, _ = strconv.Atoi(values.Get("max-keys"))
|
|
||||||
} else {
|
|
||||||
maxkeys = maxObjectList
|
|
||||||
}
|
|
||||||
fetchOwner = values.Get("fetch-owner") == "true"
|
fetchOwner = values.Get("fetch-owner") == "true"
|
||||||
encodingType = values.Get("encoding-type")
|
encodingType = values.Get("encoding-type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse bucket url queries for ?uploads
|
// Parse bucket url queries for ?uploads
|
||||||
func getBucketMultipartResources(values url.Values) (prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int, encodingType string) {
|
func getBucketMultipartResources(values url.Values) (prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int, encodingType string, errCode APIErrorCode) {
|
||||||
|
errCode = ErrNone
|
||||||
|
|
||||||
|
if values.Get("max-uploads") != "" {
|
||||||
|
var err error
|
||||||
|
if maxUploads, err = strconv.Atoi(values.Get("max-uploads")); err != nil {
|
||||||
|
errCode = ErrInvalidMaxUploads
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
maxUploads = maxUploadsList
|
||||||
|
}
|
||||||
|
|
||||||
prefix = values.Get("prefix")
|
prefix = values.Get("prefix")
|
||||||
keyMarker = values.Get("key-marker")
|
keyMarker = values.Get("key-marker")
|
||||||
uploadIDMarker = values.Get("upload-id-marker")
|
uploadIDMarker = values.Get("upload-id-marker")
|
||||||
delimiter = values.Get("delimiter")
|
delimiter = values.Get("delimiter")
|
||||||
if values.Get("max-uploads") != "" {
|
|
||||||
maxUploads, _ = strconv.Atoi(values.Get("max-uploads"))
|
|
||||||
} else {
|
|
||||||
maxUploads = maxUploadsList
|
|
||||||
}
|
|
||||||
encodingType = values.Get("encoding-type")
|
encodingType = values.Get("encoding-type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse object url queries
|
// Parse object url queries
|
||||||
func getObjectResources(values url.Values) (uploadID string, partNumberMarker, maxParts int, encodingType string) {
|
func getObjectResources(values url.Values) (uploadID string, partNumberMarker, maxParts int, encodingType string, errCode APIErrorCode) {
|
||||||
uploadID = values.Get("uploadId")
|
var err error
|
||||||
partNumberMarker, _ = strconv.Atoi(values.Get("part-number-marker"))
|
errCode = ErrNone
|
||||||
|
|
||||||
if values.Get("max-parts") != "" {
|
if values.Get("max-parts") != "" {
|
||||||
maxParts, _ = strconv.Atoi(values.Get("max-parts"))
|
if maxParts, err = strconv.Atoi(values.Get("max-parts")); err != nil {
|
||||||
|
errCode = ErrInvalidMaxParts
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
maxParts = maxPartsList
|
maxParts = maxPartsList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if values.Get("part-number-marker") != "" {
|
||||||
|
if partNumberMarker, err = strconv.Atoi(values.Get("part-number-marker")); err != nil {
|
||||||
|
errCode = ErrInvalidPartNumberMarker
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadID = values.Get("uploadId")
|
||||||
encodingType = values.Get("encoding-type")
|
encodingType = values.Get("encoding-type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,10 @@ func TestListObjectsV1Resources(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
prefix, marker, delimiter, maxKeys, encodingType := getListObjectsV1Args(testCase.values)
|
prefix, marker, delimiter, maxKeys, encodingType, argsErr := getListObjectsV1Args(testCase.values)
|
||||||
|
if argsErr != ErrNone {
|
||||||
|
t.Errorf("Test %d: argument parsing failed, got %v", i+1, argsErr)
|
||||||
|
}
|
||||||
if prefix != testCase.prefix {
|
if prefix != testCase.prefix {
|
||||||
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.prefix, prefix)
|
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.prefix, prefix)
|
||||||
}
|
}
|
||||||
@@ -198,7 +201,10 @@ func TestGetObjectsResources(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
uploadID, partNumberMarker, maxParts, encodingType := getObjectResources(testCase.values)
|
uploadID, partNumberMarker, maxParts, encodingType, argsErr := getObjectResources(testCase.values)
|
||||||
|
if argsErr != ErrNone {
|
||||||
|
t.Errorf("Test %d: argument parsing failed, got %v", i+1, argsErr)
|
||||||
|
}
|
||||||
if uploadID != testCase.uploadID {
|
if uploadID != testCase.uploadID {
|
||||||
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.uploadID, uploadID)
|
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.uploadID, uploadID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
// Represents additional fields necessary for ErrPartTooSmall S3 error.
|
// Represents additional fields necessary for ErrPartTooSmall S3 error.
|
||||||
type completeMultipartAPIError struct {
|
type completeMultipartAPIError struct {
|
||||||
@@ -42,7 +45,7 @@ type completeMultipartAPIError struct {
|
|||||||
// of this function.
|
// of this function.
|
||||||
func writePartSmallErrorResponse(w http.ResponseWriter, r *http.Request, err PartTooSmall) {
|
func writePartSmallErrorResponse(w http.ResponseWriter, r *http.Request, err PartTooSmall) {
|
||||||
|
|
||||||
apiError := getAPIError(toAPIErrorCode(err))
|
apiError := getAPIError(toAPIErrorCode(context.Background(), err))
|
||||||
// Generate complete multipart error response.
|
// Generate complete multipart error response.
|
||||||
errorResponse := getAPIErrorResponse(apiError, r.URL.Path, w.Header().Get(responseRequestIDKey))
|
errorResponse := getAPIErrorResponse(apiError, r.URL.Path, w.Header().Get(responseRequestIDKey))
|
||||||
cmpErrResp := completeMultipartAPIError{err.PartSize, int64(5242880), err.PartNumber, err.PartETag, errorResponse}
|
cmpErrResp := completeMultipartAPIError{err.PartSize, int64(5242880), err.PartNumber, err.PartETag, errorResponse}
|
||||||
|
|||||||
+11
-2
@@ -568,13 +568,22 @@ func writeSuccessResponseHeadersOnly(w http.ResponseWriter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// writeErrorRespone writes error headers
|
// writeErrorRespone writes error headers
|
||||||
func writeErrorResponse(w http.ResponseWriter, errorCode APIErrorCode, reqURL *url.URL) {
|
func writeErrorResponse(w http.ResponseWriter, errorCode APIErrorCode, reqURL *url.URL, browser bool) {
|
||||||
switch errorCode {
|
switch errorCode {
|
||||||
case ErrSlowDown, ErrServerNotInitialized, ErrReadQuorum, ErrWriteQuorum:
|
case ErrSlowDown, ErrServerNotInitialized, ErrReadQuorum, ErrWriteQuorum:
|
||||||
// Set retry-after header to indicate user-agents to retry request after 120secs.
|
// Set retry-after header to indicate user-agents to retry request after 120secs.
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
||||||
w.Header().Set("Retry-After", "120")
|
w.Header().Set("Retry-After", "120")
|
||||||
|
case ErrAccessDenied:
|
||||||
|
// The request is from browser and also if browser
|
||||||
|
// is enabled we need to redirect.
|
||||||
|
if browser && globalIsBrowserEnabled {
|
||||||
|
w.Header().Set("Location", minioReservedBucketPath+reqURL.Path)
|
||||||
|
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apiError := getAPIError(errorCode)
|
apiError := getAPIError(errorCode)
|
||||||
// Generate error response.
|
// Generate error response.
|
||||||
errorResponse := getAPIErrorResponse(apiError, reqURL.Path, w.Header().Get(responseRequestIDKey))
|
errorResponse := getAPIErrorResponse(apiError, reqURL.Path, w.Header().Get(responseRequestIDKey))
|
||||||
@@ -608,7 +617,7 @@ func writeCustomErrorResponseJSON(w http.ResponseWriter, errorCode APIErrorCode,
|
|||||||
Code: apiError.Code,
|
Code: apiError.Code,
|
||||||
Message: errBody,
|
Message: errBody,
|
||||||
Resource: reqURL.Path,
|
Resource: reqURL.Path,
|
||||||
RequestID: "3L137",
|
RequestID: w.Header().Get(responseRequestIDKey),
|
||||||
HostID: "3L137",
|
HostID: "3L137",
|
||||||
}
|
}
|
||||||
encodedErrorResponse := encodeResponseJSON(errorResponse)
|
encodedErrorResponse := encodeResponseJSON(errorResponse)
|
||||||
|
|||||||
+200
-27
@@ -19,6 +19,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -27,8 +28,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
jwtgo "github.com/dgrijalva/jwt-go"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
|
"github.com/minio/minio/pkg/iam/policy"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,6 +90,7 @@ const (
|
|||||||
authTypeSigned
|
authTypeSigned
|
||||||
authTypeSignedV2
|
authTypeSignedV2
|
||||||
authTypeJWT
|
authTypeJWT
|
||||||
|
authTypeSTS
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get request authentication type.
|
// Get request authentication type.
|
||||||
@@ -104,6 +109,8 @@ func getRequestAuthType(r *http.Request) authType {
|
|||||||
return authTypeJWT
|
return authTypeJWT
|
||||||
} else if isRequestPostPolicySignatureV4(r) {
|
} else if isRequestPostPolicySignatureV4(r) {
|
||||||
return authTypePostPolicy
|
return authTypePostPolicy
|
||||||
|
} else if _, ok := r.URL.Query()["Action"]; ok {
|
||||||
|
return authTypeSTS
|
||||||
} else if _, ok := r.Header["Authorization"]; !ok {
|
} else if _, ok := r.Header["Authorization"]; !ok {
|
||||||
return authTypeAnonymous
|
return authTypeAnonymous
|
||||||
}
|
}
|
||||||
@@ -112,43 +119,140 @@ func getRequestAuthType(r *http.Request) authType {
|
|||||||
|
|
||||||
// checkAdminRequestAuthType checks whether the request is a valid signature V2 or V4 request.
|
// checkAdminRequestAuthType checks whether the request is a valid signature V2 or V4 request.
|
||||||
// It does not accept presigned or JWT or anonymous requests.
|
// It does not accept presigned or JWT or anonymous requests.
|
||||||
func checkAdminRequestAuthType(r *http.Request, region string) APIErrorCode {
|
func checkAdminRequestAuthType(ctx context.Context, r *http.Request, region string) APIErrorCode {
|
||||||
s3Err := ErrAccessDenied
|
s3Err := ErrAccessDenied
|
||||||
if _, ok := r.Header["X-Amz-Content-Sha256"]; ok && getRequestAuthType(r) == authTypeSigned && !skipContentSha256Cksum(r) { // we only support V4 (no presign) with auth. body
|
if _, ok := r.Header["X-Amz-Content-Sha256"]; ok &&
|
||||||
s3Err = isReqAuthenticated(r, region)
|
getRequestAuthType(r) == authTypeSigned && !skipContentSha256Cksum(r) {
|
||||||
|
// We only support admin credentials to access admin APIs.
|
||||||
|
|
||||||
|
var owner bool
|
||||||
|
_, owner, s3Err = getReqAccessKeyV4(r, region)
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
return s3Err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !owner {
|
||||||
|
return ErrAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
// we only support V4 (no presign) with auth body
|
||||||
|
s3Err = isReqAuthenticated(ctx, r, region)
|
||||||
}
|
}
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("requestHeaders", dumpRequest(r))
|
reqInfo := (&logger.ReqInfo{}).AppendTags("requestHeaders", dumpRequest(r))
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||||
logger.LogIf(ctx, errors.New(getAPIError(s3Err).Description))
|
logger.LogIf(ctx, errors.New(getAPIError(s3Err).Description))
|
||||||
}
|
}
|
||||||
return s3Err
|
return s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Action, bucketName, objectName string) APIErrorCode {
|
// Fetch the security token set by the client.
|
||||||
isOwner := true
|
func getSessionToken(r *http.Request) (token string) {
|
||||||
accountName := globalServerConfig.GetCredential().AccessKey
|
token = r.Header.Get("X-Amz-Security-Token")
|
||||||
|
if token != "" {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
return r.URL.Query().Get("X-Amz-Security-Token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch claims in the security token returned by the client, doesn't return
|
||||||
|
// errors - upon errors the returned claims map will be empty.
|
||||||
|
func mustGetClaimsFromToken(r *http.Request) map[string]interface{} {
|
||||||
|
claims, _ := getClaimsFromToken(r)
|
||||||
|
return claims
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch claims in the security token returned by the client.
|
||||||
|
func getClaimsFromToken(r *http.Request) (map[string]interface{}, error) {
|
||||||
|
claims := make(map[string]interface{})
|
||||||
|
token := getSessionToken(r)
|
||||||
|
if token == "" {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
stsTokenCallback := func(jwtToken *jwtgo.Token) (interface{}, error) {
|
||||||
|
// JWT token for x-amz-security-token is signed with admin
|
||||||
|
// secret key, temporary credentials become invalid if
|
||||||
|
// server admin credentials change. This is done to ensure
|
||||||
|
// that clients cannot decode the token using the temp
|
||||||
|
// secret keys and generate an entirely new claim by essentially
|
||||||
|
// hijacking the policies. We need to make sure that this is
|
||||||
|
// based an admin credential such that token cannot be decoded
|
||||||
|
// on the client side and is treated like an opaque value.
|
||||||
|
return []byte(globalServerConfig.GetCredential().SecretKey), nil
|
||||||
|
}
|
||||||
|
p := &jwtgo.Parser{
|
||||||
|
ValidMethods: []string{
|
||||||
|
jwtgo.SigningMethodHS256.Alg(),
|
||||||
|
jwtgo.SigningMethodHS512.Alg(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
jtoken, err := p.ParseWithClaims(token, jwtgo.MapClaims(claims), stsTokenCallback)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !jtoken.Valid {
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
|
v, ok := claims["accessKey"]
|
||||||
|
if !ok {
|
||||||
|
return nil, errInvalidAccessKeyID
|
||||||
|
}
|
||||||
|
if _, ok = v.(string); !ok {
|
||||||
|
return nil, errInvalidAccessKeyID
|
||||||
|
}
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch claims in the security token returned by the client and validate the token.
|
||||||
|
func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]interface{}, APIErrorCode) {
|
||||||
|
token := getSessionToken(r)
|
||||||
|
if token != "" && cred.AccessKey == "" {
|
||||||
|
return nil, ErrNoAccessKey
|
||||||
|
}
|
||||||
|
if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
claims, err := getClaimsFromToken(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, toAPIErrorCode(context.Background(), err)
|
||||||
|
}
|
||||||
|
return claims, ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check request auth type verifies the incoming http request
|
||||||
|
// - validates the request signature
|
||||||
|
// - validates the policy action if anonymous tests bucket policies if any,
|
||||||
|
// for authenticated requests validates IAM policies.
|
||||||
|
// returns APIErrorCode if any to be replied to the client.
|
||||||
|
func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Action, bucketName, objectName string) (s3Err APIErrorCode) {
|
||||||
|
var cred auth.Credentials
|
||||||
|
var owner bool
|
||||||
switch getRequestAuthType(r) {
|
switch getRequestAuthType(r) {
|
||||||
case authTypeUnknown:
|
case authTypeUnknown, authTypeStreamingSigned:
|
||||||
return ErrAccessDenied
|
return ErrAccessDenied
|
||||||
case authTypePresignedV2, authTypeSignedV2:
|
case authTypePresignedV2, authTypeSignedV2:
|
||||||
if errorCode := isReqAuthenticatedV2(r); errorCode != ErrNone {
|
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
|
||||||
return errorCode
|
return s3Err
|
||||||
}
|
}
|
||||||
|
cred, owner, s3Err = getReqAccessKeyV2(r)
|
||||||
case authTypeSigned, authTypePresigned:
|
case authTypeSigned, authTypePresigned:
|
||||||
region := globalServerConfig.GetRegion()
|
region := globalServerConfig.GetRegion()
|
||||||
switch action {
|
switch action {
|
||||||
case policy.GetBucketLocationAction, policy.ListAllMyBucketsAction:
|
case policy.GetBucketLocationAction, policy.ListAllMyBucketsAction:
|
||||||
region = ""
|
region = ""
|
||||||
}
|
}
|
||||||
|
if s3Err = isReqAuthenticated(ctx, r, region); s3Err != ErrNone {
|
||||||
if errorCode := isReqAuthenticated(r, region); errorCode != ErrNone {
|
return s3Err
|
||||||
return errorCode
|
|
||||||
}
|
}
|
||||||
default:
|
cred, owner, s3Err = getReqAccessKeyV4(r, region)
|
||||||
isOwner = false
|
}
|
||||||
accountName = ""
|
if s3Err != ErrNone {
|
||||||
|
return s3Err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, s3Err := checkClaimsFromToken(r, cred)
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
return s3Err
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocationConstraint is valid only for CreateBucketAction.
|
// LocationConstraint is valid only for CreateBucketAction.
|
||||||
@@ -174,17 +278,31 @@ func checkRequestAuthType(ctx context.Context, r *http.Request, action policy.Ac
|
|||||||
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
r.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalPolicySys.IsAllowed(policy.Args{
|
if cred.AccessKey == "" {
|
||||||
AccountName: accountName,
|
if globalPolicySys.IsAllowed(policy.Args{
|
||||||
Action: action,
|
AccountName: cred.AccessKey,
|
||||||
|
Action: action,
|
||||||
|
BucketName: bucketName,
|
||||||
|
ConditionValues: getConditionValues(r, locationConstraint),
|
||||||
|
IsOwner: false,
|
||||||
|
ObjectName: objectName,
|
||||||
|
}) {
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
return ErrAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Action: iampolicy.Action(action),
|
||||||
BucketName: bucketName,
|
BucketName: bucketName,
|
||||||
ConditionValues: getConditionValues(r, locationConstraint),
|
ConditionValues: getConditionValues(r, ""),
|
||||||
IsOwner: isOwner,
|
|
||||||
ObjectName: objectName,
|
ObjectName: objectName,
|
||||||
|
IsOwner: owner,
|
||||||
|
Claims: claims,
|
||||||
}) {
|
}) {
|
||||||
return ErrNone
|
return ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
return ErrAccessDenied
|
return ErrAccessDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +327,7 @@ func reqSignatureV4Verify(r *http.Request, region string) (s3Error APIErrorCode)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify if request has valid AWS Signature Version '4'.
|
// Verify if request has valid AWS Signature Version '4'.
|
||||||
func isReqAuthenticated(r *http.Request, region string) (s3Error APIErrorCode) {
|
func isReqAuthenticated(ctx context.Context, r *http.Request, region string) (s3Error APIErrorCode) {
|
||||||
if errCode := reqSignatureV4Verify(r, region); errCode != ErrNone {
|
if errCode := reqSignatureV4Verify(r, region); errCode != ErrNone {
|
||||||
return errCode
|
return errCode
|
||||||
}
|
}
|
||||||
@@ -244,9 +362,9 @@ func isReqAuthenticated(r *http.Request, region string) (s3Error APIErrorCode) {
|
|||||||
|
|
||||||
// Verify 'Content-Md5' and/or 'X-Amz-Content-Sha256' if present.
|
// Verify 'Content-Md5' and/or 'X-Amz-Content-Sha256' if present.
|
||||||
// The verification happens implicit during reading.
|
// The verification happens implicit during reading.
|
||||||
reader, err := hash.NewReader(r.Body, -1, hex.EncodeToString(contentMD5), hex.EncodeToString(contentSHA256))
|
reader, err := hash.NewReader(r.Body, -1, hex.EncodeToString(contentMD5), hex.EncodeToString(contentSHA256), -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return toAPIErrorCode(err)
|
return toAPIErrorCode(ctx, err)
|
||||||
}
|
}
|
||||||
r.Body = ioutil.NopCloser(reader)
|
r.Body = ioutil.NopCloser(reader)
|
||||||
return ErrNone
|
return ErrNone
|
||||||
@@ -288,12 +406,67 @@ func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
} else if aType == authTypeJWT {
|
} else if aType == authTypeJWT {
|
||||||
// Validate Authorization header if its valid for JWT request.
|
// Validate Authorization header if its valid for JWT request.
|
||||||
if !isHTTPRequestValid(r) {
|
if _, _, authErr := webRequestAuthenticate(r); authErr != nil {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.handler.ServeHTTP(w, r)
|
a.handler.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
|
} else if aType == authTypeSTS {
|
||||||
|
a.handler.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
writeErrorResponse(w, ErrSignatureVersionNotSupported, r.URL)
|
writeErrorResponse(w, ErrSignatureVersionNotSupported, r.URL, guessIsBrowserReq(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPutAllowed - check if PUT operation is allowed on the resource, this
|
||||||
|
// call verifies bucket policies and IAM policies, supports multi user
|
||||||
|
// checks etc.
|
||||||
|
func isPutAllowed(atype authType, bucketName, objectName string, r *http.Request) (s3Err APIErrorCode) {
|
||||||
|
var cred auth.Credentials
|
||||||
|
var owner bool
|
||||||
|
switch atype {
|
||||||
|
case authTypeUnknown:
|
||||||
|
return ErrAccessDenied
|
||||||
|
case authTypeSignedV2, authTypePresignedV2:
|
||||||
|
cred, owner, s3Err = getReqAccessKeyV2(r)
|
||||||
|
case authTypeStreamingSigned, authTypePresigned, authTypeSigned:
|
||||||
|
region := globalServerConfig.GetRegion()
|
||||||
|
cred, owner, s3Err = getReqAccessKeyV4(r, region)
|
||||||
|
}
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
return s3Err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, s3Err := checkClaimsFromToken(r, cred)
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
return s3Err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cred.AccessKey == "" {
|
||||||
|
if globalPolicySys.IsAllowed(policy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Action: policy.PutObjectAction,
|
||||||
|
BucketName: bucketName,
|
||||||
|
ConditionValues: getConditionValues(r, ""),
|
||||||
|
IsOwner: false,
|
||||||
|
ObjectName: objectName,
|
||||||
|
}) {
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
return ErrAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Action: policy.PutObjectAction,
|
||||||
|
BucketName: bucketName,
|
||||||
|
ConditionValues: getConditionValues(r, ""),
|
||||||
|
ObjectName: objectName,
|
||||||
|
IsOwner: owner,
|
||||||
|
Claims: claims,
|
||||||
|
}) {
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
return ErrAccessDenied
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -377,11 +378,12 @@ func TestIsReqAuthenticated(t *testing.T) {
|
|||||||
{mustNewSignedRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrNone},
|
{mustNewSignedRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrNone},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
// Validates all testcases.
|
// Validates all testcases.
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
if s3Error := isReqAuthenticated(testCase.req, globalServerConfig.GetRegion()); s3Error != testCase.s3Error {
|
if s3Error := isReqAuthenticated(ctx, testCase.req, globalServerConfig.GetRegion()); s3Error != testCase.s3Error {
|
||||||
if _, err := ioutil.ReadAll(testCase.req.Body); toAPIErrorCode(err) != testCase.s3Error {
|
if _, err := ioutil.ReadAll(testCase.req.Body); toAPIErrorCode(ctx, err) != testCase.s3Error {
|
||||||
t.Fatalf("Test %d: Unexpected S3 error: want %d - got %d (got after reading request %d)", i, testCase.s3Error, s3Error, toAPIErrorCode(err))
|
t.Fatalf("Test %d: Unexpected S3 error: want %d - got %d (got after reading request %d)", i, testCase.s3Error, s3Error, toAPIErrorCode(ctx, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,8 +415,9 @@ func TestCheckAdminRequestAuthType(t *testing.T) {
|
|||||||
{Request: mustNewPresignedV2Request("GET", "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied},
|
{Request: mustNewPresignedV2Request("GET", "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied},
|
||||||
{Request: mustNewPresignedRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied},
|
{Request: mustNewPresignedRequest("GET", "http://127.0.0.1:9000", 0, nil, t), ErrCode: ErrAccessDenied},
|
||||||
}
|
}
|
||||||
|
ctx := context.Background()
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
if s3Error := checkAdminRequestAuthType(testCase.Request, globalServerConfig.GetRegion()); s3Error != testCase.ErrCode {
|
if s3Error := checkAdminRequestAuthType(ctx, testCase.Request, globalServerConfig.GetRegion()); s3Error != testCase.ErrCode {
|
||||||
t.Errorf("Test %d: Unexpected s3error returned wanted %d, got %d", i, testCase.ErrCode, s3Error)
|
t.Errorf("Test %d: Unexpected s3error returned wanted %d, got %d", i, testCase.ErrCode, s3Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
|||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
// insert the object.
|
// insert the object.
|
||||||
objInfo, err := obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
|
objInfo, err := obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
|
||||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
mustGetPutObjReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,8 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
|||||||
// generate md5sum for the generated data.
|
// generate md5sum for the generated data.
|
||||||
// md5sum of the data to written is required as input for NewMultipartUpload.
|
// md5sum of the data to written is required as input for NewMultipartUpload.
|
||||||
metadata := make(map[string]string)
|
metadata := make(map[string]string)
|
||||||
uploadID, err = obj.NewMultipartUpload(context.Background(), bucket, object, metadata)
|
opts := ObjectOptions{}
|
||||||
|
uploadID, err = obj.NewMultipartUpload(context.Background(), bucket, object, metadata, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -122,7 +123,7 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
|||||||
md5hex = getMD5Hash([]byte(textPartData))
|
md5hex = getMD5Hash([]byte(textPartData))
|
||||||
var partInfo PartInfo
|
var partInfo PartInfo
|
||||||
partInfo, err = obj.PutObjectPart(context.Background(), bucket, object, uploadID, j,
|
partInfo, err = obj.PutObjectPart(context.Background(), bucket, object, uploadID, j,
|
||||||
mustGetHashReader(b, bytes.NewBuffer(textPartData), int64(len(textPartData)), md5hex, sha256hex))
|
mustGetPutObjReader(b, bytes.NewBuffer(textPartData), int64(len(textPartData)), md5hex, sha256hex), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -203,7 +204,7 @@ func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
|||||||
// insert the object.
|
// insert the object.
|
||||||
var objInfo ObjectInfo
|
var objInfo ObjectInfo
|
||||||
objInfo, err = obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
|
objInfo, err = obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
|
||||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
mustGetPutObjReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -218,7 +219,7 @@ func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
|||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
var buffer = new(bytes.Buffer)
|
var buffer = new(bytes.Buffer)
|
||||||
err = obj.GetObject(context.Background(), bucket, "object"+strconv.Itoa(i%10), 0, int64(objSize), buffer, "")
|
err = obj.GetObject(context.Background(), bucket, "object"+strconv.Itoa(i%10), 0, int64(objSize), buffer, "", ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(err)
|
b.Error(err)
|
||||||
}
|
}
|
||||||
@@ -303,7 +304,7 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
|||||||
for pb.Next() {
|
for pb.Next() {
|
||||||
// insert the object.
|
// insert the object.
|
||||||
objInfo, err := obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
|
objInfo, err := obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
|
||||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
mustGetPutObjReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -343,7 +344,7 @@ func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
|||||||
// insert the object.
|
// insert the object.
|
||||||
var objInfo ObjectInfo
|
var objInfo ObjectInfo
|
||||||
objInfo, err = obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
|
objInfo, err = obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
|
||||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
mustGetPutObjReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -359,7 +360,7 @@ func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
|||||||
b.RunParallel(func(pb *testing.PB) {
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
i := 0
|
i := 0
|
||||||
for pb.Next() {
|
for pb.Next() {
|
||||||
err = obj.GetObject(context.Background(), bucket, "object"+strconv.Itoa(i), 0, int64(objSize), ioutil.Discard, "")
|
err = obj.GetObject(context.Background(), bucket, "object"+strconv.Itoa(i), 0, int64(objSize), ioutil.Discard, "", ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Error(err)
|
b.Error(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -23,7 +23,7 @@ import (
|
|||||||
|
|
||||||
"github.com/minio/highwayhash"
|
"github.com/minio/highwayhash"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/sha256-simd"
|
sha256 "github.com/minio/sha256-simd"
|
||||||
"golang.org/x/crypto/blake2b"
|
"golang.org/x/crypto/blake2b"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ func (a BitrotAlgorithm) New() hash.Hash {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Available reports whether the given algorihm is available.
|
// Available reports whether the given algorithm is available.
|
||||||
func (a BitrotAlgorithm) Available() bool {
|
func (a BitrotAlgorithm) Available() bool {
|
||||||
_, ok := bitrotAlgorithms[a]
|
_, ok := bitrotAlgorithms[a]
|
||||||
return ok
|
return ok
|
||||||
@@ -138,7 +138,9 @@ func (b *bitrotReader) ReadChunk(offset int64, length int64) ([]byte, error) {
|
|||||||
if b.buf == nil {
|
if b.buf == nil {
|
||||||
b.buf = make([]byte, b.endOffset-offset)
|
b.buf = make([]byte, b.endOffset-offset)
|
||||||
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
|
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
|
||||||
logger.LogIf(context.Background(), err)
|
ctx := context.Background()
|
||||||
|
logger.GetReqInfo(ctx).AppendTags("disk", b.disk.String())
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
)
|
)
|
||||||
@@ -58,17 +59,19 @@ func validateListObjectsArgs(prefix, marker, delimiter string, maxKeys int) APIE
|
|||||||
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListObjectsV2")
|
ctx := newContext(r, w, "ListObjectsV2")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "ListObjectsV2", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,14 +81,14 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
prefix, token, startAfter, delimiter, fetchOwner, maxKeys, _, errCode := getListObjectsV2Args(urlValues)
|
prefix, token, startAfter, delimiter, fetchOwner, maxKeys, _, errCode := getListObjectsV2Args(urlValues)
|
||||||
|
|
||||||
if errCode != ErrNone {
|
if errCode != ErrNone {
|
||||||
writeErrorResponse(w, errCode, r.URL)
|
writeErrorResponse(w, errCode, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the query params before beginning to serve the request.
|
// Validate the query params before beginning to serve the request.
|
||||||
// fetch-owner is not validated since it is a boolean
|
// fetch-owner is not validated since it is a boolean
|
||||||
if s3Error := validateListObjectsArgs(prefix, token, delimiter, maxKeys); s3Error != ErrNone {
|
if s3Error := validateListObjectsArgs(prefix, token, delimiter, maxKeys); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
listObjectsV2 := objectAPI.ListObjectsV2
|
listObjectsV2 := objectAPI.ListObjectsV2
|
||||||
@@ -97,15 +100,26 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
// marshaled into S3 compatible XML header.
|
// marshaled into S3 compatible XML header.
|
||||||
listObjectsV2Info, err := listObjectsV2(ctx, bucket, prefix, token, delimiter, maxKeys, fetchOwner, startAfter)
|
listObjectsV2Info, err := listObjectsV2(ctx, bucket, prefix, token, delimiter, maxKeys, fetchOwner, startAfter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectsV2Info.Objects {
|
for i := range listObjectsV2Info.Objects {
|
||||||
if crypto.IsEncrypted(listObjectsV2Info.Objects[i].UserDefined) {
|
var actualSize int64
|
||||||
|
if listObjectsV2Info.Objects[i].IsCompressed() {
|
||||||
|
// Read the decompressed size from the meta.json.
|
||||||
|
actualSize = listObjectsV2Info.Objects[i].GetActualSize()
|
||||||
|
if actualSize < 0 {
|
||||||
|
writeErrorResponse(w, ErrInvalidDecompressedSize, r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Set the info.Size to the actualSize.
|
||||||
|
listObjectsV2Info.Objects[i].Size = actualSize
|
||||||
|
} else if crypto.IsEncrypted(listObjectsV2Info.Objects[i].UserDefined) {
|
||||||
|
listObjectsV2Info.Objects[i].ETag = getDecryptedETag(r.Header, listObjectsV2Info.Objects[i], false)
|
||||||
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].DecryptedSize()
|
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].DecryptedSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,31 +141,37 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListObjectsV1")
|
ctx := newContext(r, w, "ListObjectsV1")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "ListObjectsV1", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract all the litsObjectsV1 query params to their native values.
|
// Extract all the litsObjectsV1 query params to their native values.
|
||||||
prefix, marker, delimiter, maxKeys, _ := getListObjectsV1Args(r.URL.Query())
|
prefix, marker, delimiter, maxKeys, _, s3Error := getListObjectsV1Args(r.URL.Query())
|
||||||
|
if s3Error != ErrNone {
|
||||||
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Validate the maxKeys lowerbound. When maxKeys > 1000, S3 returns 1000 but
|
// Validate the maxKeys lowerbound. When maxKeys > 1000, S3 returns 1000 but
|
||||||
// does not throw an error.
|
// does not throw an error.
|
||||||
if maxKeys < 0 {
|
if maxKeys < 0 {
|
||||||
writeErrorResponse(w, ErrInvalidMaxKeys, r.URL)
|
writeErrorResponse(w, ErrInvalidMaxKeys, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
} // Validate all the query params before beginning to serve the request.
|
} // Validate all the query params before beginning to serve the request.
|
||||||
if s3Error := validateListObjectsArgs(prefix, marker, delimiter, maxKeys); s3Error != ErrNone {
|
if s3Error := validateListObjectsArgs(prefix, marker, delimiter, maxKeys); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
listObjects := objectAPI.ListObjects
|
listObjects := objectAPI.ListObjects
|
||||||
@@ -163,20 +183,30 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
|||||||
// marshaled into S3 compatible XML header.
|
// marshaled into S3 compatible XML header.
|
||||||
listObjectsInfo, err := listObjects(ctx, bucket, prefix, marker, delimiter, maxKeys)
|
listObjectsInfo, err := listObjects(ctx, bucket, prefix, marker, delimiter, maxKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectsInfo.Objects {
|
for i := range listObjectsInfo.Objects {
|
||||||
if crypto.IsEncrypted(listObjectsInfo.Objects[i].UserDefined) {
|
var actualSize int64
|
||||||
|
if listObjectsInfo.Objects[i].IsCompressed() {
|
||||||
|
// Read the decompressed size from the meta.json.
|
||||||
|
actualSize = listObjectsInfo.Objects[i].GetActualSize()
|
||||||
|
if actualSize < 0 {
|
||||||
|
writeErrorResponse(w, ErrInvalidDecompressedSize, r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Set the info.Size to the actualSize.
|
||||||
|
listObjectsInfo.Objects[i].Size = actualSize
|
||||||
|
} else if crypto.IsEncrypted(listObjectsInfo.Objects[i].UserDefined) {
|
||||||
|
listObjectsInfo.Objects[i].ETag = getDecryptedETag(r.Header, listObjectsInfo.Objects[i], false)
|
||||||
listObjectsInfo.Objects[i].Size, err = listObjectsInfo.Objects[i].DecryptedSize()
|
listObjectsInfo.Objects[i].Size, err = listObjectsInfo.Objects[i].DecryptedSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, maxKeys, listObjectsInfo)
|
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, maxKeys, listObjectsInfo)
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
|
|||||||
+128
-89
@@ -89,17 +89,19 @@ func initFederatorBackend(objLayer ObjectLayer) {
|
|||||||
func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetBucketLocation")
|
ctx := newContext(r, w, "GetBucketLocation")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "GetBucketLocation", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketLocationAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketLocationAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +110,7 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
|
|||||||
getBucketInfo = api.CacheAPI().GetBucketInfo
|
getBucketInfo = api.CacheAPI().GetBucketInfo
|
||||||
}
|
}
|
||||||
if _, err := getBucketInfo(ctx, bucket); err != nil {
|
if _, err := getBucketInfo(ctx, bucket); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,36 +139,42 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
|
|||||||
func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListMultipartUploads")
|
ctx := newContext(r, w, "ListMultipartUploads")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "ListMultipartUploads", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketMultipartUploadsAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketMultipartUploadsAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, _ := getBucketMultipartResources(r.URL.Query())
|
prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, _, s3Error := getBucketMultipartResources(r.URL.Query())
|
||||||
|
if s3Error != ErrNone {
|
||||||
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
if maxUploads < 0 {
|
if maxUploads < 0 {
|
||||||
writeErrorResponse(w, ErrInvalidMaxUploads, r.URL)
|
writeErrorResponse(w, ErrInvalidMaxUploads, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if keyMarker != "" {
|
if keyMarker != "" {
|
||||||
// Marker not common with prefix is not implemented.
|
// Marker not common with prefix is not implemented.
|
||||||
if !hasPrefix(keyMarker, prefix) {
|
if !hasPrefix(keyMarker, prefix) {
|
||||||
writeErrorResponse(w, ErrNotImplemented, r.URL)
|
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listMultipartsInfo, err := objectAPI.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
listMultipartsInfo, err := objectAPI.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// generate response
|
// generate response
|
||||||
@@ -184,9 +192,11 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter,
|
|||||||
func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListBuckets")
|
ctx := newContext(r, w, "ListBuckets")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "ListBuckets", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
listBuckets := objectAPI.ListBuckets
|
listBuckets := objectAPI.ListBuckets
|
||||||
@@ -196,7 +206,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.ListAllMyBucketsAction, "", ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.ListAllMyBucketsAction, "", ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// If etcd, dns federation configured list buckets from etcd.
|
// If etcd, dns federation configured list buckets from etcd.
|
||||||
@@ -204,7 +214,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
if globalDNSConfig != nil {
|
if globalDNSConfig != nil {
|
||||||
dnsBuckets, err := globalDNSConfig.List()
|
dnsBuckets, err := globalDNSConfig.List()
|
||||||
if err != nil && err != dns.ErrNoEntriesFound {
|
if err != nil && err != dns.ErrNoEntriesFound {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
bucketSet := set.NewStringSet()
|
bucketSet := set.NewStringSet()
|
||||||
@@ -223,7 +233,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
var err error
|
var err error
|
||||||
bucketsInfo, err = listBuckets(ctx)
|
bucketsInfo, err = listBuckets(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,12 +250,14 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "DeleteMultipleObjects")
|
ctx := newContext(r, w, "DeleteMultipleObjects")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "DeleteMultipleObjects", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +266,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
// In the event access is denied, a 200 response should still be returned
|
// In the event access is denied, a 200 response should still be returned
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||||
if s3Error != ErrAccessDenied {
|
if s3Error != ErrAccessDenied {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,14 +274,14 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
// Content-Length is required and should be non-zero
|
// Content-Length is required and should be non-zero
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||||
if r.ContentLength <= 0 {
|
if r.ContentLength <= 0 {
|
||||||
writeErrorResponse(w, ErrMissingContentLength, r.URL)
|
writeErrorResponse(w, ErrMissingContentLength, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content-Md5 is requied should be set
|
// Content-Md5 is requied should be set
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||||
if _, ok := r.Header["Content-Md5"]; !ok {
|
if _, ok := r.Header["Content-Md5"]; !ok {
|
||||||
writeErrorResponse(w, ErrMissingContentMD5, r.URL)
|
writeErrorResponse(w, ErrMissingContentMD5, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +297,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
// Read incoming body XML bytes.
|
// Read incoming body XML bytes.
|
||||||
if _, err := io.ReadFull(r.Body, deleteXMLBytes); err != nil {
|
if _, err := io.ReadFull(r.Body, deleteXMLBytes); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
writeErrorResponse(w, ErrInternalError, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,7 +305,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
deleteObjects := &DeleteObjectsRequest{}
|
deleteObjects := &DeleteObjectsRequest{}
|
||||||
if err := xml.Unmarshal(deleteXMLBytes, deleteObjects); err != nil {
|
if err := xml.Unmarshal(deleteXMLBytes, deleteObjects); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeErrorResponse(w, ErrMalformedXML, r.URL)
|
writeErrorResponse(w, ErrMalformedXML, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +313,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
if globalWORMEnabled {
|
if globalWORMEnabled {
|
||||||
// Not required to check whether given objects exist or not, because
|
// Not required to check whether given objects exist or not, because
|
||||||
// DeleteMultipleObject is always successful irrespective of object existence.
|
// DeleteMultipleObject is always successful irrespective of object existence.
|
||||||
writeErrorResponse(w, ErrMethodNotAllowed, r.URL)
|
writeErrorResponse(w, ErrMethodNotAllowed, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,8 +354,8 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
}
|
}
|
||||||
// Error during delete should be collected separately.
|
// Error during delete should be collected separately.
|
||||||
deleteErrors = append(deleteErrors, DeleteError{
|
deleteErrors = append(deleteErrors, DeleteError{
|
||||||
Code: errorCodeResponse[toAPIErrorCode(err)].Code,
|
Code: errorCodeResponse[toAPIErrorCode(ctx, err)].Code,
|
||||||
Message: errorCodeResponse[toAPIErrorCode(err)].Description,
|
Message: errorCodeResponse[toAPIErrorCode(ctx, err)].Description,
|
||||||
Key: object.ObjectName,
|
Key: object.ObjectName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -370,10 +382,11 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
Object: ObjectInfo{
|
Object: ObjectInfo{
|
||||||
Name: dobj.ObjectName,
|
Name: dobj.ObjectName,
|
||||||
},
|
},
|
||||||
ReqParams: extractReqParams(r),
|
ReqParams: extractReqParams(r),
|
||||||
UserAgent: r.UserAgent(),
|
RespElements: extractRespElements(w),
|
||||||
Host: host,
|
UserAgent: r.UserAgent(),
|
||||||
Port: port,
|
Host: host,
|
||||||
|
Port: port,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,9 +397,11 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "PutBucket")
|
ctx := newContext(r, w, "PutBucket")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "PutBucket", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,21 +409,21 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.CreateBucketAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.CreateBucketAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse incoming location constraint.
|
// Parse incoming location constraint.
|
||||||
location, s3Error := parseLocationConstraint(r)
|
location, s3Error := parseLocationConstraint(r)
|
||||||
if s3Error != ErrNone {
|
if s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate if location sent by the client is valid, reject
|
// Validate if location sent by the client is valid, reject
|
||||||
// requests which do not follow valid region requirements.
|
// requests which do not follow valid region requirements.
|
||||||
if !isValidLocation(location) {
|
if !isValidLocation(location) {
|
||||||
writeErrorResponse(w, ErrInvalidRegion, r.URL)
|
writeErrorResponse(w, ErrInvalidRegion, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,12 +432,12 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
if err == dns.ErrNoEntriesFound {
|
if err == dns.ErrNoEntriesFound {
|
||||||
// Proceed to creating a bucket.
|
// Proceed to creating a bucket.
|
||||||
if err = objectAPI.MakeBucketWithLocation(ctx, bucket, location); err != nil {
|
if err = objectAPI.MakeBucketWithLocation(ctx, bucket, location); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = globalDNSConfig.Put(bucket); err != nil {
|
if err = globalDNSConfig.Put(bucket); err != nil {
|
||||||
objectAPI.DeleteBucket(ctx, bucket)
|
objectAPI.DeleteBucket(ctx, bucket)
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,18 +447,18 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
writeErrorResponse(w, ErrBucketAlreadyOwnedByYou, r.URL)
|
writeErrorResponse(w, ErrBucketAlreadyOwnedByYou, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proceed to creating a bucket.
|
// Proceed to creating a bucket.
|
||||||
err := objectAPI.MakeBucketWithLocation(ctx, bucket, location)
|
err := objectAPI.MakeBucketWithLocation(ctx, bucket, location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,28 +475,37 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "PostPolicyBucket")
|
ctx := newContext(r, w, "PostPolicyBucket")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "PostPolicyBucket", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if crypto.S3KMS.IsRequested(r.Header) { // SSE-KMS is not supported
|
||||||
|
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !objectAPI.IsEncryptionSupported() && hasServerSideEncryptionHeader(r.Header) {
|
||||||
|
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket := mux.Vars(r)["bucket"]
|
bucket := mux.Vars(r)["bucket"]
|
||||||
|
|
||||||
// Require Content-Length to be set in the request
|
// Require Content-Length to be set in the request
|
||||||
size := r.ContentLength
|
size := r.ContentLength
|
||||||
if size < 0 {
|
if size < 0 {
|
||||||
writeErrorResponse(w, ErrMissingContentLength, r.URL)
|
writeErrorResponse(w, ErrMissingContentLength, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resource, err := getResource(r.URL.Path, r.Host, globalDomainName)
|
resource, err := getResource(r.URL.Path, r.Host, globalDomainName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, ErrInvalidRequest, r.URL)
|
writeErrorResponse(w, ErrInvalidRequest, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Make sure that the URL does not contain object name.
|
// Make sure that the URL does not contain object name.
|
||||||
if bucket != filepath.Clean(resource[1:]) {
|
if bucket != filepath.Clean(resource[1:]) {
|
||||||
writeErrorResponse(w, ErrMethodNotAllowed, r.URL)
|
writeErrorResponse(w, ErrMethodNotAllowed, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,7 +514,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
reader, err := r.MultipartReader()
|
reader, err := r.MultipartReader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL)
|
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,7 +522,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
form, err := reader.ReadForm(maxFormMemory)
|
form, err := reader.ReadForm(maxFormMemory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL)
|
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,13 +533,13 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
fileBody, fileName, fileSize, formValues, err := extractPostPolicyFormValues(ctx, form)
|
fileBody, fileName, fileSize, formValues, err := extractPostPolicyFormValues(ctx, form)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL)
|
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if file is provided, error out otherwise.
|
// Check if file is provided, error out otherwise.
|
||||||
if fileBody == nil {
|
if fileBody == nil {
|
||||||
writeErrorResponse(w, ErrPOSTFileRequired, r.URL)
|
writeErrorResponse(w, ErrPOSTFileRequired, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,7 +561,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
if successRedirect != "" {
|
if successRedirect != "" {
|
||||||
redirectURL, err = url.Parse(successRedirect)
|
redirectURL, err = url.Parse(successRedirect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL)
|
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -545,58 +569,67 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
// Verify policy signature.
|
// Verify policy signature.
|
||||||
apiErr := doesPolicySignatureMatch(formValues)
|
apiErr := doesPolicySignatureMatch(formValues)
|
||||||
if apiErr != ErrNone {
|
if apiErr != ErrNone {
|
||||||
writeErrorResponse(w, apiErr, r.URL)
|
writeErrorResponse(w, apiErr, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy"))
|
policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL)
|
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
postPolicyForm, err := parsePostPolicyForm(string(policyBytes))
|
// Handle policy if it is set.
|
||||||
if err != nil {
|
if len(policyBytes) > 0 {
|
||||||
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL)
|
postPolicyForm, err := parsePostPolicyForm(string(policyBytes))
|
||||||
return
|
if err != nil {
|
||||||
}
|
writeErrorResponse(w, ErrMalformedPOSTRequest, r.URL, guessIsBrowserReq(r))
|
||||||
|
|
||||||
// Make sure formValues adhere to policy restrictions.
|
|
||||||
if apiErr = checkPostPolicy(formValues, postPolicyForm); apiErr != ErrNone {
|
|
||||||
writeErrorResponse(w, apiErr, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the object size is within expected range, also the file size
|
|
||||||
// should not exceed the maximum single Put size (5 GiB)
|
|
||||||
lengthRange := postPolicyForm.Conditions.ContentLengthRange
|
|
||||||
if lengthRange.Valid {
|
|
||||||
if fileSize < lengthRange.Min {
|
|
||||||
writeErrorResponse(w, toAPIErrorCode(errDataTooSmall), r.URL)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileSize > lengthRange.Max || isMaxObjectSize(fileSize) {
|
// Make sure formValues adhere to policy restrictions.
|
||||||
writeErrorResponse(w, toAPIErrorCode(errDataTooLarge), r.URL)
|
if apiErr = checkPostPolicy(formValues, postPolicyForm); apiErr != ErrNone {
|
||||||
|
writeErrorResponse(w, apiErr, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure that the object size is within expected range, also the file size
|
||||||
|
// should not exceed the maximum single Put size (5 GiB)
|
||||||
|
lengthRange := postPolicyForm.Conditions.ContentLengthRange
|
||||||
|
if lengthRange.Valid {
|
||||||
|
if fileSize < lengthRange.Min {
|
||||||
|
writeErrorResponse(w, toAPIErrorCode(ctx, errDataTooSmall), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileSize > lengthRange.Max || isMaxObjectSize(fileSize) {
|
||||||
|
writeErrorResponse(w, toAPIErrorCode(ctx, errDataTooLarge), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract metadata to be saved from received Form.
|
// Extract metadata to be saved from received Form.
|
||||||
metadata := make(map[string]string)
|
metadata := make(map[string]string)
|
||||||
err = extractMetadataFromMap(ctx, formValues, metadata)
|
err = extractMetadataFromMap(ctx, formValues, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
writeErrorResponse(w, ErrInternalError, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hashReader, err := hash.NewReader(fileBody, fileSize, "", "")
|
hashReader, err := hash.NewReader(fileBody, fileSize, "", "", fileSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
rawReader := hashReader
|
||||||
|
pReader := NewPutObjReader(rawReader, nil, nil)
|
||||||
|
var objectEncryptionKey []byte
|
||||||
|
|
||||||
|
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||||
|
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||||
|
}
|
||||||
if objectAPI.IsEncryptionSupported() {
|
if objectAPI.IsEncryptionSupported() {
|
||||||
if hasServerSideEncryptionHeader(formValues) && !hasSuffix(object, slashSeparator) { // handle SSE-C and SSE-S3 requests
|
if hasServerSideEncryptionHeader(formValues) && !hasSuffix(object, slashSeparator) { // handle SSE-C and SSE-S3 requests
|
||||||
var reader io.Reader
|
var reader io.Reader
|
||||||
@@ -604,27 +637,28 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
if crypto.SSEC.IsRequested(formValues) {
|
if crypto.SSEC.IsRequested(formValues) {
|
||||||
key, err = ParseSSECustomerHeader(formValues)
|
key, err = ParseSSECustomerHeader(formValues)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader, err = newEncryptReader(hashReader, key, bucket, object, metadata, crypto.S3.IsRequested(formValues))
|
reader, objectEncryptionKey, err = newEncryptReader(hashReader, key, bucket, object, metadata, crypto.S3.IsRequested(formValues))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info := ObjectInfo{Size: fileSize}
|
info := ObjectInfo{Size: fileSize}
|
||||||
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "") // do not try to verify encrypted content
|
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", fileSize) // do not try to verify encrypted content
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
pReader = NewPutObjReader(rawReader, hashReader, objectEncryptionKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
objInfo, err := objectAPI.PutObject(ctx, bucket, object, hashReader, metadata)
|
objInfo, err := objectAPI.PutObject(ctx, bucket, object, pReader, metadata, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,13 +674,14 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
|
|
||||||
// Notify object created event.
|
// Notify object created event.
|
||||||
defer sendEvent(eventArgs{
|
defer sendEvent(eventArgs{
|
||||||
EventName: event.ObjectCreatedPost,
|
EventName: event.ObjectCreatedPost,
|
||||||
BucketName: objInfo.Bucket,
|
BucketName: objInfo.Bucket,
|
||||||
Object: objInfo,
|
Object: objInfo,
|
||||||
ReqParams: extractReqParams(r),
|
ReqParams: extractReqParams(r),
|
||||||
UserAgent: r.UserAgent(),
|
RespElements: extractRespElements(w),
|
||||||
Host: host,
|
UserAgent: r.UserAgent(),
|
||||||
Port: port,
|
Host: host,
|
||||||
|
Port: port,
|
||||||
})
|
})
|
||||||
|
|
||||||
if successRedirect != "" {
|
if successRedirect != "" {
|
||||||
@@ -682,6 +717,8 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "HeadBucket")
|
ctx := newContext(r, w, "HeadBucket")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "HeadBucket", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
@@ -701,7 +738,7 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
|
|||||||
getBucketInfo = api.CacheAPI().GetBucketInfo
|
getBucketInfo = api.CacheAPI().GetBucketInfo
|
||||||
}
|
}
|
||||||
if _, err := getBucketInfo(ctx, bucket); err != nil {
|
if _, err := getBucketInfo(ctx, bucket); err != nil {
|
||||||
writeErrorResponseHeadersOnly(w, toAPIErrorCode(err))
|
writeErrorResponseHeadersOnly(w, toAPIErrorCode(ctx, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,17 +749,19 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
|
|||||||
func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "DeleteBucket")
|
ctx := newContext(r, w, "DeleteBucket")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "DeleteBucket", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,7 +771,7 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
}
|
}
|
||||||
// Attempt to delete bucket.
|
// Attempt to delete bucket.
|
||||||
if err := deleteBucket(ctx, bucket); err != nil {
|
if err := deleteBucket(ctx, bucket); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,7 +783,7 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
if err := globalDNSConfig.Delete(bucket); err != nil {
|
if err := globalDNSConfig.Delete(bucket); err != nil {
|
||||||
// Deleting DNS entry failed, attempt to create the bucket again.
|
// Deleting DNS entry failed, attempt to create the bucket again.
|
||||||
objectAPI.MakeBucketWithLocation(ctx, bucket, "")
|
objectAPI.MakeBucketWithLocation(ctx, bucket, "")
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -84,7 +84,7 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
|
|||||||
// initialize httptest Recorder, this records any mutations to response writer inside the handler.
|
// initialize httptest Recorder, this records any mutations to response writer inside the handler.
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
// construct HTTP request for Get bucket location.
|
// construct HTTP request for Get bucket location.
|
||||||
req, err := newTestSignedRequestV4("GET", getBucketLocationURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey)
|
req, err := newTestSignedRequestV4("GET", getBucketLocationURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for GetBucketLocationHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for GetBucketLocationHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
|
|||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("GET", getBucketLocationURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey)
|
reqV2, err := newTestSignedRequestV2("GET", getBucketLocationURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
@@ -220,7 +220,7 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
|
|||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
// construct HTTP request for HEAD bucket.
|
// construct HTTP request for HEAD bucket.
|
||||||
req, err := newTestSignedRequestV4("HEAD", getHEADBucketURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey)
|
req, err := newTestSignedRequestV4("HEAD", getHEADBucketURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for HeadBucketHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for HeadBucketHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -235,7 +235,7 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
|
|||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("HEAD", getHEADBucketURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey)
|
reqV2, err := newTestSignedRequestV2("HEAD", getHEADBucketURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
@@ -437,7 +437,7 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
|
|||||||
|
|
||||||
// construct HTTP request for List multipart uploads endpoint.
|
// construct HTTP request for List multipart uploads endpoint.
|
||||||
u := getListMultipartUploadsURLWithParams("", testCase.bucket, testCase.prefix, testCase.keyMarker, testCase.uploadIDMarker, testCase.delimiter, testCase.maxUploads)
|
u := getListMultipartUploadsURLWithParams("", testCase.bucket, testCase.prefix, testCase.keyMarker, testCase.uploadIDMarker, testCase.delimiter, testCase.maxUploads)
|
||||||
req, gerr := newTestSignedRequestV4("GET", u, 0, nil, testCase.accessKey, testCase.secretKey)
|
req, gerr := newTestSignedRequestV4("GET", u, 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if gerr != nil {
|
if gerr != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for ListMultipartUploadsHandler: <ERROR> %v", i+1, instanceType, gerr)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for ListMultipartUploadsHandler: <ERROR> %v", i+1, instanceType, gerr)
|
||||||
}
|
}
|
||||||
@@ -454,7 +454,7 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
|
|||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
|
|
||||||
// verify response for V2 signed HTTP request.
|
// verify response for V2 signed HTTP request.
|
||||||
reqV2, err := newTestSignedRequestV2("GET", u, 0, nil, testCase.accessKey, testCase.secretKey)
|
reqV2, err := newTestSignedRequestV2("GET", u, 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -471,7 +471,7 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
|
|||||||
|
|
||||||
// construct HTTP request for List multipart uploads endpoint.
|
// construct HTTP request for List multipart uploads endpoint.
|
||||||
u := getListMultipartUploadsURLWithParams("", bucketName, "", "", "", "", "")
|
u := getListMultipartUploadsURLWithParams("", bucketName, "", "", "", "", "")
|
||||||
req, err := newTestSignedRequestV4("GET", u, 0, nil, "", "") // Generate an anonymous request.
|
req, err := newTestSignedRequestV4("GET", u, 0, nil, "", "", nil) // Generate an anonymous request.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %s: Failed to create HTTP request for ListMultipartUploadsHandler: <ERROR> %v", instanceType, err)
|
t.Fatalf("Test %s: Failed to create HTTP request for ListMultipartUploadsHandler: <ERROR> %v", instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -551,7 +551,7 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
|
|||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
req, lerr := newTestSignedRequestV4("GET", getListBucketURL(""), 0, nil, testCase.accessKey, testCase.secretKey)
|
req, lerr := newTestSignedRequestV4("GET", getListBucketURL(""), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if lerr != nil {
|
if lerr != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for ListBucketsHandler: <ERROR> %v", i+1, instanceType, lerr)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for ListBucketsHandler: <ERROR> %v", i+1, instanceType, lerr)
|
||||||
}
|
}
|
||||||
@@ -568,7 +568,7 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
|
|||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
|
|
||||||
// verify response for V2 signed HTTP request.
|
// verify response for V2 signed HTTP request.
|
||||||
reqV2, err := newTestSignedRequestV2("GET", getListBucketURL(""), 0, nil, testCase.accessKey, testCase.secretKey)
|
reqV2, err := newTestSignedRequestV2("GET", getListBucketURL(""), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
@@ -625,7 +625,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
objectName := "test-object-" + strconv.Itoa(i)
|
objectName := "test-object-" + strconv.Itoa(i)
|
||||||
// uploading the object.
|
// uploading the object.
|
||||||
_, err = obj.PutObject(context.Background(), bucketName, objectName, mustGetHashReader(t, bytes.NewBuffer(contentBytes), int64(len(contentBytes)), "", sha256sum), nil)
|
_, err = obj.PutObject(context.Background(), bucketName, objectName, mustGetPutObjReader(t, bytes.NewBuffer(contentBytes), int64(len(contentBytes)), "", sha256sum), nil, ObjectOptions{})
|
||||||
// if object upload fails stop the test.
|
// if object upload fails stop the test.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Put Object %d: Error uploading object: <ERROR> %v", i, err)
|
t.Fatalf("Put Object %d: Error uploading object: <ERROR> %v", i, err)
|
||||||
@@ -745,7 +745,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
// Generate a signed or anonymous request based on the testCase
|
// Generate a signed or anonymous request based on the testCase
|
||||||
if testCase.accessKey != "" {
|
if testCase.accessKey != "" {
|
||||||
req, err = newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", bucketName),
|
req, err = newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", bucketName),
|
||||||
int64(len(testCase.objects)), bytes.NewReader(testCase.objects), testCase.accessKey, testCase.secretKey)
|
int64(len(testCase.objects)), bytes.NewReader(testCase.objects), testCase.accessKey, testCase.secretKey, nil)
|
||||||
} else {
|
} else {
|
||||||
req, err = newTestRequest("POST", getDeleteMultipleObjectsURL("", bucketName),
|
req, err = newTestRequest("POST", getDeleteMultipleObjectsURL("", bucketName),
|
||||||
int64(len(testCase.objects)), bytes.NewReader(testCase.objects))
|
int64(len(testCase.objects)), bytes.NewReader(testCase.objects))
|
||||||
@@ -785,7 +785,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
nilBucket := "dummy-bucket"
|
nilBucket := "dummy-bucket"
|
||||||
nilObject := ""
|
nilObject := ""
|
||||||
|
|
||||||
nilReq, err := newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", nilBucket), 0, nil, "", "")
|
nilReq, err := newTestSignedRequestV4("POST", getDeleteMultipleObjectsURL("", nilBucket), 0, nil, "", "", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,28 +44,30 @@ var errNoSuchNotifications = errors.New("The specified bucket does not have buck
|
|||||||
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetBucketNotification")
|
ctx := newContext(r, w, "GetBucketNotification")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "GetBucketNotification", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucketName := vars["bucket"]
|
bucketName := vars["bucket"]
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !objAPI.IsNotificationSupported() {
|
if !objAPI.IsNotificationSupported() {
|
||||||
writeErrorResponse(w, ErrNotImplemented, r.URL)
|
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketNotificationAction, bucketName, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketNotificationAction, bucketName, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := objAPI.GetBucketInfo(ctx, bucketName)
|
_, err := objAPI.GetBucketInfo(ctx, bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,17 +76,21 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// Ignore errNoSuchNotifications to comply with AWS S3.
|
// Ignore errNoSuchNotifications to comply with AWS S3.
|
||||||
if err != errNoSuchNotifications {
|
if err != errNoSuchNotifications {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nConfig = &event.Config{}
|
nConfig = &event.Config{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If xml namespace is empty, set a default value before returning.
|
||||||
|
if nConfig.XMLNS == "" {
|
||||||
|
nConfig.XMLNS = "http://s3.amazonaws.com/doc/2006-03-01/"
|
||||||
|
}
|
||||||
|
|
||||||
notificationBytes, err := xml.Marshal(nConfig)
|
notificationBytes, err := xml.Marshal(nConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,14 +102,16 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "PutBucketNotification")
|
ctx := newContext(r, w, "PutBucketNotification")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "PutBucketNotification", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !objectAPI.IsNotificationSupported() {
|
if !objectAPI.IsNotificationSupported() {
|
||||||
writeErrorResponse(w, ErrNotImplemented, r.URL)
|
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,19 +119,19 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
bucketName := vars["bucket"]
|
bucketName := vars["bucket"]
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketNotificationAction, bucketName, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketNotificationAction, bucketName, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := objectAPI.GetBucketInfo(ctx, bucketName)
|
_, err := objectAPI.GetBucketInfo(ctx, bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutBucketNotification always needs a Content-Length.
|
// PutBucketNotification always needs a Content-Length.
|
||||||
if r.ContentLength <= 0 {
|
if r.ContentLength <= 0 {
|
||||||
writeErrorResponse(w, ErrMissingContentLength, r.URL)
|
writeErrorResponse(w, ErrMissingContentLength, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,15 +140,15 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
apiErr := ErrMalformedXML
|
apiErr := ErrMalformedXML
|
||||||
if event.IsEventError(err) {
|
if event.IsEventError(err) {
|
||||||
apiErr = toAPIErrorCode(err)
|
apiErr = toAPIErrorCode(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeErrorResponse(w, apiErr, r.URL)
|
writeErrorResponse(w, apiErr, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = saveNotificationConfig(objectAPI, bucketName, config); err != nil {
|
if err = saveNotificationConfig(ctx, objectAPI, bucketName, config); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,22 +164,28 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListenBucketNotification")
|
ctx := newContext(r, w, "ListenBucketNotification")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "ListenBucketNotification", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
// Validate if bucket exists.
|
// Validate if bucket exists.
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !objAPI.IsNotificationSupported() {
|
if !objAPI.IsNotificationSupported() {
|
||||||
writeErrorResponse(w, ErrNotImplemented, r.URL)
|
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !objAPI.IsListenBucketSupported() {
|
||||||
|
writeErrorResponse(w, ErrNotImplemented, r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucketName := vars["bucket"]
|
bucketName := vars["bucket"]
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.ListenBucketNotificationAction, bucketName, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.ListenBucketNotificationAction, bucketName, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,11 +193,11 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
|
|
||||||
var prefix string
|
var prefix string
|
||||||
if len(values["prefix"]) > 1 {
|
if len(values["prefix"]) > 1 {
|
||||||
writeErrorResponse(w, ErrFilterNamePrefix, r.URL)
|
writeErrorResponse(w, ErrFilterNamePrefix, r.URL, guessIsBrowserReq(r))
|
||||||
}
|
}
|
||||||
if len(values["prefix"]) == 1 {
|
if len(values["prefix"]) == 1 {
|
||||||
if err := event.ValidateFilterRuleValue(values["prefix"][0]); err != nil {
|
if err := event.ValidateFilterRuleValue(values["prefix"][0]); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,11 +206,11 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
|
|
||||||
var suffix string
|
var suffix string
|
||||||
if len(values["suffix"]) > 1 {
|
if len(values["suffix"]) > 1 {
|
||||||
writeErrorResponse(w, ErrFilterNameSuffix, r.URL)
|
writeErrorResponse(w, ErrFilterNameSuffix, r.URL, guessIsBrowserReq(r))
|
||||||
}
|
}
|
||||||
if len(values["suffix"]) == 1 {
|
if len(values["suffix"]) == 1 {
|
||||||
if err := event.ValidateFilterRuleValue(values["suffix"][0]); err != nil {
|
if err := event.ValidateFilterRuleValue(values["suffix"][0]); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +223,7 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
for _, s := range values["events"] {
|
for _, s := range values["events"] {
|
||||||
eventName, err := event.ParseName(s)
|
eventName, err := event.ParseName(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,19 +231,19 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := objAPI.GetBucketInfo(ctx, bucketName); err != nil {
|
if _, err := objAPI.GetBucketInfo(ctx, bucketName); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
host, err := xnet.ParseHost(r.RemoteAddr)
|
host, err := xnet.ParseHost(r.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
target, err := target.NewHTTPClientTarget(*host, w)
|
target, err := target.NewHTTPClientTarget(*host, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,8 +251,7 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
|
|
||||||
if err = globalNotificationSys.AddRemoteTarget(bucketName, target, rulesMap); err != nil {
|
if err = globalNotificationSys.AddRemoteTarget(bucketName, target, rulesMap); err != nil {
|
||||||
logger.GetReqInfo(ctx).AppendTags("target", target.ID().Name)
|
logger.GetReqInfo(ctx).AppendTags("target", target.ID().Name)
|
||||||
logger.LogIf(ctx, err)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer globalNotificationSys.RemoveRemoteTarget(bucketName, target.ID())
|
defer globalNotificationSys.RemoveRemoteTarget(bucketName, target.ID())
|
||||||
@@ -246,14 +259,13 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
|
|
||||||
thisAddr, err := xnet.ParseHost(GetLocalPeer(globalEndpoints))
|
thisAddr, err := xnet.ParseHost(GetLocalPeer(globalEndpoints))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = SaveListener(objAPI, bucketName, eventNames, pattern, target.ID(), *thisAddr); err != nil {
|
if err = SaveListener(objAPI, bucketName, eventNames, pattern, target.ID(), *thisAddr); err != nil {
|
||||||
logger.GetReqInfo(ctx).AppendTags("target", target.ID().Name)
|
logger.GetReqInfo(ctx).AppendTags("target", target.ID().Name)
|
||||||
logger.LogIf(ctx, err)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,8 +275,7 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
|
|
||||||
if err = RemoveListener(objAPI, bucketName, target.ID(), *thisAddr); err != nil {
|
if err = RemoveListener(objAPI, bucketName, target.ID(), *thisAddr); err != nil {
|
||||||
logger.GetReqInfo(ctx).AppendTags("target", target.ID().Name)
|
logger.GetReqInfo(ctx).AppendTags("target", target.ID().Name)
|
||||||
logger.LogIf(ctx, err)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ const (
|
|||||||
func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "PutBucketPolicy")
|
ctx := newContext(r, w, "PutBucketPolicy")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "PutBucketPolicy", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,43 +52,43 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
|||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if bucket exists.
|
// Check if bucket exists.
|
||||||
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error out if Content-Length is missing.
|
// Error out if Content-Length is missing.
|
||||||
// PutBucketPolicy always needs Content-Length.
|
// PutBucketPolicy always needs Content-Length.
|
||||||
if r.ContentLength <= 0 {
|
if r.ContentLength <= 0 {
|
||||||
writeErrorResponse(w, ErrMissingContentLength, r.URL)
|
writeErrorResponse(w, ErrMissingContentLength, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error out if Content-Length is beyond allowed size.
|
// Error out if Content-Length is beyond allowed size.
|
||||||
if r.ContentLength > maxBucketPolicySize {
|
if r.ContentLength > maxBucketPolicySize {
|
||||||
writeErrorResponse(w, ErrEntityTooLarge, r.URL)
|
writeErrorResponse(w, ErrEntityTooLarge, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bucketPolicy, err := policy.ParseConfig(io.LimitReader(r.Body, r.ContentLength), bucket)
|
bucketPolicy, err := policy.ParseConfig(io.LimitReader(r.Body, r.ContentLength), bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, ErrMalformedPolicy, r.URL)
|
writeErrorResponse(w, ErrMalformedPolicy, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version in policy must not be empty
|
// Version in policy must not be empty
|
||||||
if bucketPolicy.Version == "" {
|
if bucketPolicy.Version == "" {
|
||||||
writeErrorResponse(w, ErrMalformedPolicy, r.URL)
|
writeErrorResponse(w, ErrMalformedPolicy, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = objAPI.SetBucketPolicy(ctx, bucket, bucketPolicy); err != nil {
|
if err = objAPI.SetBucketPolicy(ctx, bucket, bucketPolicy); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,9 +103,11 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
|||||||
func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "DeleteBucketPolicy")
|
ctx := newContext(r, w, "DeleteBucketPolicy")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "DeleteBucketPolicy", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,18 +115,18 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
|||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.DeleteBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if bucket exists.
|
// Check if bucket exists.
|
||||||
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := objAPI.DeleteBucketPolicy(ctx, bucket); err != nil {
|
if err := objAPI.DeleteBucketPolicy(ctx, bucket); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,9 +141,11 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
|||||||
func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "GetBucketPolicy")
|
ctx := newContext(r, w, "GetBucketPolicy")
|
||||||
|
|
||||||
|
defer logger.AuditLog(w, r, "GetBucketPolicy", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,27 +153,26 @@ func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *ht
|
|||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if bucket exists.
|
// Check if bucket exists.
|
||||||
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read bucket access policy.
|
// Read bucket access policy.
|
||||||
bucketPolicy, err := objAPI.GetBucketPolicy(ctx, bucket)
|
bucketPolicy, err := objAPI.GetBucketPolicy(ctx, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
policyData, err := json.Marshal(bucketPolicy)
|
policyData, err := json.Marshal(bucketPolicy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
writeErrorResponse(w, toAPIErrorCode(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
recV4 := httptest.NewRecorder()
|
recV4 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV4, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", testCase.bucketName),
|
reqV4, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", testCase.bucketName),
|
||||||
int64(testCase.policyLen), testCase.bucketPolicyReader, testCase.accessKey, testCase.secretKey)
|
int64(testCase.policyLen), testCase.bucketPolicyReader, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -268,7 +268,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("PUT", getPutPolicyURL("", testCase.bucketName),
|
reqV2, err := newTestSignedRequestV2("PUT", getPutPolicyURL("", testCase.bucketName),
|
||||||
int64(testCase.policyLen), testCase.bucketPolicyReader, testCase.accessKey, testCase.secretKey)
|
int64(testCase.policyLen), testCase.bucketPolicyReader, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -304,7 +304,7 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
nilBucket := "dummy-bucket"
|
nilBucket := "dummy-bucket"
|
||||||
|
|
||||||
nilReq, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", nilBucket),
|
nilReq, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", nilBucket),
|
||||||
0, nil, "", "")
|
0, nil, "", "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
@@ -346,7 +346,7 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
recV4 := httptest.NewRecorder()
|
recV4 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV4, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
reqV4, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
||||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
|
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -360,7 +360,7 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
reqV2, err := newTestSignedRequestV2("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
||||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
|
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -417,7 +417,7 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
recV4 := httptest.NewRecorder()
|
recV4 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV4, err := newTestSignedRequestV4("GET", getGetPolicyURL("", testCase.bucketName),
|
reqV4, err := newTestSignedRequestV4("GET", getGetPolicyURL("", testCase.bucketName),
|
||||||
0, nil, testCase.accessKey, testCase.secretKey)
|
0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
@@ -456,7 +456,7 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("GET", getGetPolicyURL("", testCase.bucketName),
|
reqV2, err := newTestSignedRequestV2("GET", getGetPolicyURL("", testCase.bucketName),
|
||||||
0, nil, testCase.accessKey, testCase.secretKey)
|
0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -511,7 +511,7 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
nilBucket := "dummy-bucket"
|
nilBucket := "dummy-bucket"
|
||||||
|
|
||||||
nilReq, err := newTestSignedRequestV4("GET", getGetPolicyURL("", nilBucket),
|
nilReq, err := newTestSignedRequestV4("GET", getGetPolicyURL("", nilBucket),
|
||||||
0, nil, "", "")
|
0, nil, "", "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
@@ -591,7 +591,7 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
|||||||
recV4 := httptest.NewRecorder()
|
recV4 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV4, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
reqV4, err := newTestSignedRequestV4("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
||||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
|
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -641,7 +641,7 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
|||||||
recV4 := httptest.NewRecorder()
|
recV4 := httptest.NewRecorder()
|
||||||
// construct HTTP request for Delete bucket policy endpoint.
|
// construct HTTP request for Delete bucket policy endpoint.
|
||||||
reqV4, err := newTestSignedRequestV4("DELETE", getDeletePolicyURL("", testCase.bucketName),
|
reqV4, err := newTestSignedRequestV4("DELETE", getDeletePolicyURL("", testCase.bucketName),
|
||||||
0, nil, testCase.accessKey, testCase.secretKey)
|
0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -663,7 +663,7 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
|||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
reqV2, err := newTestSignedRequestV2("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
||||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
|
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -680,7 +680,7 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
|||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for Delete bucket policy endpoint.
|
// construct HTTP request for Delete bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2("DELETE", getDeletePolicyURL("", testCase.bucketName),
|
reqV2, err := newTestSignedRequestV2("DELETE", getDeletePolicyURL("", testCase.bucketName),
|
||||||
0, nil, testCase.accessKey, testCase.secretKey)
|
0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -714,7 +714,7 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
|||||||
nilBucket := "dummy-bucket"
|
nilBucket := "dummy-bucket"
|
||||||
|
|
||||||
nilReq, err := newTestSignedRequestV4("DELETE", getDeletePolicyURL("", nilBucket),
|
nilReq, err := newTestSignedRequestV4("DELETE", getDeletePolicyURL("", nilBucket),
|
||||||
0, nil, "", "")
|
0, nil, "", "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("Minio %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
|
|||||||
+10
-10
@@ -71,9 +71,13 @@ func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
|
|||||||
// Get all CA file names.
|
// Get all CA file names.
|
||||||
var caFiles []string
|
var caFiles []string
|
||||||
fis, err := readDir(certsCAsDir)
|
fis, err := readDir(certsCAsDir)
|
||||||
if err != nil {
|
if err != nil && err != errFileNotFound {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Return success if CA's directory is missing.
|
||||||
|
if err == errFileNotFound {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
for _, fi := range fis {
|
for _, fi := range fis {
|
||||||
// Skip all directories.
|
// Skip all directories.
|
||||||
if hasSuffix(fi, slashSeparator) {
|
if hasSuffix(fi, slashSeparator) {
|
||||||
@@ -150,24 +154,20 @@ func loadX509KeyPair(certFile, keyFile string) (tls.Certificate, error) {
|
|||||||
return cert, nil
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSSLConfig() (x509Certs []*x509.Certificate, rootCAs *x509.CertPool, c *certs.Certs, secureConn bool, err error) {
|
func getTLSConfig() (x509Certs []*x509.Certificate, c *certs.Certs, secureConn bool, err error) {
|
||||||
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
|
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
|
||||||
return nil, nil, nil, false, nil
|
return nil, nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if x509Certs, err = parsePublicCertFile(getPublicCertFile()); err != nil {
|
if x509Certs, err = parsePublicCertFile(getPublicCertFile()); err != nil {
|
||||||
return nil, nil, nil, false, err
|
return nil, nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err = certs.New(getPublicCertFile(), getPrivateKeyFile(), loadX509KeyPair)
|
c, err = certs.New(getPublicCertFile(), getPrivateKeyFile(), loadX509KeyPair)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, false, err
|
return nil, nil, false, err
|
||||||
}
|
|
||||||
|
|
||||||
if rootCAs, err = getRootCAs(getCADir()); err != nil {
|
|
||||||
return nil, nil, nil, false, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
secureConn = true
|
secureConn = true
|
||||||
return x509Certs, rootCAs, c, secureConn, nil
|
return x509Certs, c, secureConn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -223,7 +223,8 @@ func TestGetRootCAs(t *testing.T) {
|
|||||||
certCAsDir string
|
certCAsDir string
|
||||||
expectedErr error
|
expectedErr error
|
||||||
}{
|
}{
|
||||||
{"nonexistent-dir", errFileNotFound},
|
// ignores non-existent directories.
|
||||||
|
{"nonexistent-dir", nil},
|
||||||
// Ignores directories.
|
// Ignores directories.
|
||||||
{dir1, nil},
|
{dir1, nil},
|
||||||
// Ignore empty directory.
|
// Ignore empty directory.
|
||||||
|
|||||||
+139
-35
@@ -17,6 +17,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@@ -26,13 +27,15 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
etcd "github.com/coreos/etcd/clientv3"
|
etcd "github.com/coreos/etcd/clientv3"
|
||||||
|
dns2 "github.com/miekg/dns"
|
||||||
"github.com/minio/cli"
|
"github.com/minio/cli"
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio-go/pkg/set"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/cmd/logger/target/console"
|
||||||
|
"github.com/minio/minio/cmd/logger/target/http"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/dns"
|
"github.com/minio/minio/pkg/dns"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
"github.com/minio/minio-go/pkg/set"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check for updates and print a notification message
|
// Check for updates and print a notification message
|
||||||
@@ -49,20 +52,59 @@ func checkUpdate(mode string) {
|
|||||||
|
|
||||||
// Load logger targets based on user's configuration
|
// Load logger targets based on user's configuration
|
||||||
func loadLoggers() {
|
func loadLoggers() {
|
||||||
if globalServerConfig.Logger.Console.Enabled {
|
auditEndpoint, ok := os.LookupEnv("MINIO_AUDIT_LOGGER_HTTP_ENDPOINT")
|
||||||
// Enable console logging
|
if ok {
|
||||||
logger.AddTarget(logger.NewConsole())
|
// Enable audit HTTP logging through ENV.
|
||||||
|
logger.AddAuditTarget(http.New(auditEndpoint, NewCustomHTTPTransport()))
|
||||||
}
|
}
|
||||||
for _, l := range globalServerConfig.Logger.HTTP {
|
|
||||||
if l.Enabled {
|
loggerEndpoint, ok := os.LookupEnv("MINIO_LOGGER_HTTP_ENDPOINT")
|
||||||
// Enable http logging
|
if ok {
|
||||||
logger.AddTarget(logger.NewHTTP(l.Endpoint, NewCustomHTTPTransport()))
|
// Enable HTTP logging through ENV.
|
||||||
|
logger.AddTarget(http.New(loggerEndpoint, NewCustomHTTPTransport()))
|
||||||
|
} else {
|
||||||
|
for _, l := range globalServerConfig.Logger.HTTP {
|
||||||
|
if l.Enabled {
|
||||||
|
// Enable http logging
|
||||||
|
logger.AddTarget(http.New(l.Endpoint, NewCustomHTTPTransport()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if globalServerConfig.Logger.Console.Enabled {
|
||||||
|
// Enable console logging
|
||||||
|
logger.AddTarget(console.New())
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCommonCmdArgs(ctx *cli.Context) {
|
func handleCommonCmdArgs(ctx *cli.Context) {
|
||||||
|
|
||||||
|
// Get "json" flag from command line argument and
|
||||||
|
// enable json and quite modes if jason flag is turned on.
|
||||||
|
globalCLIContext.JSON = ctx.IsSet("json") || ctx.GlobalIsSet("json")
|
||||||
|
if globalCLIContext.JSON {
|
||||||
|
logger.EnableJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get quiet flag from command line argument.
|
||||||
|
globalCLIContext.Quiet = ctx.IsSet("quiet") || ctx.GlobalIsSet("quiet")
|
||||||
|
if globalCLIContext.Quiet {
|
||||||
|
logger.EnableQuiet()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get anonymous flag from command line argument.
|
||||||
|
globalCLIContext.Anonymous = ctx.IsSet("anonymous") || ctx.GlobalIsSet("anonymous")
|
||||||
|
if globalCLIContext.Anonymous {
|
||||||
|
logger.EnableAnonymous()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch address option
|
||||||
|
globalCLIContext.Addr = ctx.GlobalString("address")
|
||||||
|
if globalCLIContext.Addr == "" || globalCLIContext.Addr == ":"+globalMinioDefaultPort {
|
||||||
|
globalCLIContext.Addr = ctx.String("address")
|
||||||
|
}
|
||||||
|
|
||||||
var configDir string
|
var configDir string
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@@ -95,10 +137,23 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
|||||||
setConfigDir(configDirAbs)
|
setConfigDir(configDirAbs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parses the given compression exclude list `extensions` or `content-types`.
|
||||||
|
func parseCompressIncludes(includes []string) ([]string, error) {
|
||||||
|
for _, e := range includes {
|
||||||
|
if len(e) == 0 {
|
||||||
|
return nil, uiErrInvalidCompressionIncludesValue(nil).Msg("extension/mime-type (%s) cannot be empty", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return includes, nil
|
||||||
|
}
|
||||||
|
|
||||||
func handleCommonEnvVars() {
|
func handleCommonEnvVars() {
|
||||||
|
compressEnvDelimiter := ","
|
||||||
// Start profiler if env is set.
|
// Start profiler if env is set.
|
||||||
if profiler := os.Getenv("_MINIO_PROFILER"); profiler != "" {
|
if profiler := os.Getenv("_MINIO_PROFILER"); profiler != "" {
|
||||||
globalProfiler = startProfiler(profiler)
|
var err error
|
||||||
|
globalProfiler, err = startProfiler(profiler, "")
|
||||||
|
logger.FatalIf(err, "Unable to setup a profiler")
|
||||||
}
|
}
|
||||||
|
|
||||||
accessKey := os.Getenv("MINIO_ACCESS_KEY")
|
accessKey := os.Getenv("MINIO_ACCESS_KEY")
|
||||||
@@ -108,21 +163,17 @@ func handleCommonEnvVars() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(uiErrInvalidCredentials(err), "Unable to validate credentials inherited from the shell environment")
|
logger.Fatal(uiErrInvalidCredentials(err), "Unable to validate credentials inherited from the shell environment")
|
||||||
}
|
}
|
||||||
|
cred.Expiration = timeSentinel
|
||||||
|
|
||||||
// credential Envs are set globally.
|
// credential Envs are set globally.
|
||||||
globalIsEnvCreds = true
|
globalIsEnvCreds = true
|
||||||
globalActiveCred = cred
|
globalActiveCred = cred
|
||||||
}
|
}
|
||||||
|
|
||||||
// In distributed setup users need to set ENVs always.
|
|
||||||
if !globalIsEnvCreds && globalIsDistXL {
|
|
||||||
logger.Fatal(uiErrEnvCredentialsMissingServer(nil), "Unable to start distributed server mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
if browser := os.Getenv("MINIO_BROWSER"); browser != "" {
|
if browser := os.Getenv("MINIO_BROWSER"); browser != "" {
|
||||||
browserFlag, err := ParseBoolFlag(browser)
|
browserFlag, err := ParseBoolFlag(browser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(uiErrInvalidBrowserValue(nil).Msg("Unknown value `%s`", browser), "Unable to validate MINIO_BROWSER environment variable")
|
logger.Fatal(uiErrInvalidBrowserValue(nil).Msg("Unknown value `%s`", browser), "Invalid MINIO_BROWSER value in environment variable")
|
||||||
}
|
}
|
||||||
|
|
||||||
// browser Envs are set globally, this does not represent
|
// browser Envs are set globally, this does not represent
|
||||||
@@ -141,28 +192,71 @@ func handleCommonEnvVars() {
|
|||||||
etcdEndpointsEnv, ok := os.LookupEnv("MINIO_ETCD_ENDPOINTS")
|
etcdEndpointsEnv, ok := os.LookupEnv("MINIO_ETCD_ENDPOINTS")
|
||||||
if ok {
|
if ok {
|
||||||
etcdEndpoints := strings.Split(etcdEndpointsEnv, ",")
|
etcdEndpoints := strings.Split(etcdEndpointsEnv, ",")
|
||||||
|
|
||||||
|
var etcdSecure bool
|
||||||
|
for _, endpoint := range etcdEndpoints {
|
||||||
|
u, err := xnet.ParseURL(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints)
|
||||||
|
}
|
||||||
|
// If one of the endpoint is https, we will use https directly.
|
||||||
|
etcdSecure = etcdSecure || u.Scheme == "https"
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
globalEtcdClient, err = etcd.New(etcd.Config{
|
if etcdSecure {
|
||||||
Endpoints: etcdEndpoints,
|
// This is only to support client side certificate authentication
|
||||||
DialTimeout: defaultDialTimeout,
|
// https://coreos.com/etcd/docs/latest/op-guide/security.html
|
||||||
DialKeepAliveTime: defaultDialKeepAlive,
|
etcdClientCertFile, ok1 := os.LookupEnv("MINIO_ETCD_CLIENT_CERT")
|
||||||
})
|
etcdClientCertKey, ok2 := os.LookupEnv("MINIO_ETCD_CLIENT_CERT_KEY")
|
||||||
|
var getClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
|
||||||
|
if ok1 && ok2 {
|
||||||
|
getClientCertificate = func(unused *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
||||||
|
cert, terr := tls.LoadX509KeyPair(etcdClientCertFile, etcdClientCertKey)
|
||||||
|
return &cert, terr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalEtcdClient, err = etcd.New(etcd.Config{
|
||||||
|
Endpoints: etcdEndpoints,
|
||||||
|
DialTimeout: defaultDialTimeout,
|
||||||
|
DialKeepAliveTime: defaultDialKeepAlive,
|
||||||
|
TLS: &tls.Config{
|
||||||
|
RootCAs: globalRootCAs,
|
||||||
|
GetClientCertificate: getClientCertificate,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
globalEtcdClient, err = etcd.New(etcd.Config{
|
||||||
|
Endpoints: etcdEndpoints,
|
||||||
|
DialTimeout: defaultDialTimeout,
|
||||||
|
DialKeepAliveTime: defaultDialKeepAlive,
|
||||||
|
})
|
||||||
|
}
|
||||||
logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints)
|
logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
globalDomainName, globalIsEnvDomainName = os.LookupEnv("MINIO_DOMAIN")
|
globalDomainName, globalIsEnvDomainName = os.LookupEnv("MINIO_DOMAIN")
|
||||||
|
if globalDomainName != "" {
|
||||||
|
if _, ok = dns2.IsDomainName(globalDomainName); !ok {
|
||||||
|
logger.Fatal(uiErrInvalidDomainValue(nil).Msg("Unknown value `%s`", globalDomainName), "Invalid MINIO_DOMAIN value in environment variable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
minioEndpointsEnv, ok := os.LookupEnv("MINIO_PUBLIC_IPS")
|
minioEndpointsEnv, ok := os.LookupEnv("MINIO_PUBLIC_IPS")
|
||||||
if ok {
|
if ok {
|
||||||
minioEndpoints := strings.Split(minioEndpointsEnv, ",")
|
minioEndpoints := strings.Split(minioEndpointsEnv, ",")
|
||||||
globalDomainIPs = set.NewStringSet()
|
|
||||||
for i, ip := range minioEndpoints {
|
for i, ip := range minioEndpoints {
|
||||||
if net.ParseIP(ip) == nil {
|
if net.ParseIP(ip) == nil {
|
||||||
logger.FatalIf(errInvalidArgument, "Unable to initialize Minio server with invalid MINIO_PUBLIC_IPS[%d]: %s", i, ip)
|
logger.FatalIf(errInvalidArgument, "Unable to initialize Minio server with invalid MINIO_PUBLIC_IPS[%d]: %s", i, ip)
|
||||||
}
|
}
|
||||||
globalDomainIPs.Add(ip)
|
|
||||||
}
|
}
|
||||||
|
updateDomainIPs(set.CreateStringSet(minioEndpoints...))
|
||||||
|
} else {
|
||||||
|
// Add found interfaces IP address to global domain IPS,
|
||||||
|
// loopback addresses will be naturally dropped.
|
||||||
|
updateDomainIPs(localIP4)
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalDomainName != "" && !globalDomainIPs.IsEmpty() && globalEtcdClient != nil {
|
if globalDomainName != "" && !globalDomainIPs.IsEmpty() && globalEtcdClient != nil {
|
||||||
var err error
|
var err error
|
||||||
globalDNSConfig, err = dns.NewCoreDNS(globalDomainName, globalDomainIPs, globalMinioPort, globalEtcdClient)
|
globalDNSConfig, err = dns.NewCoreDNS(globalDomainName, globalDomainIPs, globalMinioPort, globalEtcdClient)
|
||||||
@@ -243,7 +337,7 @@ func handleCommonEnvVars() {
|
|||||||
if worm := os.Getenv("MINIO_WORM"); worm != "" {
|
if worm := os.Getenv("MINIO_WORM"); worm != "" {
|
||||||
wormFlag, err := ParseBoolFlag(worm)
|
wormFlag, err := ParseBoolFlag(worm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(uiErrInvalidWormValue(nil).Msg("Unknown value `%s`", worm), "Unable to validate MINIO_WORM environment variable")
|
logger.Fatal(uiErrInvalidWormValue(nil).Msg("Unknown value `%s`", worm), "Invalid MINIO_WORM value in environment variable")
|
||||||
}
|
}
|
||||||
|
|
||||||
// worm Envs are set globally, this does not represent
|
// worm Envs are set globally, this does not represent
|
||||||
@@ -252,17 +346,27 @@ func handleCommonEnvVars() {
|
|||||||
globalWORMEnabled = bool(wormFlag)
|
globalWORMEnabled = bool(wormFlag)
|
||||||
}
|
}
|
||||||
|
|
||||||
kmsConf, err := crypto.NewVaultConfig()
|
if compress := os.Getenv("MINIO_COMPRESS"); compress != "" {
|
||||||
if err != nil {
|
globalIsCompressionEnabled = strings.EqualFold(compress, "true")
|
||||||
logger.Fatal(err, "Unable to initialize hashicorp vault")
|
|
||||||
}
|
}
|
||||||
if kmsConf.Vault.Endpoint != "" {
|
|
||||||
kms, err := crypto.NewVault(kmsConf)
|
compressExtensions := os.Getenv("MINIO_COMPRESS_EXTENSIONS")
|
||||||
if err != nil {
|
compressMimeTypes := os.Getenv("MINIO_COMPRESS_MIMETYPES")
|
||||||
logger.Fatal(err, "Unable to initialize KMS")
|
if compressExtensions != "" || compressMimeTypes != "" {
|
||||||
|
globalIsEnvCompression = true
|
||||||
|
if compressExtensions != "" {
|
||||||
|
extensions, err := parseCompressIncludes(strings.Split(compressExtensions, compressEnvDelimiter))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err, "Invalid MINIO_COMPRESS_EXTENSIONS value (`%s`)", extensions)
|
||||||
|
}
|
||||||
|
globalCompressExtensions = extensions
|
||||||
|
}
|
||||||
|
if compressMimeTypes != "" {
|
||||||
|
contenttypes, err := parseCompressIncludes(strings.Split(compressMimeTypes, compressEnvDelimiter))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err, "Invalid MINIO_COMPRESS_MIMETYPES value (`%s`)", contenttypes)
|
||||||
|
}
|
||||||
|
globalCompressMimeTypes = contenttypes
|
||||||
}
|
}
|
||||||
globalKMS = kms
|
|
||||||
globalKMSKeyID = kmsConf.Vault.Key.Name
|
|
||||||
globalKMSConfig = kmsConf
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
etcd "github.com/coreos/etcd/clientv3"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errConfigNotFound = errors.New("config file not found")
|
||||||
|
|
||||||
|
func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) ([]byte, error) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
// Read entire content by setting size to -1
|
||||||
|
if err := objAPI.GetObject(ctx, minioMetaBucket, configFile, 0, -1, &buffer, "", ObjectOptions{}); err != nil {
|
||||||
|
// Treat object not found as config not found.
|
||||||
|
if isErrObjectNotFound(err) {
|
||||||
|
return nil, errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return config not found on empty content.
|
||||||
|
if buffer.Len() == 0 {
|
||||||
|
return nil, errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteConfigEtcd(ctx context.Context, client *etcd.Client, configFile string) error {
|
||||||
|
_, err := client.Delete(ctx, configFile)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteConfig(ctx context.Context, objAPI ObjectLayer, configFile string) error {
|
||||||
|
return objAPI.DeleteObject(ctx, minioMetaBucket, configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveConfigEtcd(ctx context.Context, client *etcd.Client, configFile string, data []byte) error {
|
||||||
|
_, err := client.Put(ctx, configFile, string(data))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveConfig(ctx context.Context, objAPI ObjectLayer, configFile string, data []byte) error {
|
||||||
|
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data), int64(len(data)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = objAPI.PutObject(ctx, minioMetaBucket, configFile, NewPutObjReader(hashReader, nil, nil), nil, ObjectOptions{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfigEtcd(ctx context.Context, client *etcd.Client, configFile string) ([]byte, error) {
|
||||||
|
resp, err := client.Get(ctx, configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.Count == 0 {
|
||||||
|
return nil, errConfigNotFound
|
||||||
|
}
|
||||||
|
for _, ev := range resp.Kvs {
|
||||||
|
if string(ev.Key) == configFile {
|
||||||
|
return ev.Value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchConfig - watches for changes on `configFile` on etcd and loads them.
|
||||||
|
func watchConfig(objAPI ObjectLayer, configFile string, loadCfgFn func(ObjectLayer) error) {
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
for watchResp := range globalEtcdClient.Watch(context.Background(), configFile) {
|
||||||
|
for _, event := range watchResp.Events {
|
||||||
|
if event.IsModify() || event.IsCreate() {
|
||||||
|
loadCfgFn(objAPI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkConfigEtcd(ctx context.Context, client *etcd.Client, configFile string) error {
|
||||||
|
resp, err := globalEtcdClient.Get(ctx, configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.Count == 0 {
|
||||||
|
return errConfigNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkConfig(ctx context.Context, objAPI ObjectLayer, configFile string) error {
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
return checkConfigEtcd(ctx, globalEtcdClient, configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := objAPI.GetObjectInfo(ctx, minioMetaBucket, configFile, ObjectOptions{}); err != nil {
|
||||||
|
// Treat object not found as config not found.
|
||||||
|
if isErrObjectNotFound(err) {
|
||||||
|
return errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+261
-62
@@ -20,16 +20,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/event/target"
|
"github.com/minio/minio/pkg/event/target"
|
||||||
|
"github.com/minio/minio/pkg/iam/policy"
|
||||||
|
"github.com/minio/minio/pkg/iam/validator"
|
||||||
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Steps to move from version N to version N+1
|
// Steps to move from version N to version N+1
|
||||||
@@ -41,9 +43,9 @@ import (
|
|||||||
// 6. Make changes in config-current_test.go for any test change
|
// 6. Make changes in config-current_test.go for any test change
|
||||||
|
|
||||||
// Config version
|
// Config version
|
||||||
const serverConfigVersion = "28"
|
const serverConfigVersion = "33"
|
||||||
|
|
||||||
type serverConfig = serverConfigV28
|
type serverConfig = serverConfigV33
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// globalServerConfig server config.
|
// globalServerConfig server config.
|
||||||
@@ -64,6 +66,12 @@ func (s *serverConfig) SetRegion(region string) {
|
|||||||
|
|
||||||
// GetRegion get current region.
|
// GetRegion get current region.
|
||||||
func (s *serverConfig) GetRegion() string {
|
func (s *serverConfig) GetRegion() string {
|
||||||
|
if globalIsEnvRegion {
|
||||||
|
return globalServerRegion
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return s.Region
|
return s.Region
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,12 +99,6 @@ func (s *serverConfig) GetCredential() auth.Credentials {
|
|||||||
return s.Credential
|
return s.Credential
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBrowser set if browser is enabled.
|
|
||||||
func (s *serverConfig) SetBrowser(b bool) {
|
|
||||||
// Set the new value.
|
|
||||||
s.Browser = BoolFlag(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWorm set if worm is enabled.
|
// SetWorm set if worm is enabled.
|
||||||
func (s *serverConfig) SetWorm(b bool) {
|
func (s *serverConfig) SetWorm(b bool) {
|
||||||
// Set the new value.
|
// Set the new value.
|
||||||
@@ -111,16 +113,23 @@ func (s *serverConfig) SetStorageClass(standardClass, rrsClass storageClass) {
|
|||||||
// GetStorageClass reads storage class fields from current config.
|
// GetStorageClass reads storage class fields from current config.
|
||||||
// It returns the standard and reduced redundancy storage class struct
|
// It returns the standard and reduced redundancy storage class struct
|
||||||
func (s *serverConfig) GetStorageClass() (storageClass, storageClass) {
|
func (s *serverConfig) GetStorageClass() (storageClass, storageClass) {
|
||||||
|
if globalIsStorageClass {
|
||||||
|
return globalStandardStorageClass, globalRRStorageClass
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
return storageClass{}, storageClass{}
|
||||||
|
}
|
||||||
return s.StorageClass.Standard, s.StorageClass.RRS
|
return s.StorageClass.Standard, s.StorageClass.RRS
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBrowser get current credentials.
|
|
||||||
func (s *serverConfig) GetBrowser() bool {
|
|
||||||
return bool(s.Browser)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWorm get current credentials.
|
// GetWorm get current credentials.
|
||||||
func (s *serverConfig) GetWorm() bool {
|
func (s *serverConfig) GetWorm() bool {
|
||||||
|
if globalIsEnvWORM {
|
||||||
|
return globalWORMEnabled
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return bool(s.Worm)
|
return bool(s.Worm)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,10 +143,24 @@ func (s *serverConfig) SetCacheConfig(drives, exclude []string, expiry int, maxu
|
|||||||
|
|
||||||
// GetCacheConfig gets the current cache config
|
// GetCacheConfig gets the current cache config
|
||||||
func (s *serverConfig) GetCacheConfig() CacheConfig {
|
func (s *serverConfig) GetCacheConfig() CacheConfig {
|
||||||
|
if globalIsDiskCacheEnabled {
|
||||||
|
return CacheConfig{
|
||||||
|
Drives: globalCacheDrives,
|
||||||
|
Exclude: globalCacheExcludes,
|
||||||
|
Expiry: globalCacheExpiry,
|
||||||
|
MaxUse: globalCacheMaxUse,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
return CacheConfig{}
|
||||||
|
}
|
||||||
return s.Cache
|
return s.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serverConfig) Validate() error {
|
func (s *serverConfig) Validate() error {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if s.Version != serverConfigVersion {
|
if s.Version != serverConfigVersion {
|
||||||
return fmt.Errorf("configuration version mismatch. Expected: ‘%s’, Got: ‘%s’", serverConfigVersion, s.Version)
|
return fmt.Errorf("configuration version mismatch. Expected: ‘%s’, Got: ‘%s’", serverConfigVersion, s.Version)
|
||||||
}
|
}
|
||||||
@@ -150,81 +173,88 @@ func (s *serverConfig) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Region: nothing to validate
|
// Region: nothing to validate
|
||||||
// Browser, Worm, Cache and StorageClass values are already validated during json unmarshal
|
// Worm, Cache and StorageClass values are already validated during json unmarshal
|
||||||
|
|
||||||
if s.Domain != "" {
|
|
||||||
if _, ok := dns.IsDomainName(s.Domain); !ok {
|
|
||||||
return errors.New("invalid domain name")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range s.Notify.AMQP {
|
for _, v := range s.Notify.AMQP {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("amqp: %s", err.Error())
|
return fmt.Errorf("amqp: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.Elasticsearch {
|
for _, v := range s.Notify.Elasticsearch {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("elasticsearch: %s", err.Error())
|
return fmt.Errorf("elasticsearch: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.Kafka {
|
for _, v := range s.Notify.Kafka {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("kafka: %s", err.Error())
|
return fmt.Errorf("kafka: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.MQTT {
|
for _, v := range s.Notify.MQTT {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("mqtt: %s", err.Error())
|
return fmt.Errorf("mqtt: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.MySQL {
|
for _, v := range s.Notify.MySQL {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("mysql: %s", err.Error())
|
return fmt.Errorf("mysql: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.NATS {
|
for _, v := range s.Notify.NATS {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("nats: %s", err.Error())
|
return fmt.Errorf("nats: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range s.Notify.NSQ {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("nsq: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.PostgreSQL {
|
for _, v := range s.Notify.PostgreSQL {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("postgreSQL: %s", err.Error())
|
return fmt.Errorf("postgreSQL: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.Redis {
|
for _, v := range s.Notify.Redis {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("redis: %s", err.Error())
|
return fmt.Errorf("redis: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range s.Notify.Webhook {
|
for _, v := range s.Notify.Webhook {
|
||||||
if err := v.Validate(); err != nil {
|
if err := v.Validate(); err != nil {
|
||||||
return fmt.Errorf("webhook: %s", err.Error())
|
return fmt.Errorf("webhook: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetCompressionConfig sets the current compression config
|
||||||
|
func (s *serverConfig) SetCompressionConfig(extensions []string, mimeTypes []string) {
|
||||||
|
s.Compression.Extensions = extensions
|
||||||
|
s.Compression.MimeTypes = mimeTypes
|
||||||
|
s.Compression.Enabled = globalIsCompressionEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCompressionConfig gets the current compression config
|
||||||
|
func (s *serverConfig) GetCompressionConfig() compressionConfig {
|
||||||
|
return s.Compression
|
||||||
|
}
|
||||||
|
|
||||||
func (s *serverConfig) loadFromEnvs() {
|
func (s *serverConfig) loadFromEnvs() {
|
||||||
// If env is set override the credentials from config file.
|
// If env is set override the credentials from config file.
|
||||||
if globalIsEnvCreds {
|
if globalIsEnvCreds {
|
||||||
s.SetCredential(globalActiveCred)
|
s.SetCredential(globalActiveCred)
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalIsEnvBrowser {
|
|
||||||
s.SetBrowser(globalIsBrowserEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsEnvWORM {
|
if globalIsEnvWORM {
|
||||||
s.SetWorm(globalWORMEnabled)
|
s.SetWorm(globalWORMEnabled)
|
||||||
}
|
}
|
||||||
@@ -233,10 +263,6 @@ func (s *serverConfig) loadFromEnvs() {
|
|||||||
s.SetRegion(globalServerRegion)
|
s.SetRegion(globalServerRegion)
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalIsEnvDomainName {
|
|
||||||
s.Domain = globalDomainName
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsStorageClass {
|
if globalIsStorageClass {
|
||||||
s.SetStorageClass(globalStandardStorageClass, globalRRStorageClass)
|
s.SetStorageClass(globalStandardStorageClass, globalRRStorageClass)
|
||||||
}
|
}
|
||||||
@@ -245,9 +271,134 @@ func (s *serverConfig) loadFromEnvs() {
|
|||||||
s.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
|
s.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalKMS != nil {
|
if err := Environment.LookupKMSConfig(s.KMS); err != nil {
|
||||||
s.KMS = globalKMSConfig
|
logger.FatalIf(err, "Unable to setup the KMS")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if globalIsEnvCompression {
|
||||||
|
s.SetCompressionConfig(globalCompressExtensions, globalCompressMimeTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if jwksURL, ok := os.LookupEnv("MINIO_IAM_JWKS_URL"); ok {
|
||||||
|
if u, err := xnet.ParseURL(jwksURL); err == nil {
|
||||||
|
s.OpenID.JWKS.URL = u
|
||||||
|
s.OpenID.JWKS.PopulatePublicKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opaURL, ok := os.LookupEnv("MINIO_IAM_OPA_URL"); ok {
|
||||||
|
if u, err := xnet.ParseURL(opaURL); err == nil {
|
||||||
|
s.Policy.OPA.URL = u
|
||||||
|
s.Policy.OPA.AuthToken = os.Getenv("MINIO_IAM_OPA_AUTHTOKEN")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNotificationTargets tries to establish connections to all notification
|
||||||
|
// targets when enabled. This is a good way to make sure all configurations
|
||||||
|
// set by the user can work.
|
||||||
|
func (s *serverConfig) TestNotificationTargets() error {
|
||||||
|
for k, v := range s.Notify.AMQP {
|
||||||
|
if !v.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t, err := target.NewAMQPTarget(k, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("amqp(%s): %s", k, err.Error())
|
||||||
|
}
|
||||||
|
t.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range s.Notify.Elasticsearch {
|
||||||
|
if !v.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t, err := target.NewElasticsearchTarget(k, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("elasticsearch(%s): %s", k, err.Error())
|
||||||
|
}
|
||||||
|
t.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range s.Notify.Kafka {
|
||||||
|
if !v.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t, err := target.NewKafkaTarget(k, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("kafka(%s): %s", k, err.Error())
|
||||||
|
}
|
||||||
|
t.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range s.Notify.MQTT {
|
||||||
|
if !v.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t, err := target.NewMQTTTarget(k, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("mqtt(%s): %s", k, err.Error())
|
||||||
|
}
|
||||||
|
t.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range s.Notify.MySQL {
|
||||||
|
if !v.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t, err := target.NewMySQLTarget(k, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("mysql(%s): %s", k, err.Error())
|
||||||
|
}
|
||||||
|
t.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range s.Notify.NATS {
|
||||||
|
if !v.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t, err := target.NewNATSTarget(k, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nats(%s): %s", k, err.Error())
|
||||||
|
}
|
||||||
|
t.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range s.Notify.NSQ {
|
||||||
|
if !v.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t, err := target.NewNSQTarget(k, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nsq(%s): %s", k, err.Error())
|
||||||
|
}
|
||||||
|
t.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range s.Notify.PostgreSQL {
|
||||||
|
if !v.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t, err := target.NewPostgreSQLTarget(k, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("postgreSQL(%s): %s", k, err.Error())
|
||||||
|
}
|
||||||
|
t.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range s.Notify.Redis {
|
||||||
|
if !v.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t, err := target.NewRedisTarget(k, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("redis(%s): %s", k, err.Error())
|
||||||
|
}
|
||||||
|
t.Close()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the string describing a difference with the given
|
// Returns the string describing a difference with the given
|
||||||
@@ -261,18 +412,18 @@ func (s *serverConfig) ConfigDiff(t *serverConfig) string {
|
|||||||
return "Credential configuration differs"
|
return "Credential configuration differs"
|
||||||
case s.Region != t.Region:
|
case s.Region != t.Region:
|
||||||
return "Region configuration differs"
|
return "Region configuration differs"
|
||||||
case s.Browser != t.Browser:
|
|
||||||
return "Browser configuration differs"
|
|
||||||
case s.Domain != t.Domain:
|
|
||||||
return "Domain configuration differs"
|
|
||||||
case s.StorageClass != t.StorageClass:
|
case s.StorageClass != t.StorageClass:
|
||||||
return "StorageClass configuration differs"
|
return "StorageClass configuration differs"
|
||||||
case !reflect.DeepEqual(s.Cache, t.Cache):
|
case !reflect.DeepEqual(s.Cache, t.Cache):
|
||||||
return "Cache configuration differs"
|
return "Cache configuration differs"
|
||||||
|
case !reflect.DeepEqual(s.Compression, t.Compression):
|
||||||
|
return "Compression configuration differs"
|
||||||
case !reflect.DeepEqual(s.Notify.AMQP, t.Notify.AMQP):
|
case !reflect.DeepEqual(s.Notify.AMQP, t.Notify.AMQP):
|
||||||
return "AMQP Notification configuration differs"
|
return "AMQP Notification configuration differs"
|
||||||
case !reflect.DeepEqual(s.Notify.NATS, t.Notify.NATS):
|
case !reflect.DeepEqual(s.Notify.NATS, t.Notify.NATS):
|
||||||
return "NATS Notification configuration differs"
|
return "NATS Notification configuration differs"
|
||||||
|
case !reflect.DeepEqual(s.Notify.NSQ, t.Notify.NSQ):
|
||||||
|
return "NSQ Notification configuration differs"
|
||||||
case !reflect.DeepEqual(s.Notify.Elasticsearch, t.Notify.Elasticsearch):
|
case !reflect.DeepEqual(s.Notify.Elasticsearch, t.Notify.Elasticsearch):
|
||||||
return "ElasticSearch Notification configuration differs"
|
return "ElasticSearch Notification configuration differs"
|
||||||
case !reflect.DeepEqual(s.Notify.Redis, t.Notify.Redis):
|
case !reflect.DeepEqual(s.Notify.Redis, t.Notify.Redis):
|
||||||
@@ -308,7 +459,6 @@ func newServerConfig() *serverConfig {
|
|||||||
Version: serverConfigVersion,
|
Version: serverConfigVersion,
|
||||||
Credential: cred,
|
Credential: cred,
|
||||||
Region: globalMinioDefaultRegion,
|
Region: globalMinioDefaultRegion,
|
||||||
Browser: true,
|
|
||||||
StorageClass: storageClassConfig{
|
StorageClass: storageClassConfig{
|
||||||
Standard: storageClass{},
|
Standard: storageClass{},
|
||||||
RRS: storageClass{},
|
RRS: storageClass{},
|
||||||
@@ -321,6 +471,11 @@ func newServerConfig() *serverConfig {
|
|||||||
},
|
},
|
||||||
KMS: crypto.KMSConfig{},
|
KMS: crypto.KMSConfig{},
|
||||||
Notify: notifier{},
|
Notify: notifier{},
|
||||||
|
Compression: compressionConfig{
|
||||||
|
Enabled: false,
|
||||||
|
Extensions: globalCompressExtensions,
|
||||||
|
MimeTypes: globalCompressMimeTypes,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure to initialize notification configs.
|
// Make sure to initialize notification configs.
|
||||||
@@ -334,6 +489,8 @@ func newServerConfig() *serverConfig {
|
|||||||
srvCfg.Notify.Redis["1"] = target.RedisArgs{}
|
srvCfg.Notify.Redis["1"] = target.RedisArgs{}
|
||||||
srvCfg.Notify.NATS = make(map[string]target.NATSArgs)
|
srvCfg.Notify.NATS = make(map[string]target.NATSArgs)
|
||||||
srvCfg.Notify.NATS["1"] = target.NATSArgs{}
|
srvCfg.Notify.NATS["1"] = target.NATSArgs{}
|
||||||
|
srvCfg.Notify.NSQ = make(map[string]target.NSQArgs)
|
||||||
|
srvCfg.Notify.NSQ["1"] = target.NSQArgs{}
|
||||||
srvCfg.Notify.PostgreSQL = make(map[string]target.PostgreSQLArgs)
|
srvCfg.Notify.PostgreSQL = make(map[string]target.PostgreSQLArgs)
|
||||||
srvCfg.Notify.PostgreSQL["1"] = target.PostgreSQLArgs{}
|
srvCfg.Notify.PostgreSQL["1"] = target.PostgreSQLArgs{}
|
||||||
srvCfg.Notify.MySQL = make(map[string]target.MySQLArgs)
|
srvCfg.Notify.MySQL = make(map[string]target.MySQLArgs)
|
||||||
@@ -361,18 +518,12 @@ func (s *serverConfig) loadToCachedConfigs() {
|
|||||||
if !globalIsEnvCreds {
|
if !globalIsEnvCreds {
|
||||||
globalActiveCred = s.GetCredential()
|
globalActiveCred = s.GetCredential()
|
||||||
}
|
}
|
||||||
if !globalIsEnvBrowser {
|
|
||||||
globalIsBrowserEnabled = s.GetBrowser()
|
|
||||||
}
|
|
||||||
if !globalIsEnvWORM {
|
if !globalIsEnvWORM {
|
||||||
globalWORMEnabled = s.GetWorm()
|
globalWORMEnabled = s.GetWorm()
|
||||||
}
|
}
|
||||||
if !globalIsEnvRegion {
|
if !globalIsEnvRegion {
|
||||||
globalServerRegion = s.GetRegion()
|
globalServerRegion = s.GetRegion()
|
||||||
}
|
}
|
||||||
if !globalIsEnvDomainName {
|
|
||||||
globalDomainName = s.Domain
|
|
||||||
}
|
|
||||||
if !globalIsStorageClass {
|
if !globalIsStorageClass {
|
||||||
globalStandardStorageClass, globalRRStorageClass = s.GetStorageClass()
|
globalStandardStorageClass, globalRRStorageClass = s.GetStorageClass()
|
||||||
}
|
}
|
||||||
@@ -383,18 +534,36 @@ func (s *serverConfig) loadToCachedConfigs() {
|
|||||||
globalCacheExpiry = cacheConf.Expiry
|
globalCacheExpiry = cacheConf.Expiry
|
||||||
globalCacheMaxUse = cacheConf.MaxUse
|
globalCacheMaxUse = cacheConf.MaxUse
|
||||||
}
|
}
|
||||||
if globalKMS == nil {
|
if err := Environment.LookupKMSConfig(s.KMS); err != nil {
|
||||||
globalKMSConfig = s.KMS
|
logger.FatalIf(err, "Unable to setup the KMS")
|
||||||
if kms, err := crypto.NewVault(globalKMSConfig); err == nil {
|
}
|
||||||
globalKMS = kms
|
|
||||||
globalKMSKeyID = globalKMSConfig.Vault.Key.Name
|
if !globalIsCompressionEnabled {
|
||||||
|
compressionConf := s.GetCompressionConfig()
|
||||||
|
globalCompressExtensions = compressionConf.Extensions
|
||||||
|
globalCompressMimeTypes = compressionConf.MimeTypes
|
||||||
|
globalIsCompressionEnabled = compressionConf.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIAMValidators == nil {
|
||||||
|
globalIAMValidators = getAuthValidators(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalPolicyOPA == nil {
|
||||||
|
if s.Policy.OPA.URL != nil && s.Policy.OPA.URL.String() != "" {
|
||||||
|
globalPolicyOPA = iampolicy.NewOpa(iampolicy.OpaArgs{
|
||||||
|
URL: s.Policy.OPA.URL,
|
||||||
|
AuthToken: s.Policy.OPA.AuthToken,
|
||||||
|
Transport: NewCustomHTTPTransport(),
|
||||||
|
CloseRespFn: CloseResponse,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newConfig - initialize a new server config, saves env parameters if
|
// newSrvConfig - initialize a new server config, saves env parameters if
|
||||||
// found, otherwise use default parameters
|
// found, otherwise use default parameters
|
||||||
func newConfig(objAPI ObjectLayer) error {
|
func newSrvConfig(objAPI ObjectLayer) error {
|
||||||
// Initialize server config.
|
// Initialize server config.
|
||||||
srvCfg := newServerConfig()
|
srvCfg := newServerConfig()
|
||||||
|
|
||||||
@@ -410,7 +579,7 @@ func newConfig(objAPI ObjectLayer) error {
|
|||||||
globalServerConfigMu.Unlock()
|
globalServerConfigMu.Unlock()
|
||||||
|
|
||||||
// Save config into file.
|
// Save config into file.
|
||||||
return saveServerConfig(objAPI, globalServerConfig)
|
return saveServerConfig(context.Background(), objAPI, globalServerConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getValidConfig - returns valid server configuration
|
// getValidConfig - returns valid server configuration
|
||||||
@@ -445,6 +614,20 @@ func loadConfig(objAPI ObjectLayer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getAuthValidators - returns ValidatorList which contains
|
||||||
|
// enabled providers in server config.
|
||||||
|
// A new authentication provider is added like below
|
||||||
|
// * Add a new provider in pkg/iam/validator package.
|
||||||
|
func getAuthValidators(config *serverConfig) *validator.Validators {
|
||||||
|
validators := validator.NewValidators()
|
||||||
|
|
||||||
|
if config.OpenID.JWKS.URL != nil {
|
||||||
|
validators.Add(validator.NewJWT(config.OpenID.JWKS))
|
||||||
|
}
|
||||||
|
|
||||||
|
return validators
|
||||||
|
}
|
||||||
|
|
||||||
// getNotificationTargets - returns TargetList which contains enabled targets in serverConfig.
|
// getNotificationTargets - returns TargetList which contains enabled targets in serverConfig.
|
||||||
// A new notification target is added like below
|
// A new notification target is added like below
|
||||||
// * Add a new target in pkg/event/target package.
|
// * Add a new target in pkg/event/target package.
|
||||||
@@ -452,7 +635,9 @@ func loadConfig(objAPI ObjectLayer) error {
|
|||||||
// * Handle the configuration in this function to create/add into TargetList.
|
// * Handle the configuration in this function to create/add into TargetList.
|
||||||
func getNotificationTargets(config *serverConfig) *event.TargetList {
|
func getNotificationTargets(config *serverConfig) *event.TargetList {
|
||||||
targetList := event.NewTargetList()
|
targetList := event.NewTargetList()
|
||||||
|
if config == nil {
|
||||||
|
return targetList
|
||||||
|
}
|
||||||
for id, args := range config.Notify.AMQP {
|
for id, args := range config.Notify.AMQP {
|
||||||
if args.Enable {
|
if args.Enable {
|
||||||
newTarget, err := target.NewAMQPTarget(id, args)
|
newTarget, err := target.NewAMQPTarget(id, args)
|
||||||
@@ -539,6 +724,20 @@ func getNotificationTargets(config *serverConfig) *event.TargetList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for id, args := range config.Notify.NSQ {
|
||||||
|
if args.Enable {
|
||||||
|
newTarget, err := target.NewNSQTarget(id, args)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for id, args := range config.Notify.PostgreSQL {
|
for id, args := range config.Notify.PostgreSQL {
|
||||||
if args.Enable {
|
if args.Enable {
|
||||||
newTarget, err := target.NewPostgreSQLTarget(id, args)
|
newTarget, err := target.NewPostgreSQLTarget(id, args)
|
||||||
|
|||||||
+53
-30
@@ -17,6 +17,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -51,7 +52,7 @@ func TestServerConfig(t *testing.T) {
|
|||||||
t.Errorf("Expecting version %s found %s", globalServerConfig.GetVersion(), serverConfigVersion)
|
t.Errorf("Expecting version %s found %s", globalServerConfig.GetVersion(), serverConfigVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := saveServerConfig(objLayer, globalServerConfig); err != nil {
|
if err := saveServerConfig(context.Background(), objLayer, globalServerConfig); err != nil {
|
||||||
t.Fatalf("Unable to save updated config file %s", err)
|
t.Fatalf("Unable to save updated config file %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +67,9 @@ func TestServerConfigWithEnvs(t *testing.T) {
|
|||||||
os.Setenv("MINIO_BROWSER", "off")
|
os.Setenv("MINIO_BROWSER", "off")
|
||||||
defer os.Unsetenv("MINIO_BROWSER")
|
defer os.Unsetenv("MINIO_BROWSER")
|
||||||
|
|
||||||
|
os.Setenv("MINIO_WORM", "on")
|
||||||
|
defer os.Unsetenv("MINIO_WORM")
|
||||||
|
|
||||||
os.Setenv("MINIO_ACCESS_KEY", "minio")
|
os.Setenv("MINIO_ACCESS_KEY", "minio")
|
||||||
defer os.Unsetenv("MINIO_ACCESS_KEY")
|
defer os.Unsetenv("MINIO_ACCESS_KEY")
|
||||||
|
|
||||||
@@ -97,31 +101,37 @@ func TestServerConfigWithEnvs(t *testing.T) {
|
|||||||
serverHandleEnvVars()
|
serverHandleEnvVars()
|
||||||
|
|
||||||
// Init config
|
// Init config
|
||||||
initConfig()
|
initConfig(objLayer)
|
||||||
|
|
||||||
// Check if serverConfig has
|
// Check if serverConfig has browser disabled
|
||||||
if globalServerConfig.GetBrowser() {
|
if globalIsBrowserEnabled {
|
||||||
t.Errorf("Expecting browser is set to false found %v", globalServerConfig.GetBrowser())
|
t.Error("Expected browser to be disabled but it is not")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if serverConfig has
|
// Check if serverConfig returns WORM config from the env
|
||||||
|
if !globalServerConfig.GetWorm() {
|
||||||
|
t.Error("Expected WORM to be enabled but it is not")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if serverConfig has region from the environment
|
||||||
if globalServerConfig.GetRegion() != "us-west-1" {
|
if globalServerConfig.GetRegion() != "us-west-1" {
|
||||||
t.Errorf("Expecting region to be \"us-west-1\" found %v", globalServerConfig.GetRegion())
|
t.Errorf("Expected region to be \"us-west-1\", found %v", globalServerConfig.GetRegion())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if serverConfig has
|
// Check if serverConfig has credentials from the environment
|
||||||
cred := globalServerConfig.GetCredential()
|
cred := globalServerConfig.GetCredential()
|
||||||
|
|
||||||
if cred.AccessKey != "minio" {
|
if cred.AccessKey != "minio" {
|
||||||
t.Errorf("Expecting access key to be `minio` found %s", cred.AccessKey)
|
t.Errorf("Expected access key to be `minio`, found %s", cred.AccessKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cred.SecretKey != "minio123" {
|
if cred.SecretKey != "minio123" {
|
||||||
t.Errorf("Expecting access key to be `minio123` found %s", cred.SecretKey)
|
t.Errorf("Expected access key to be `minio123`, found %s", cred.SecretKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalServerConfig.Domain != "domain.com" {
|
// Check if serverConfig has the correct domain
|
||||||
t.Errorf("Expecting Domain to be `domain.com` found " + globalServerConfig.Domain)
|
if globalDomainName != "domain.com" {
|
||||||
|
t.Errorf("Expected Domain to be `domain.com`, found " + globalDomainName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +188,7 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "amqp": { "1": { "enable": true, "url": "", "exchange": "", "routingKey": "", "exchangeType": "", "mandatory": false, "immediate": false, "durable": false, "internal": false, "noWait": false, "autoDeleted": false }}}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "amqp": { "1": { "enable": true, "url": "", "exchange": "", "routingKey": "", "exchangeType": "", "mandatory": false, "immediate": false, "durable": false, "internal": false, "noWait": false, "autoDeleted": false }}}}`, false},
|
||||||
|
|
||||||
// Test 12 - Test NATS
|
// Test 12 - Test NATS
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nats": { "1": { "enable": true, "address": "", "subject": "", "username": "", "password": "", "token": "", "secure": false, "pingInterval": 0, "streaming": { "enable": false, "clusterID": "", "clientID": "", "async": false, "maxPubAcksInflight": 0 } } }}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nats": { "1": { "enable": true, "address": "", "subject": "", "username": "", "password": "", "token": "", "secure": false, "pingInterval": 0, "streaming": { "enable": false, "clusterID": "", "async": false, "maxPubAcksInflight": 0 } } }}}`, false},
|
||||||
|
|
||||||
// Test 13 - Test ElasticSearch
|
// Test 13 - Test ElasticSearch
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "url": "", "index": "" } }}}`, false},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "url": "", "index": "" } }}}`, false},
|
||||||
@@ -224,10 +234,13 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
|
|
||||||
// Test 27 - Test MQTT
|
// Test 27 - 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},
|
{`{"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},
|
||||||
|
|
||||||
|
// Test 28 - Test NSQ
|
||||||
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nsq": { "1": { "enable": true, "nsqdAddress": "", "topic": ""} }}}`, false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
if err = saveConfig(objLayer, configPath, []byte(testCase.configData)); err != nil {
|
if err = saveConfig(context.Background(), objLayer, configPath, []byte(testCase.configData)); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_, err = getValidConfig(objLayer)
|
_, err = getValidConfig(objLayer)
|
||||||
@@ -250,77 +263,87 @@ func TestConfigDiff(t *testing.T) {
|
|||||||
{&serverConfig{}, nil, "Given configuration is empty"},
|
{&serverConfig{}, nil, "Given configuration is empty"},
|
||||||
// 2
|
// 2
|
||||||
{
|
{
|
||||||
&serverConfig{Credential: auth.Credentials{"u1", "p1"}},
|
&serverConfig{Credential: auth.Credentials{
|
||||||
&serverConfig{Credential: auth.Credentials{"u1", "p2"}},
|
AccessKey: "u1",
|
||||||
|
SecretKey: "p1",
|
||||||
|
Expiration: timeSentinel,
|
||||||
|
}},
|
||||||
|
&serverConfig{Credential: auth.Credentials{
|
||||||
|
AccessKey: "u1",
|
||||||
|
SecretKey: "p2",
|
||||||
|
Expiration: timeSentinel,
|
||||||
|
}},
|
||||||
"Credential configuration differs",
|
"Credential configuration differs",
|
||||||
},
|
},
|
||||||
// 3
|
// 3
|
||||||
{&serverConfig{Region: "us-east-1"}, &serverConfig{Region: "us-west-1"}, "Region configuration differs"},
|
{&serverConfig{Region: "us-east-1"}, &serverConfig{Region: "us-west-1"}, "Region configuration differs"},
|
||||||
// 4
|
// 4
|
||||||
{&serverConfig{Browser: false}, &serverConfig{Browser: true}, "Browser configuration differs"},
|
|
||||||
// 5
|
|
||||||
{&serverConfig{Domain: "domain1"}, &serverConfig{Domain: "domain2"}, "Domain configuration differs"},
|
|
||||||
// 6
|
|
||||||
{
|
{
|
||||||
&serverConfig{StorageClass: storageClassConfig{storageClass{"1", 8}, storageClass{"2", 6}}},
|
&serverConfig{StorageClass: storageClassConfig{storageClass{"1", 8}, storageClass{"2", 6}}},
|
||||||
&serverConfig{StorageClass: storageClassConfig{storageClass{"1", 8}, storageClass{"2", 4}}},
|
&serverConfig{StorageClass: storageClassConfig{storageClass{"1", 8}, storageClass{"2", 4}}},
|
||||||
"StorageClass configuration differs",
|
"StorageClass configuration differs",
|
||||||
},
|
},
|
||||||
// 7
|
// 5
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{AMQP: map[string]target.AMQPArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{AMQP: map[string]target.AMQPArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{AMQP: map[string]target.AMQPArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{AMQP: map[string]target.AMQPArgs{"1": {Enable: false}}}},
|
||||||
"AMQP Notification configuration differs",
|
"AMQP Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 8
|
// 6
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{NATS: map[string]target.NATSArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{NATS: map[string]target.NATSArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{NATS: map[string]target.NATSArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{NATS: map[string]target.NATSArgs{"1": {Enable: false}}}},
|
||||||
"NATS Notification configuration differs",
|
"NATS Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 9
|
// 7
|
||||||
|
{
|
||||||
|
&serverConfig{Notify: notifier{NSQ: map[string]target.NSQArgs{"1": {Enable: true}}}},
|
||||||
|
&serverConfig{Notify: notifier{NSQ: map[string]target.NSQArgs{"1": {Enable: false}}}},
|
||||||
|
"NSQ Notification configuration differs",
|
||||||
|
},
|
||||||
|
// 8
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{Elasticsearch: map[string]target.ElasticsearchArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{Elasticsearch: map[string]target.ElasticsearchArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{Elasticsearch: map[string]target.ElasticsearchArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{Elasticsearch: map[string]target.ElasticsearchArgs{"1": {Enable: false}}}},
|
||||||
"ElasticSearch Notification configuration differs",
|
"ElasticSearch Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 10
|
// 9
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{Redis: map[string]target.RedisArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{Redis: map[string]target.RedisArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{Redis: map[string]target.RedisArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{Redis: map[string]target.RedisArgs{"1": {Enable: false}}}},
|
||||||
"Redis Notification configuration differs",
|
"Redis Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 11
|
// 10
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{PostgreSQL: map[string]target.PostgreSQLArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{PostgreSQL: map[string]target.PostgreSQLArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{PostgreSQL: map[string]target.PostgreSQLArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{PostgreSQL: map[string]target.PostgreSQLArgs{"1": {Enable: false}}}},
|
||||||
"PostgreSQL Notification configuration differs",
|
"PostgreSQL Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 12
|
// 11
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{Kafka: map[string]target.KafkaArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{Kafka: map[string]target.KafkaArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{Kafka: map[string]target.KafkaArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{Kafka: map[string]target.KafkaArgs{"1": {Enable: false}}}},
|
||||||
"Kafka Notification configuration differs",
|
"Kafka Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 13
|
// 12
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{Webhook: map[string]target.WebhookArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{Webhook: map[string]target.WebhookArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{Webhook: map[string]target.WebhookArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{Webhook: map[string]target.WebhookArgs{"1": {Enable: false}}}},
|
||||||
"Webhook Notification configuration differs",
|
"Webhook Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 14
|
// 13
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{MySQL: map[string]target.MySQLArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{MySQL: map[string]target.MySQLArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{MySQL: map[string]target.MySQLArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{MySQL: map[string]target.MySQLArgs{"1": {Enable: false}}}},
|
||||||
"MySQL Notification configuration differs",
|
"MySQL Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 15
|
// 14
|
||||||
{
|
{
|
||||||
&serverConfig{Notify: notifier{MQTT: map[string]target.MQTTArgs{"1": {Enable: true}}}},
|
&serverConfig{Notify: notifier{MQTT: map[string]target.MQTTArgs{"1": {Enable: true}}}},
|
||||||
&serverConfig{Notify: notifier{MQTT: map[string]target.MQTTArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{MQTT: map[string]target.MQTTArgs{"1": {Enable: false}}}},
|
||||||
"MQTT Notification configuration differs",
|
"MQTT Notification configuration differs",
|
||||||
},
|
},
|
||||||
// 16
|
// 15
|
||||||
{
|
{
|
||||||
&serverConfig{Logger: loggerConfig{
|
&serverConfig{Logger: loggerConfig{
|
||||||
Console: loggerConsole{Enabled: true},
|
Console: loggerConsole{Enabled: true},
|
||||||
|
|||||||
+9
-1
@@ -74,7 +74,15 @@ func (config *ConfigDir) GetCADir() string {
|
|||||||
|
|
||||||
// Create - creates configuration directory tree.
|
// Create - creates configuration directory tree.
|
||||||
func (config *ConfigDir) Create() error {
|
func (config *ConfigDir) Create() error {
|
||||||
return os.MkdirAll(config.GetCADir(), 0700)
|
err := os.MkdirAll(config.GetCADir(), 0700)
|
||||||
|
// It is possible in kubernetes like deployments this directory
|
||||||
|
// is already mounted and is not writable, ignore any write errors.
|
||||||
|
if err != nil {
|
||||||
|
if os.IsPermission(err) {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMinioConfigFile - returns absolute path of config.json file.
|
// GetMinioConfigFile - returns absolute path of config.json file.
|
||||||
|
|||||||
+292
-24
@@ -18,6 +18,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -29,6 +30,8 @@ import (
|
|||||||
"github.com/minio/minio/pkg/dns"
|
"github.com/minio/minio/pkg/dns"
|
||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/event/target"
|
"github.com/minio/minio/pkg/event/target"
|
||||||
|
"github.com/minio/minio/pkg/iam/policy"
|
||||||
|
"github.com/minio/minio/pkg/iam/validator"
|
||||||
xnet "github.com/minio/minio/pkg/net"
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
"github.com/minio/minio/pkg/quick"
|
"github.com/minio/minio/pkg/quick"
|
||||||
)
|
)
|
||||||
@@ -916,7 +919,7 @@ func migrateV12ToV13() error {
|
|||||||
// Copy over fields from V12 into V13 config struct
|
// Copy over fields from V12 into V13 config struct
|
||||||
srvConfig := &serverConfigV13{
|
srvConfig := &serverConfigV13{
|
||||||
Logger: &loggerV7{},
|
Logger: &loggerV7{},
|
||||||
Notify: ¬ifier{},
|
Notify: ¬ifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "13"
|
srvConfig.Version = "13"
|
||||||
srvConfig.Credential = cv12.Credential
|
srvConfig.Credential = cv12.Credential
|
||||||
@@ -996,7 +999,7 @@ func migrateV13ToV14() error {
|
|||||||
// Copy over fields from V13 into V14 config struct
|
// Copy over fields from V13 into V14 config struct
|
||||||
srvConfig := &serverConfigV14{
|
srvConfig := &serverConfigV14{
|
||||||
Logger: &loggerV7{},
|
Logger: &loggerV7{},
|
||||||
Notify: ¬ifier{},
|
Notify: ¬ifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "14"
|
srvConfig.Version = "14"
|
||||||
srvConfig.Credential = cv13.Credential
|
srvConfig.Credential = cv13.Credential
|
||||||
@@ -1081,7 +1084,7 @@ func migrateV14ToV15() error {
|
|||||||
// Copy over fields from V14 into V15 config struct
|
// Copy over fields from V14 into V15 config struct
|
||||||
srvConfig := &serverConfigV15{
|
srvConfig := &serverConfigV15{
|
||||||
Logger: &loggerV7{},
|
Logger: &loggerV7{},
|
||||||
Notify: ¬ifier{},
|
Notify: ¬ifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "15"
|
srvConfig.Version = "15"
|
||||||
srvConfig.Credential = cv14.Credential
|
srvConfig.Credential = cv14.Credential
|
||||||
@@ -1171,7 +1174,7 @@ func migrateV15ToV16() error {
|
|||||||
// Copy over fields from V15 into V16 config struct
|
// Copy over fields from V15 into V16 config struct
|
||||||
srvConfig := &serverConfigV16{
|
srvConfig := &serverConfigV16{
|
||||||
Logger: &loggers{},
|
Logger: &loggers{},
|
||||||
Notify: ¬ifier{},
|
Notify: ¬ifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "16"
|
srvConfig.Version = "16"
|
||||||
srvConfig.Credential = cv15.Credential
|
srvConfig.Credential = cv15.Credential
|
||||||
@@ -1261,7 +1264,7 @@ func migrateV16ToV17() error {
|
|||||||
// Copy over fields from V16 into V17 config struct
|
// Copy over fields from V16 into V17 config struct
|
||||||
srvConfig := &serverConfigV17{
|
srvConfig := &serverConfigV17{
|
||||||
Logger: &loggers{},
|
Logger: &loggers{},
|
||||||
Notify: ¬ifier{},
|
Notify: ¬ifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "17"
|
srvConfig.Version = "17"
|
||||||
srvConfig.Credential = cv16.Credential
|
srvConfig.Credential = cv16.Credential
|
||||||
@@ -1382,7 +1385,7 @@ func migrateV17ToV18() error {
|
|||||||
// Copy over fields from V17 into V18 config struct
|
// Copy over fields from V17 into V18 config struct
|
||||||
srvConfig := &serverConfigV17{
|
srvConfig := &serverConfigV17{
|
||||||
Logger: &loggers{},
|
Logger: &loggers{},
|
||||||
Notify: ¬ifier{},
|
Notify: ¬ifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "18"
|
srvConfig.Version = "18"
|
||||||
srvConfig.Credential = cv17.Credential
|
srvConfig.Credential = cv17.Credential
|
||||||
@@ -1484,7 +1487,7 @@ func migrateV18ToV19() error {
|
|||||||
// Copy over fields from V18 into V19 config struct
|
// Copy over fields from V18 into V19 config struct
|
||||||
srvConfig := &serverConfigV18{
|
srvConfig := &serverConfigV18{
|
||||||
Logger: &loggers{},
|
Logger: &loggers{},
|
||||||
Notify: ¬ifier{},
|
Notify: ¬ifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "19"
|
srvConfig.Version = "19"
|
||||||
srvConfig.Credential = cv18.Credential
|
srvConfig.Credential = cv18.Credential
|
||||||
@@ -1590,7 +1593,7 @@ func migrateV19ToV20() error {
|
|||||||
// Copy over fields from V19 into V20 config struct
|
// Copy over fields from V19 into V20 config struct
|
||||||
srvConfig := &serverConfigV20{
|
srvConfig := &serverConfigV20{
|
||||||
Logger: &loggers{},
|
Logger: &loggers{},
|
||||||
Notify: ¬ifier{},
|
Notify: ¬ifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "20"
|
srvConfig.Version = "20"
|
||||||
srvConfig.Credential = cv19.Credential
|
srvConfig.Credential = cv19.Credential
|
||||||
@@ -1694,7 +1697,7 @@ func migrateV20ToV21() error {
|
|||||||
|
|
||||||
// Copy over fields from V20 into V21 config struct
|
// Copy over fields from V20 into V21 config struct
|
||||||
srvConfig := &serverConfigV21{
|
srvConfig := &serverConfigV21{
|
||||||
Notify: ¬ifier{},
|
Notify: ¬ifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "21"
|
srvConfig.Version = "21"
|
||||||
srvConfig.Credential = cv20.Credential
|
srvConfig.Credential = cv20.Credential
|
||||||
@@ -1798,7 +1801,7 @@ func migrateV21ToV22() error {
|
|||||||
|
|
||||||
// Copy over fields from V21 into V22 config struct
|
// Copy over fields from V21 into V22 config struct
|
||||||
srvConfig := &serverConfigV22{
|
srvConfig := &serverConfigV22{
|
||||||
Notify: notifier{},
|
Notify: notifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "22"
|
srvConfig.Version = "22"
|
||||||
srvConfig.Credential = cv21.Credential
|
srvConfig.Credential = cv21.Credential
|
||||||
@@ -1902,7 +1905,7 @@ func migrateV22ToV23() error {
|
|||||||
|
|
||||||
// Copy over fields from V22 into V23 config struct
|
// Copy over fields from V22 into V23 config struct
|
||||||
srvConfig := &serverConfigV23{
|
srvConfig := &serverConfigV23{
|
||||||
Notify: notifier{},
|
Notify: notifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "23"
|
srvConfig.Version = "23"
|
||||||
srvConfig.Credential = cv22.Credential
|
srvConfig.Credential = cv22.Credential
|
||||||
@@ -2015,7 +2018,7 @@ func migrateV23ToV24() error {
|
|||||||
|
|
||||||
// Copy over fields from V23 into V24 config struct
|
// Copy over fields from V23 into V24 config struct
|
||||||
srvConfig := &serverConfigV24{
|
srvConfig := &serverConfigV24{
|
||||||
Notify: notifier{},
|
Notify: notifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "24"
|
srvConfig.Version = "24"
|
||||||
srvConfig.Credential = cv23.Credential
|
srvConfig.Credential = cv23.Credential
|
||||||
@@ -2128,7 +2131,7 @@ func migrateV24ToV25() error {
|
|||||||
|
|
||||||
// Copy over fields from V24 into V25 config struct
|
// Copy over fields from V24 into V25 config struct
|
||||||
srvConfig := &serverConfigV25{
|
srvConfig := &serverConfigV25{
|
||||||
Notify: notifier{},
|
Notify: notifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "25"
|
srvConfig.Version = "25"
|
||||||
srvConfig.Credential = cv24.Credential
|
srvConfig.Credential = cv24.Credential
|
||||||
@@ -2246,7 +2249,7 @@ func migrateV25ToV26() error {
|
|||||||
|
|
||||||
// Copy over fields from V25 into V26 config struct
|
// Copy over fields from V25 into V26 config struct
|
||||||
srvConfig := &serverConfigV26{
|
srvConfig := &serverConfigV26{
|
||||||
Notify: notifier{},
|
Notify: notifierV3{},
|
||||||
}
|
}
|
||||||
srvConfig.Version = "26"
|
srvConfig.Version = "26"
|
||||||
srvConfig.Credential = cv25.Credential
|
srvConfig.Credential = cv25.Credential
|
||||||
@@ -2387,6 +2390,7 @@ func migrateV27ToV28() error {
|
|||||||
|
|
||||||
// config V28 is backward compatible with V27, load the old
|
// config V28 is backward compatible with V27, load the old
|
||||||
// config file in serverConfigV28 struct and initialize KMSConfig
|
// config file in serverConfigV28 struct and initialize KMSConfig
|
||||||
|
|
||||||
srvConfig := &serverConfigV28{}
|
srvConfig := &serverConfigV28{}
|
||||||
_, err := quick.LoadConfig(configFile, globalEtcdClient, srvConfig)
|
_, err := quick.LoadConfig(configFile, globalEtcdClient, srvConfig)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -2409,41 +2413,305 @@ func migrateV27ToV28() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrates '.minio.sys/config.json' v27 to v28.
|
// Migrates ${HOME}/.minio/config.json to '<export_path>/.minio.sys/config/config.json'
|
||||||
func migrateMinioSysConfig(objAPI ObjectLayer) error {
|
func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) {
|
||||||
|
defer func() {
|
||||||
|
// Rename config.json to config.json.deprecated only upon
|
||||||
|
// success of this function.
|
||||||
|
if err == nil {
|
||||||
|
os.Rename(getConfigFile(), getConfigFile()+".deprecated")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
// Construct path to config.json for the given bucket.
|
// Construct path to config.json for the given bucket.
|
||||||
configFile := path.Join(bucketConfigPrefix, minioConfigFile)
|
|
||||||
transactionConfigFile := configFile + ".transaction"
|
transactionConfigFile := configFile + ".transaction"
|
||||||
|
|
||||||
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
|
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
|
||||||
// and configFile, take a transaction lock to avoid race.
|
// and configFile, take a transaction lock to avoid data race between readConfig()
|
||||||
|
// and saveConfig().
|
||||||
|
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
|
||||||
|
if err = objLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer objLock.Unlock()
|
||||||
|
|
||||||
|
// Verify if backend already has the file.
|
||||||
|
if err = checkConfig(context.Background(), objAPI, configFile); err != errConfigNotFound {
|
||||||
|
return err
|
||||||
|
} // if errConfigNotFound proceed to migrate..
|
||||||
|
|
||||||
|
var config = &serverConfig{}
|
||||||
|
if _, err = Load(getConfigFile(), config); err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Read from deprecate file as well if necessary.
|
||||||
|
if _, err = Load(getConfigFile()+".deprecated", config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveServerConfig(context.Background(), objAPI, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrates '.minio.sys/config.json' to v33.
|
||||||
|
func migrateMinioSysConfig(objAPI ObjectLayer) error {
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
|
||||||
|
// Construct path to config.json for the given bucket.
|
||||||
|
transactionConfigFile := configFile + ".transaction"
|
||||||
|
|
||||||
|
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
|
||||||
|
// and configFile, take a transaction lock to avoid data race between readConfig()
|
||||||
|
// and saveConfig().
|
||||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
|
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
|
||||||
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer objLock.Unlock()
|
defer objLock.Unlock()
|
||||||
|
|
||||||
return migrateV27ToV28MinioSys(objAPI)
|
if err := migrateV27ToV28MinioSys(objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := migrateV28ToV29MinioSys(objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := migrateV29ToV30MinioSys(objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := migrateV30ToV31MinioSys(objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := migrateV31ToV32MinioSys(objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return migrateV32ToV33MinioSys(objAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkConfigVersion(objAPI ObjectLayer, configFile string, version string) (bool, []byte, error) {
|
||||||
|
data, err := readConfig(context.Background(), objAPI, configFile)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionConfig struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
vcfg := &versionConfig
|
||||||
|
if err = json.Unmarshal(data, vcfg); err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
return vcfg.Version == version, data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateV27ToV28MinioSys(objAPI ObjectLayer) error {
|
func migrateV27ToV28MinioSys(objAPI ObjectLayer) error {
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
srvConfig, err := readServerConfig(context.Background(), objAPI)
|
ok, data, err := checkConfigVersion(objAPI, configFile, "27")
|
||||||
if err == errConfigNotFound {
|
if err == errConfigNotFound {
|
||||||
return nil
|
return nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return fmt.Errorf("Unable to load config file. %v", err)
|
return fmt.Errorf("Unable to load config file. %v", err)
|
||||||
}
|
}
|
||||||
if srvConfig.Version != "27" {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
srvConfig.Version = "28"
|
cfg := &serverConfigV28{}
|
||||||
srvConfig.KMS = crypto.KMSConfig{}
|
if err = json.Unmarshal(data, cfg); err != nil {
|
||||||
if err = saveServerConfig(objAPI, srvConfig); err != nil {
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = "28"
|
||||||
|
cfg.KMS = crypto.KMSConfig{}
|
||||||
|
|
||||||
|
data, err = json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||||
return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %v", err)
|
return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
|
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migrateV28ToV29MinioSys(objAPI ObjectLayer) error {
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
|
||||||
|
ok, data, err := checkConfigVersion(objAPI, configFile, "28")
|
||||||
|
if err == errConfigNotFound {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("Unable to load config file. %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &serverConfigV29{}
|
||||||
|
if err = json.Unmarshal(data, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = "29"
|
||||||
|
data, err = json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||||
|
return fmt.Errorf("Failed to migrate config from ‘28’ to ‘29’. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(configMigrateMSGTemplate, configFile, "28", "29")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateV29ToV30MinioSys(objAPI ObjectLayer) error {
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
|
||||||
|
ok, data, err := checkConfigVersion(objAPI, configFile, "29")
|
||||||
|
if err == errConfigNotFound {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("Unable to load config file. %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &serverConfigV30{}
|
||||||
|
if err = json.Unmarshal(data, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = "30"
|
||||||
|
// Init compression config.For future migration, Compression config needs to be copied over from previous version.
|
||||||
|
cfg.Compression.Enabled = false
|
||||||
|
cfg.Compression.Extensions = globalCompressExtensions
|
||||||
|
cfg.Compression.MimeTypes = globalCompressMimeTypes
|
||||||
|
|
||||||
|
data, err = json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||||
|
return fmt.Errorf("Failed to migrate config from ‘29’ to ‘30’. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(configMigrateMSGTemplate, configFile, "29", "30")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateV30ToV31MinioSys(objAPI ObjectLayer) error {
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
|
||||||
|
ok, data, err := checkConfigVersion(objAPI, configFile, "30")
|
||||||
|
if err == errConfigNotFound {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("Unable to load config file. %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &serverConfigV31{}
|
||||||
|
if err = json.Unmarshal(data, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = "31"
|
||||||
|
cfg.OpenID.JWKS = validator.JWKSArgs{
|
||||||
|
URL: &xnet.URL{},
|
||||||
|
}
|
||||||
|
cfg.Policy.OPA = iampolicy.OpaArgs{
|
||||||
|
URL: &xnet.URL{},
|
||||||
|
AuthToken: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||||
|
return fmt.Errorf("Failed to migrate config from ‘30’ to ‘31’. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(configMigrateMSGTemplate, configFile, "30", "31")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateV31ToV32MinioSys(objAPI ObjectLayer) error {
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
|
||||||
|
ok, data, err := checkConfigVersion(objAPI, configFile, "31")
|
||||||
|
if err == errConfigNotFound {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("Unable to load config file. %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &serverConfigV32{}
|
||||||
|
if err = json.Unmarshal(data, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = "32"
|
||||||
|
cfg.Notify.NSQ = make(map[string]target.NSQArgs)
|
||||||
|
cfg.Notify.NSQ["1"] = target.NSQArgs{}
|
||||||
|
|
||||||
|
data, err = json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||||
|
return fmt.Errorf("Failed to migrate config from ‘31’ to ‘32’. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(configMigrateMSGTemplate, configFile, "31", "32")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateV32ToV33MinioSys(objAPI ObjectLayer) error {
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
|
||||||
|
ok, data, err := checkConfigVersion(objAPI, configFile, "32")
|
||||||
|
if err == errConfigNotFound {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("Unable to load config file. %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &serverConfigV33{}
|
||||||
|
if err = json.Unmarshal(data, cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = "33"
|
||||||
|
|
||||||
|
data, err = json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = saveConfig(context.Background(), objAPI, configFile, data); err != nil {
|
||||||
|
return fmt.Errorf("Failed to migrate config from 32 to 33 . %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(configMigrateMSGTemplate, configFile, "32", "33")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -159,8 +159,8 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test if a config migration from v2 to v28 is successfully done
|
// Test if a config migration from v2 to v33 is successfully done
|
||||||
func TestServerConfigMigrateV2toV28(t *testing.T) {
|
func TestServerConfigMigrateV2toV33(t *testing.T) {
|
||||||
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -174,10 +174,6 @@ func TestServerConfigMigrateV2toV28(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(fsDir)
|
defer os.RemoveAll(fsDir)
|
||||||
|
|
||||||
globalObjLayerMutex.Lock()
|
|
||||||
globalObjectAPI = objLayer
|
|
||||||
globalObjLayerMutex.Unlock()
|
|
||||||
|
|
||||||
configPath := rootPath + "/" + minioConfigFile
|
configPath := rootPath + "/" + minioConfigFile
|
||||||
|
|
||||||
// Create a corrupted config file
|
// Create a corrupted config file
|
||||||
@@ -203,12 +199,16 @@ func TestServerConfigMigrateV2toV28(t *testing.T) {
|
|||||||
t.Fatal("Unexpected error: ", err)
|
t.Fatal("Unexpected error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := migrateConfigToMinioSys(); err != nil {
|
if err := migrateConfigToMinioSys(objLayer); err != nil {
|
||||||
|
t.Fatal("Unexpected error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := migrateMinioSysConfig(objLayer); err != nil {
|
||||||
t.Fatal("Unexpected error: ", err)
|
t.Fatal("Unexpected error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize server config and check again if everything is fine
|
// Initialize server config and check again if everything is fine
|
||||||
if err := loadConfig(newObjectLayerFn()); err != nil {
|
if err := loadConfig(objLayer); err != nil {
|
||||||
t.Fatalf("Unable to initialize from updated config file %s", err)
|
t.Fatalf("Unable to initialize from updated config file %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +222,7 @@ func TestServerConfigMigrateV2toV28(t *testing.T) {
|
|||||||
if globalServerConfig.Credential.AccessKey != accessKey {
|
if globalServerConfig.Credential.AccessKey != accessKey {
|
||||||
t.Fatalf("Access key lost during migration, expected: %v, found:%v", accessKey, globalServerConfig.Credential.AccessKey)
|
t.Fatalf("Access key lost during migration, expected: %v, found:%v", accessKey, globalServerConfig.Credential.AccessKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalServerConfig.Credential.SecretKey != secretKey {
|
if globalServerConfig.Credential.SecretKey != secretKey {
|
||||||
t.Fatalf("Secret key lost during migration, expected: %v, found: %v", secretKey, globalServerConfig.Credential.SecretKey)
|
t.Fatalf("Secret key lost during migration, expected: %v, found: %v", secretKey, globalServerConfig.Credential.SecretKey)
|
||||||
}
|
}
|
||||||
@@ -318,7 +319,6 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
|||||||
if err := migrateV26ToV27(); err == nil {
|
if err := migrateV26ToV27(); err == nil {
|
||||||
t.Fatal("migrateConfigV26ToV27() should fail with a corrupted json")
|
t.Fatal("migrateConfigV26ToV27() should fail with a corrupted json")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := migrateV27ToV28(); err == nil {
|
if err := migrateV27ToV28(); err == nil {
|
||||||
t.Fatal("migrateConfigV27ToV28() should fail with a corrupted json")
|
t.Fatal("migrateConfigV27ToV28() should fail with a corrupted json")
|
||||||
}
|
}
|
||||||
|
|||||||
+198
-18
@@ -22,6 +22,8 @@ import (
|
|||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/event/target"
|
"github.com/minio/minio/pkg/event/target"
|
||||||
|
"github.com/minio/minio/pkg/iam/policy"
|
||||||
|
"github.com/minio/minio/pkg/iam/validator"
|
||||||
"github.com/minio/minio/pkg/quick"
|
"github.com/minio/minio/pkg/quick"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -371,7 +373,7 @@ type serverConfigV12 struct {
|
|||||||
Notify notifierV2 `json:"notify"`
|
Notify notifierV2 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type notifier struct {
|
type notifierV3 struct {
|
||||||
AMQP map[string]target.AMQPArgs `json:"amqp"`
|
AMQP map[string]target.AMQPArgs `json:"amqp"`
|
||||||
Elasticsearch map[string]target.ElasticsearchArgs `json:"elasticsearch"`
|
Elasticsearch map[string]target.ElasticsearchArgs `json:"elasticsearch"`
|
||||||
Kafka map[string]target.KafkaArgs `json:"kafka"`
|
Kafka map[string]target.KafkaArgs `json:"kafka"`
|
||||||
@@ -396,7 +398,7 @@ type serverConfigV13 struct {
|
|||||||
Logger *loggerV7 `json:"logger"`
|
Logger *loggerV7 `json:"logger"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify *notifier `json:"notify"`
|
Notify *notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverConfigV14 server configuration version '14' which is like
|
// serverConfigV14 server configuration version '14' which is like
|
||||||
@@ -413,7 +415,7 @@ type serverConfigV14 struct {
|
|||||||
Logger *loggerV7 `json:"logger"`
|
Logger *loggerV7 `json:"logger"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify *notifier `json:"notify"`
|
Notify *notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverConfigV15 server configuration version '15' which is like
|
// serverConfigV15 server configuration version '15' which is like
|
||||||
@@ -430,7 +432,7 @@ type serverConfigV15 struct {
|
|||||||
Logger *loggerV7 `json:"logger"`
|
Logger *loggerV7 `json:"logger"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify *notifier `json:"notify"`
|
Notify *notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileLogger is introduced to workaround the dependency about logrus
|
// FileLogger is introduced to workaround the dependency about logrus
|
||||||
@@ -468,7 +470,7 @@ type serverConfigV16 struct {
|
|||||||
Logger *loggers `json:"logger"`
|
Logger *loggers `json:"logger"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify *notifier `json:"notify"`
|
Notify *notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverConfigV17 server configuration version '17' which is like
|
// serverConfigV17 server configuration version '17' which is like
|
||||||
@@ -487,7 +489,7 @@ type serverConfigV17 struct {
|
|||||||
Logger *loggers `json:"logger"`
|
Logger *loggers `json:"logger"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify *notifier `json:"notify"`
|
Notify *notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverConfigV18 server configuration version '18' which is like
|
// serverConfigV18 server configuration version '18' which is like
|
||||||
@@ -506,7 +508,7 @@ type serverConfigV18 struct {
|
|||||||
Logger *loggers `json:"logger"`
|
Logger *loggers `json:"logger"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify *notifier `json:"notify"`
|
Notify *notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverConfigV19 server configuration version '19' which is like
|
// serverConfigV19 server configuration version '19' which is like
|
||||||
@@ -524,7 +526,7 @@ type serverConfigV19 struct {
|
|||||||
Logger *loggers `json:"logger"`
|
Logger *loggers `json:"logger"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify *notifier `json:"notify"`
|
Notify *notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverConfigV20 server configuration version '20' which is like
|
// serverConfigV20 server configuration version '20' which is like
|
||||||
@@ -543,7 +545,7 @@ type serverConfigV20 struct {
|
|||||||
Logger *loggers `json:"logger"`
|
Logger *loggers `json:"logger"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify *notifier `json:"notify"`
|
Notify *notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverConfigV21 is just like version '20' without logger field
|
// serverConfigV21 is just like version '20' without logger field
|
||||||
@@ -558,7 +560,7 @@ type serverConfigV21 struct {
|
|||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify *notifier `json:"notify"`
|
Notify *notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverConfigV22 is just like version '21' with added support
|
// serverConfigV22 is just like version '21' with added support
|
||||||
@@ -579,7 +581,7 @@ type serverConfigV22 struct {
|
|||||||
StorageClass storageClassConfig `json:"storageclass"`
|
StorageClass storageClassConfig `json:"storageclass"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify notifier `json:"notify"`
|
Notify notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverConfigV23 is just like version '22' with addition of cache field.
|
// serverConfigV23 is just like version '22' with addition of cache field.
|
||||||
@@ -602,7 +604,7 @@ type serverConfigV23 struct {
|
|||||||
Cache CacheConfig `json:"cache"`
|
Cache CacheConfig `json:"cache"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify notifier `json:"notify"`
|
Notify notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverConfigV24 is just like version '23', we had to revert
|
// serverConfigV24 is just like version '23', we had to revert
|
||||||
@@ -626,7 +628,7 @@ type serverConfigV24 struct {
|
|||||||
Cache CacheConfig `json:"cache"`
|
Cache CacheConfig `json:"cache"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify notifier `json:"notify"`
|
Notify notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverConfigV25 is just like version '24', stores additionally
|
// serverConfigV25 is just like version '24', stores additionally
|
||||||
@@ -653,7 +655,7 @@ type serverConfigV25 struct {
|
|||||||
Cache CacheConfig `json:"cache"`
|
Cache CacheConfig `json:"cache"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify notifier `json:"notify"`
|
Notify notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverConfigV26 is just like version '25', stores additionally
|
// serverConfigV26 is just like version '25', stores additionally
|
||||||
@@ -677,7 +679,7 @@ type serverConfigV26 struct {
|
|||||||
Cache CacheConfig `json:"cache"`
|
Cache CacheConfig `json:"cache"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify notifier `json:"notify"`
|
Notify notifierV3 `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type loggerConsole struct {
|
type loggerConsole struct {
|
||||||
@@ -718,7 +720,7 @@ type serverConfigV27 struct {
|
|||||||
Cache CacheConfig `json:"cache"`
|
Cache CacheConfig `json:"cache"`
|
||||||
|
|
||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify notifier `json:"notify"`
|
Notify notifierV3 `json:"notify"`
|
||||||
|
|
||||||
// Logger configuration
|
// Logger configuration
|
||||||
Logger loggerConfig `json:"logger"`
|
Logger loggerConfig `json:"logger"`
|
||||||
@@ -737,9 +739,126 @@ type serverConfigV28 struct {
|
|||||||
// S3 API configuration.
|
// S3 API configuration.
|
||||||
Credential auth.Credentials `json:"credential"`
|
Credential auth.Credentials `json:"credential"`
|
||||||
Region string `json:"region"`
|
Region string `json:"region"`
|
||||||
Browser BoolFlag `json:"browser"`
|
|
||||||
Worm BoolFlag `json:"worm"`
|
Worm BoolFlag `json:"worm"`
|
||||||
Domain string `json:"domain"`
|
|
||||||
|
// Storage class configuration
|
||||||
|
StorageClass storageClassConfig `json:"storageclass"`
|
||||||
|
|
||||||
|
// Cache configuration
|
||||||
|
Cache CacheConfig `json:"cache"`
|
||||||
|
|
||||||
|
// KMS configuration
|
||||||
|
KMS crypto.KMSConfig `json:"kms"`
|
||||||
|
|
||||||
|
// Notification queue configuration.
|
||||||
|
Notify notifierV3 `json:"notify"`
|
||||||
|
|
||||||
|
// Logger configuration
|
||||||
|
Logger loggerConfig `json:"logger"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverConfigV29 is just like version '28'.
|
||||||
|
type serverConfigV29 serverConfigV28
|
||||||
|
|
||||||
|
// compressionConfig represents the compression settings.
|
||||||
|
type compressionConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Extensions []string `json:"extensions"`
|
||||||
|
MimeTypes []string `json:"mime-types"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverConfigV30 is just like version '29', stores additionally
|
||||||
|
// extensions and mimetypes fields for compression.
|
||||||
|
type serverConfigV30 struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
|
||||||
|
// S3 API configuration.
|
||||||
|
Credential auth.Credentials `json:"credential"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Worm BoolFlag `json:"worm"`
|
||||||
|
|
||||||
|
// Storage class configuration
|
||||||
|
StorageClass storageClassConfig `json:"storageclass"`
|
||||||
|
|
||||||
|
// Cache configuration
|
||||||
|
Cache CacheConfig `json:"cache"`
|
||||||
|
|
||||||
|
// KMS configuration
|
||||||
|
KMS crypto.KMSConfig `json:"kms"`
|
||||||
|
|
||||||
|
// Notification queue configuration.
|
||||||
|
Notify notifierV3 `json:"notify"`
|
||||||
|
|
||||||
|
// Logger configuration
|
||||||
|
Logger loggerConfig `json:"logger"`
|
||||||
|
|
||||||
|
// Compression configuration
|
||||||
|
Compression compressionConfig `json:"compress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverConfigV31 is just like version '30', with OPA and OpenID configuration.
|
||||||
|
type serverConfigV31 struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
|
||||||
|
// S3 API configuration.
|
||||||
|
Credential auth.Credentials `json:"credential"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Worm BoolFlag `json:"worm"`
|
||||||
|
|
||||||
|
// Storage class configuration
|
||||||
|
StorageClass storageClassConfig `json:"storageclass"`
|
||||||
|
|
||||||
|
// Cache configuration
|
||||||
|
Cache CacheConfig `json:"cache"`
|
||||||
|
|
||||||
|
// KMS configuration
|
||||||
|
KMS crypto.KMSConfig `json:"kms"`
|
||||||
|
|
||||||
|
// Notification queue configuration.
|
||||||
|
Notify notifierV3 `json:"notify"`
|
||||||
|
|
||||||
|
// Logger configuration
|
||||||
|
Logger loggerConfig `json:"logger"`
|
||||||
|
|
||||||
|
// Compression configuration
|
||||||
|
Compression compressionConfig `json:"compress"`
|
||||||
|
|
||||||
|
// OpenID configuration
|
||||||
|
OpenID struct {
|
||||||
|
// JWKS validator config.
|
||||||
|
JWKS validator.JWKSArgs `json:"jwks"`
|
||||||
|
} `json:"openid"`
|
||||||
|
|
||||||
|
// External policy enforcements.
|
||||||
|
Policy struct {
|
||||||
|
// OPA configuration.
|
||||||
|
OPA iampolicy.OpaArgs `json:"opa"`
|
||||||
|
|
||||||
|
// Add new external policy enforcements here.
|
||||||
|
} `json:"policy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type notifier struct {
|
||||||
|
AMQP map[string]target.AMQPArgs `json:"amqp"`
|
||||||
|
Elasticsearch map[string]target.ElasticsearchArgs `json:"elasticsearch"`
|
||||||
|
Kafka map[string]target.KafkaArgs `json:"kafka"`
|
||||||
|
MQTT map[string]target.MQTTArgs `json:"mqtt"`
|
||||||
|
MySQL map[string]target.MySQLArgs `json:"mysql"`
|
||||||
|
NATS map[string]target.NATSArgs `json:"nats"`
|
||||||
|
NSQ map[string]target.NSQArgs `json:"nsq"`
|
||||||
|
PostgreSQL map[string]target.PostgreSQLArgs `json:"postgresql"`
|
||||||
|
Redis map[string]target.RedisArgs `json:"redis"`
|
||||||
|
Webhook map[string]target.WebhookArgs `json:"webhook"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverConfigV32 is just like version '31' with added nsq notifer.
|
||||||
|
type serverConfigV32 struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
|
||||||
|
// S3 API configuration.
|
||||||
|
Credential auth.Credentials `json:"credential"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Worm BoolFlag `json:"worm"`
|
||||||
|
|
||||||
// Storage class configuration
|
// Storage class configuration
|
||||||
StorageClass storageClassConfig `json:"storageclass"`
|
StorageClass storageClassConfig `json:"storageclass"`
|
||||||
@@ -755,4 +874,65 @@ type serverConfigV28 struct {
|
|||||||
|
|
||||||
// Logger configuration
|
// Logger configuration
|
||||||
Logger loggerConfig `json:"logger"`
|
Logger loggerConfig `json:"logger"`
|
||||||
|
|
||||||
|
// Compression configuration
|
||||||
|
Compression compressionConfig `json:"compress"`
|
||||||
|
|
||||||
|
// OpenID configuration
|
||||||
|
OpenID struct {
|
||||||
|
// JWKS validator config.
|
||||||
|
JWKS validator.JWKSArgs `json:"jwks"`
|
||||||
|
} `json:"openid"`
|
||||||
|
|
||||||
|
// External policy enforcements.
|
||||||
|
Policy struct {
|
||||||
|
// OPA configuration.
|
||||||
|
OPA iampolicy.OpaArgs `json:"opa"`
|
||||||
|
|
||||||
|
// Add new external policy enforcements here.
|
||||||
|
} `json:"policy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverConfigV33 is just like version '32', removes clientID from NATS.
|
||||||
|
type serverConfigV33 struct {
|
||||||
|
quick.Config `json:"-"` // ignore interfaces
|
||||||
|
|
||||||
|
Version string `json:"version"`
|
||||||
|
|
||||||
|
// S3 API configuration.
|
||||||
|
Credential auth.Credentials `json:"credential"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Worm BoolFlag `json:"worm"`
|
||||||
|
|
||||||
|
// Storage class configuration
|
||||||
|
StorageClass storageClassConfig `json:"storageclass"`
|
||||||
|
|
||||||
|
// Cache configuration
|
||||||
|
Cache CacheConfig `json:"cache"`
|
||||||
|
|
||||||
|
// KMS configuration
|
||||||
|
KMS crypto.KMSConfig `json:"kms"`
|
||||||
|
|
||||||
|
// Notification queue configuration.
|
||||||
|
Notify notifier `json:"notify"`
|
||||||
|
|
||||||
|
// Logger configuration
|
||||||
|
Logger loggerConfig `json:"logger"`
|
||||||
|
|
||||||
|
// Compression configuration
|
||||||
|
Compression compressionConfig `json:"compress"`
|
||||||
|
|
||||||
|
// OpenID configuration
|
||||||
|
OpenID struct {
|
||||||
|
// JWKS validator config.
|
||||||
|
JWKS validator.JWKSArgs `json:"jwks"`
|
||||||
|
} `json:"openid"`
|
||||||
|
|
||||||
|
// External policy enforcements.
|
||||||
|
Policy struct {
|
||||||
|
// OPA configuration.
|
||||||
|
OPA iampolicy.OpaArgs `json:"opa"`
|
||||||
|
|
||||||
|
// Add new external policy enforcements here.
|
||||||
|
} `json:"policy"`
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-147
@@ -20,15 +20,13 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"os"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/hash"
|
|
||||||
"github.com/minio/minio/pkg/quick"
|
"github.com/minio/minio/pkg/quick"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,60 +35,55 @@ const (
|
|||||||
|
|
||||||
// Minio configuration file.
|
// Minio configuration file.
|
||||||
minioConfigFile = "config.json"
|
minioConfigFile = "config.json"
|
||||||
|
|
||||||
|
// Minio backup file
|
||||||
|
minioConfigBackupFile = minioConfigFile + ".backup"
|
||||||
)
|
)
|
||||||
|
|
||||||
func saveServerConfig(objAPI ObjectLayer, config *serverConfig) error {
|
func saveServerConfig(ctx context.Context, objAPI ObjectLayer, config *serverConfig) error {
|
||||||
if err := quick.CheckData(config); err != nil {
|
if err := quick.CheckData(config); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(config)
|
data, err := json.MarshalIndent(config, "", "\t")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
if globalEtcdClient != nil {
|
if globalEtcdClient != nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||||
_, err := globalEtcdClient.Put(ctx, configFile, string(data))
|
_, err = globalEtcdClient.Put(timeoutCtx, configFile, string(data))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return saveConfig(objAPI, configFile, data)
|
// Create a backup of the current config
|
||||||
}
|
oldData, err := readConfig(ctx, objAPI, configFile)
|
||||||
|
if err == nil {
|
||||||
func readConfigEtcd(configFile string) ([]byte, error) {
|
backupConfigFile := path.Join(minioConfigPrefix, minioConfigBackupFile)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
if err = saveConfig(ctx, objAPI, backupConfigFile, oldData); err != nil {
|
||||||
resp, err := globalEtcdClient.Get(ctx, configFile)
|
return err
|
||||||
defer cancel()
|
}
|
||||||
if err != nil {
|
} else {
|
||||||
return nil, err
|
if err != errConfigNotFound {
|
||||||
}
|
return err
|
||||||
if resp.Count == 0 {
|
|
||||||
return nil, errConfigNotFound
|
|
||||||
}
|
|
||||||
for _, ev := range resp.Kvs {
|
|
||||||
if string(ev.Key) == configFile {
|
|
||||||
return ev.Value, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, errConfigNotFound
|
|
||||||
|
// Save the new config in the std config path
|
||||||
|
return saveConfig(ctx, objAPI, configFile, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readServerConfig(ctx context.Context, objAPI ObjectLayer) (*serverConfig, error) {
|
func readServerConfig(ctx context.Context, objAPI ObjectLayer) (*serverConfig, error) {
|
||||||
var configData []byte
|
var configData []byte
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
if globalEtcdClient != nil {
|
if globalEtcdClient != nil {
|
||||||
configData, err = readConfigEtcd(configFile)
|
configData, err = readConfigEtcd(ctx, globalEtcdClient, configFile)
|
||||||
} else {
|
} else {
|
||||||
var reader io.Reader
|
configData, err = readConfig(ctx, objAPI, configFile)
|
||||||
reader, err = readConfig(ctx, objAPI, configFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
configData, err = ioutil.ReadAll(reader)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -105,81 +98,17 @@ func readServerConfig(ctx context.Context, objAPI ObjectLayer) (*serverConfig, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config = &serverConfig{}
|
var config = &serverConfig{}
|
||||||
if err := json.Unmarshal(configData, config); err != nil {
|
if err = json.Unmarshal(configData, config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := quick.CheckData(config); err != nil {
|
if err = quick.CheckData(config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkServerConfigEtcd(configFile string) error {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
||||||
resp, err := globalEtcdClient.Get(ctx, configFile)
|
|
||||||
defer cancel()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if resp.Count == 0 {
|
|
||||||
return errConfigNotFound
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkServerConfig(ctx context.Context, objAPI ObjectLayer) error {
|
|
||||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
|
||||||
if globalEtcdClient != nil {
|
|
||||||
return checkServerConfigEtcd(configFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := objAPI.GetObjectInfo(ctx, minioMetaBucket, configFile); err != nil {
|
|
||||||
if isErrObjectNotFound(err) {
|
|
||||||
return errConfigNotFound
|
|
||||||
}
|
|
||||||
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveConfig(objAPI ObjectLayer, configFile string, data []byte) error {
|
|
||||||
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = objAPI.PutObject(context.Background(), minioMetaBucket, configFile, hashReader, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var errConfigNotFound = errors.New("config file not found")
|
|
||||||
|
|
||||||
func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) (*bytes.Buffer, error) {
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
// Read entire content by setting size to -1
|
|
||||||
if err := objAPI.GetObject(ctx, minioMetaBucket, configFile, 0, -1, &buffer, ""); err != nil {
|
|
||||||
// Ignore if err is ObjectNotFound or IncompleteBody when bucket is not configured with notification
|
|
||||||
if isErrObjectNotFound(err) || isErrIncompleteBody(err) || isInsufficientReadQuorum(err) {
|
|
||||||
return nil, errConfigNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return config not found on empty content.
|
|
||||||
if buffer.Len() == 0 {
|
|
||||||
return nil, errConfigNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return &buffer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigSys - config system.
|
// ConfigSys - config system.
|
||||||
type ConfigSys struct{}
|
type ConfigSys struct{}
|
||||||
|
|
||||||
@@ -193,7 +122,33 @@ func (sys *ConfigSys) Init(objAPI ObjectLayer) error {
|
|||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
return errInvalidArgument
|
return errInvalidArgument
|
||||||
}
|
}
|
||||||
return loadConfig(objAPI)
|
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
defer close(doneCh)
|
||||||
|
|
||||||
|
// Initializing configuration needs a retry mechanism for
|
||||||
|
// the following reasons:
|
||||||
|
// - Read quorum is lost just after the initialization
|
||||||
|
// of the object layer.
|
||||||
|
// - Write quorum not met when upgrading configuration
|
||||||
|
// version is needed.
|
||||||
|
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _ = <-retryTimerCh:
|
||||||
|
err := initConfig(objAPI)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) ||
|
||||||
|
strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) {
|
||||||
|
logger.Info("Waiting for configuration to be initialized..")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfigSys - creates new config system object.
|
// NewConfigSys - creates new config system object.
|
||||||
@@ -201,67 +156,59 @@ func NewConfigSys() *ConfigSys {
|
|||||||
return &ConfigSys{}
|
return &ConfigSys{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrates ${HOME}/.minio/config.json to '<export_path>/.minio.sys/config/minio.json'
|
|
||||||
func migrateConfigToMinioSys() error {
|
|
||||||
// Construct path to config.json for the given bucket.
|
|
||||||
configFile := path.Join(bucketConfigPrefix, minioConfigFile)
|
|
||||||
transactionConfigFile := configFile + ".transaction"
|
|
||||||
|
|
||||||
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
|
|
||||||
// and configFile, take a transaction lock to avoid race.
|
|
||||||
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
|
|
||||||
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer objLock.Unlock()
|
|
||||||
|
|
||||||
// Verify if backend already has the file.
|
|
||||||
if err := checkServerConfig(context.Background(), newObjectLayerFn()); err != errConfigNotFound {
|
|
||||||
return err
|
|
||||||
} // if errConfigNotFound proceed to migrate..
|
|
||||||
|
|
||||||
var config = &serverConfig{}
|
|
||||||
if _, err := Load(getConfigFile(), config); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return saveServerConfig(newObjectLayerFn(), config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize and load config from remote etcd or local config directory
|
// Initialize and load config from remote etcd or local config directory
|
||||||
func initConfig() {
|
func initConfig(objAPI ObjectLayer) error {
|
||||||
|
if objAPI == nil {
|
||||||
|
return errServerNotInitialized
|
||||||
|
}
|
||||||
|
|
||||||
if globalEtcdClient != nil {
|
if globalEtcdClient != nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
resp, err := globalEtcdClient.Get(ctx, getConfigFile())
|
resp, err := globalEtcdClient.Get(ctx, getConfigFile())
|
||||||
cancel()
|
cancel()
|
||||||
if err == nil && resp.Count > 0 {
|
if err == nil && resp.Count > 0 {
|
||||||
logger.FatalIf(migrateConfig(), "Config migration failed")
|
if err = migrateConfig(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrates etcd ${HOME}/.minio/config.json to '/config/config.json'
|
||||||
|
if err := migrateConfigToMinioSys(objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if isFile(getConfigFile()) {
|
if isFile(getConfigFile()) {
|
||||||
logger.FatalIf(migrateConfig(), "Config migration failed")
|
if err := migrateConfig(); err != nil {
|
||||||
|
return err
|
||||||
// Migrates ${HOME}/.minio/config.json to '<export_path>/.minio.sys/config/config.json'
|
|
||||||
if err := migrateConfigToMinioSys(); err != nil {
|
|
||||||
logger.Fatal(err, "Unable to migrate 'config.json' to '.minio.sys/config/config.json'")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// Migrates ${HOME}/.minio/config.json or config.json.deprecated
|
||||||
objAPI := newObjectLayerFn()
|
// to '<export_path>/.minio.sys/config/config.json'
|
||||||
if objAPI == nil {
|
// ignore if the file doesn't exist.
|
||||||
logger.FatalIf(errServerNotInitialized, "Server is not initialized yet unable to proceed")
|
if err := migrateConfigToMinioSys(objAPI); err != nil && !os.IsNotExist(err) {
|
||||||
}
|
return err
|
||||||
if err := checkServerConfig(context.Background(), objAPI); err != nil {
|
|
||||||
if err == errConfigNotFound {
|
|
||||||
// Config file does not exist, we create it fresh and return upon success.
|
|
||||||
logger.FatalIf(newConfig(objAPI), "Unable to initialize minio config for the first time")
|
|
||||||
logger.Info("Created minio configuration file successfully at " + getConfigDir())
|
|
||||||
} else {
|
|
||||||
logger.FatalIf(err, "Unable to load the configuration file")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.FatalIf(migrateMinioSysConfig(objAPI), "Config migration failed for minio.sys config")
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
|
||||||
logger.FatalIf(loadConfig(objAPI), "Unable to load the configuration file")
|
// Watch config for changes and reloads them in-memory.
|
||||||
|
go watchConfig(objAPI, configFile, loadConfig)
|
||||||
|
|
||||||
|
if err := checkConfig(context.Background(), objAPI, configFile); err != nil {
|
||||||
|
if err == errConfigNotFound {
|
||||||
|
// Config file does not exist, we create it fresh and return upon success.
|
||||||
|
if err = newSrvConfig(objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := migrateMinioSysConfig(objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadConfig(objAPI)
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-67
@@ -17,90 +17,52 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Writes S3 compatible copy part range error.
|
// Writes S3 compatible copy part range error.
|
||||||
func writeCopyPartErr(w http.ResponseWriter, err error, url *url.URL) {
|
func writeCopyPartErr(w http.ResponseWriter, err error, url *url.URL, browser bool) {
|
||||||
switch err {
|
switch err {
|
||||||
case errInvalidRange:
|
case errInvalidRange:
|
||||||
writeErrorResponse(w, ErrInvalidCopyPartRange, url)
|
writeErrorResponse(w, ErrInvalidCopyPartRange, url, browser)
|
||||||
return
|
return
|
||||||
case errInvalidRangeSource:
|
case errInvalidRangeSource:
|
||||||
writeErrorResponse(w, ErrInvalidCopyPartRangeSource, url)
|
writeErrorResponse(w, ErrInvalidCopyPartRangeSource, url, browser)
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
writeErrorResponse(w, ErrInternalError, url)
|
writeErrorResponse(w, ErrInternalError, url, browser)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses x-amz-copy-source-range for CopyObjectPart API. Specifically written to
|
// Parses x-amz-copy-source-range for CopyObjectPart API. Its behavior
|
||||||
// differentiate the behavior between regular httpRange header v/s x-amz-copy-source-range.
|
// is different from regular HTTP range header. It only supports the
|
||||||
// The range of bytes to copy from the source object. The range value must use the form
|
// form `bytes=first-last` where first and last are zero-based byte
|
||||||
// bytes=first-last, where the first and last are the zero-based byte offsets to copy.
|
// offsets. See
|
||||||
// For example, bytes=0-9 indicates that you want to copy the first ten bytes of the source.
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadUploadPartCopy.html
|
// http://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadUploadPartCopy.html
|
||||||
func parseCopyPartRange(rangeString string, resourceSize int64) (hrange *httpRange, err error) {
|
// for full details. This function treats an empty rangeString as
|
||||||
// Return error if given range string doesn't start with byte range prefix.
|
// referring to the whole resource.
|
||||||
if !strings.HasPrefix(rangeString, byteRangePrefix) {
|
func parseCopyPartRangeSpec(rangeString string) (hrange *HTTPRangeSpec, err error) {
|
||||||
return nil, fmt.Errorf("'%s' does not start with '%s'", rangeString, byteRangePrefix)
|
hrange, err = parseRequestRangeSpec(rangeString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if hrange.IsSuffixLength || hrange.Start < 0 || hrange.End < 0 {
|
||||||
// Trim byte range prefix.
|
|
||||||
byteRangeString := strings.TrimPrefix(rangeString, byteRangePrefix)
|
|
||||||
|
|
||||||
// Check if range string contains delimiter '-', else return error. eg. "bytes=8"
|
|
||||||
sepIndex := strings.Index(byteRangeString, "-")
|
|
||||||
if sepIndex == -1 {
|
|
||||||
return nil, errInvalidRange
|
return nil, errInvalidRange
|
||||||
}
|
}
|
||||||
|
return hrange, nil
|
||||||
offsetBeginString := byteRangeString[:sepIndex]
|
}
|
||||||
offsetBegin := int64(-1)
|
|
||||||
// Convert offsetBeginString only if its not empty.
|
// checkCopyPartRangeWithSize adds more check to the range string in case of
|
||||||
if len(offsetBeginString) > 0 {
|
// copy object part. This API requires having specific start and end range values
|
||||||
if !validBytePos.MatchString(offsetBeginString) {
|
// e.g. 'bytes=3-10'. Other use cases will be rejected.
|
||||||
return nil, errInvalidRange
|
func checkCopyPartRangeWithSize(rs *HTTPRangeSpec, resourceSize int64) (err error) {
|
||||||
}
|
if rs == nil {
|
||||||
if offsetBegin, err = strconv.ParseInt(offsetBeginString, 10, 64); err != nil {
|
return nil
|
||||||
return nil, errInvalidRange
|
}
|
||||||
}
|
if rs.IsSuffixLength || rs.Start >= resourceSize || rs.End >= resourceSize {
|
||||||
}
|
return errInvalidRangeSource
|
||||||
|
}
|
||||||
offsetEndString := byteRangeString[sepIndex+1:]
|
return nil
|
||||||
offsetEnd := int64(-1)
|
|
||||||
// Convert offsetEndString only if its not empty.
|
|
||||||
if len(offsetEndString) > 0 {
|
|
||||||
if !validBytePos.MatchString(offsetEndString) {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
if offsetEnd, err = strconv.ParseInt(offsetEndString, 10, 64); err != nil {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// rangeString contains first byte positions. eg. "bytes=2-" or
|
|
||||||
// rangeString contains last bye positions. eg. "bytes=-2"
|
|
||||||
if offsetBegin == -1 || offsetEnd == -1 {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last byte position should not be greater than first byte
|
|
||||||
// position. eg. "bytes=5-2"
|
|
||||||
if offsetBegin > offsetEnd {
|
|
||||||
return nil, errInvalidRange
|
|
||||||
}
|
|
||||||
|
|
||||||
// First and last byte positions should not be >= resourceSize.
|
|
||||||
if offsetBegin >= resourceSize || offsetEnd >= resourceSize {
|
|
||||||
return nil, errInvalidRangeSource
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success..
|
|
||||||
return &httpRange{offsetBegin, offsetEnd, resourceSize}, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-17
@@ -19,35 +19,37 @@ package cmd
|
|||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
// Test parseCopyPartRange()
|
// Test parseCopyPartRange()
|
||||||
func TestParseCopyPartRange(t *testing.T) {
|
func TestParseCopyPartRangeSpec(t *testing.T) {
|
||||||
// Test success cases.
|
// Test success cases.
|
||||||
successCases := []struct {
|
successCases := []struct {
|
||||||
rangeString string
|
rangeString string
|
||||||
offsetBegin int64
|
offsetBegin int64
|
||||||
offsetEnd int64
|
offsetEnd int64
|
||||||
length int64
|
|
||||||
}{
|
}{
|
||||||
{"bytes=2-5", 2, 5, 4},
|
{"bytes=2-5", 2, 5},
|
||||||
{"bytes=2-9", 2, 9, 8},
|
{"bytes=2-9", 2, 9},
|
||||||
{"bytes=2-2", 2, 2, 1},
|
{"bytes=2-2", 2, 2},
|
||||||
{"bytes=0000-0006", 0, 6, 7},
|
{"bytes=0000-0006", 0, 6},
|
||||||
}
|
}
|
||||||
|
objectSize := int64(10)
|
||||||
|
|
||||||
for _, successCase := range successCases {
|
for _, successCase := range successCases {
|
||||||
hrange, err := parseCopyPartRange(successCase.rangeString, 10)
|
rs, err := parseCopyPartRangeSpec(successCase.rangeString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected: <nil>, got: %s", err)
|
t.Fatalf("expected: <nil>, got: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if hrange.offsetBegin != successCase.offsetBegin {
|
start, length, err1 := rs.GetOffsetLength(objectSize)
|
||||||
t.Fatalf("expected: %d, got: %d", successCase.offsetBegin, hrange.offsetBegin)
|
if err1 != nil {
|
||||||
|
t.Fatalf("expected: <nil>, got: %s", err1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if hrange.offsetEnd != successCase.offsetEnd {
|
if start != successCase.offsetBegin {
|
||||||
t.Fatalf("expected: %d, got: %d", successCase.offsetEnd, hrange.offsetEnd)
|
t.Fatalf("expected: %d, got: %d", successCase.offsetBegin, start)
|
||||||
}
|
}
|
||||||
if hrange.getLength() != successCase.length {
|
|
||||||
t.Fatalf("expected: %d, got: %d", successCase.length, hrange.getLength())
|
if start+length-1 != successCase.offsetEnd {
|
||||||
|
t.Fatalf("expected: %d, got: %d", successCase.offsetEnd, start+length-1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,15 +61,16 @@ func TestParseCopyPartRange(t *testing.T) {
|
|||||||
"bytes=2-+5",
|
"bytes=2-+5",
|
||||||
"bytes=2--5",
|
"bytes=2--5",
|
||||||
"bytes=-",
|
"bytes=-",
|
||||||
"",
|
|
||||||
"2-5",
|
"2-5",
|
||||||
"bytes = 2-5",
|
"bytes = 2-5",
|
||||||
"bytes=2 - 5",
|
"bytes=2 - 5",
|
||||||
"bytes=0-0,-1",
|
"bytes=0-0,-1",
|
||||||
"bytes=2-5 ",
|
"bytes=2-5 ",
|
||||||
|
"bytes=-1",
|
||||||
|
"bytes=1-",
|
||||||
}
|
}
|
||||||
for _, rangeString := range invalidRangeStrings {
|
for _, rangeString := range invalidRangeStrings {
|
||||||
if _, err := parseCopyPartRange(rangeString, 10); err == nil {
|
if _, err := parseCopyPartRangeSpec(rangeString); err == nil {
|
||||||
t.Fatalf("expected: an error, got: <nil> for range %s", rangeString)
|
t.Fatalf("expected: an error, got: <nil> for range %s", rangeString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,8 +81,14 @@ func TestParseCopyPartRange(t *testing.T) {
|
|||||||
"bytes=20-30",
|
"bytes=20-30",
|
||||||
}
|
}
|
||||||
for _, rangeString := range errorRangeString {
|
for _, rangeString := range errorRangeString {
|
||||||
if _, err := parseCopyPartRange(rangeString, 10); err != errInvalidRangeSource {
|
rs, err := parseCopyPartRangeSpec(rangeString)
|
||||||
t.Fatalf("expected: %s, got: %s", errInvalidRangeSource, err)
|
if err == nil {
|
||||||
|
err1 := checkCopyPartRangeWithSize(rs, objectSize)
|
||||||
|
if err1 != errInvalidRangeSource {
|
||||||
|
t.Fatalf("expected: %s, got: %s", errInvalidRangeSource, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Fatalf("expected: %s, got: <nil>", errInvalidRangeSource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ package crypto
|
|||||||
|
|
||||||
// KMSConfig has the KMS config for hashicorp vault
|
// KMSConfig has the KMS config for hashicorp vault
|
||||||
type KMSConfig struct {
|
type KMSConfig struct {
|
||||||
Vault VaultConfig `json:"vault"`
|
AutoEncryption bool `json:"-"`
|
||||||
|
Vault VaultConfig `json:"vault"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ var (
|
|||||||
// base64-encoded string or not 256 bits long.
|
// base64-encoded string or not 256 bits long.
|
||||||
ErrInvalidCustomerKey = errors.New("The SSE-C client key is invalid")
|
ErrInvalidCustomerKey = errors.New("The SSE-C client key is invalid")
|
||||||
|
|
||||||
|
// ErrSecretKeyMismatch indicates that the provided secret key (SSE-C client key / SSE-S3 KMS key)
|
||||||
|
// does not match the secret key used during encrypting the object.
|
||||||
|
ErrSecretKeyMismatch = errors.New("The secret key does not match the secret key used during upload")
|
||||||
|
|
||||||
// ErrCustomerKeyMD5Mismatch indicates that the SSE-C key MD5 does not match the
|
// ErrCustomerKeyMD5Mismatch indicates that the SSE-C key MD5 does not match the
|
||||||
// computed MD5 sum. This means that the client provided either the wrong key for
|
// computed MD5 sum. This means that the client provided either the wrong key for
|
||||||
// a certain MD5 checksum or the wrong MD5 for a certain key.
|
// a certain MD5 checksum or the wrong MD5 for a certain key.
|
||||||
|
|||||||
@@ -74,6 +74,14 @@ const (
|
|||||||
SSEAlgorithmKMS = "aws:kms"
|
SSEAlgorithmKMS = "aws:kms"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RemoveSensitiveHeaders removes confidential encryption
|
||||||
|
// information - e.g. the SSE-C key - from the HTTP headers.
|
||||||
|
// It has the same semantics as RemoveSensitiveEntires.
|
||||||
|
func RemoveSensitiveHeaders(h http.Header) {
|
||||||
|
h.Del(SSECKey)
|
||||||
|
h.Del(SSECopyKey)
|
||||||
|
}
|
||||||
|
|
||||||
// S3 represents AWS SSE-S3. It provides functionality to handle
|
// S3 represents AWS SSE-S3. It provides functionality to handle
|
||||||
// SSE-S3 requests.
|
// SSE-S3 requests.
|
||||||
var S3 = s3{}
|
var S3 = s3{}
|
||||||
@@ -164,7 +172,6 @@ func (ssecCopy) IsRequested(h http.Header) bool {
|
|||||||
// ParseHTTP parses the SSE-C headers and returns the SSE-C client key
|
// ParseHTTP parses the SSE-C headers and returns the SSE-C client key
|
||||||
// on success. SSE-C copy headers are ignored.
|
// on success. SSE-C copy headers are ignored.
|
||||||
func (ssec) ParseHTTP(h http.Header) (key [32]byte, err error) {
|
func (ssec) ParseHTTP(h http.Header) (key [32]byte, err error) {
|
||||||
defer h.Del(SSECKey) // remove SSE-C key from headers after parsing
|
|
||||||
if h.Get(SSECAlgorithm) != SSEAlgorithmAES256 {
|
if h.Get(SSECAlgorithm) != SSEAlgorithmAES256 {
|
||||||
return key, ErrInvalidCustomerAlgorithm
|
return key, ErrInvalidCustomerAlgorithm
|
||||||
}
|
}
|
||||||
@@ -190,7 +197,6 @@ func (ssec) ParseHTTP(h http.Header) (key [32]byte, err error) {
|
|||||||
// ParseHTTP parses the SSE-C copy headers and returns the SSE-C client key
|
// ParseHTTP parses the SSE-C copy headers and returns the SSE-C client key
|
||||||
// on success. Regular SSE-C headers are ignored.
|
// on success. Regular SSE-C headers are ignored.
|
||||||
func (ssecCopy) ParseHTTP(h http.Header) (key [32]byte, err error) {
|
func (ssecCopy) ParseHTTP(h http.Header) (key [32]byte, err error) {
|
||||||
defer h.Del(SSECopyKey) // remove SSE-C copy key of source object from headers after parsing
|
|
||||||
if h.Get(SSECopyAlgorithm) != SSEAlgorithmAES256 {
|
if h.Get(SSECopyAlgorithm) != SSEAlgorithmAES256 {
|
||||||
return key, ErrInvalidCustomerAlgorithm
|
return key, ErrInvalidCustomerAlgorithm
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-3
@@ -16,6 +16,7 @@ package crypto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -250,9 +251,6 @@ func TestSSECParse(t *testing.T) {
|
|||||||
if err == nil && key == zeroKey {
|
if err == nil && key == zeroKey {
|
||||||
t.Errorf("Test %d: parsed client key is zero key", i)
|
t.Errorf("Test %d: parsed client key is zero key", i)
|
||||||
}
|
}
|
||||||
if _, ok := test.Header[SSECKey]; ok {
|
|
||||||
t.Errorf("Test %d: client key is not removed from HTTP headers after parsing", i)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,3 +335,102 @@ func TestSSECopyParse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var removeSensitiveHeadersTests = []struct {
|
||||||
|
Header, ExpectedHeader http.Header
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
SSECKey: []string{""},
|
||||||
|
SSECopyKey: []string{""},
|
||||||
|
},
|
||||||
|
ExpectedHeader: http.Header{},
|
||||||
|
},
|
||||||
|
{ // Standard SSE-C request headers
|
||||||
|
Header: http.Header{
|
||||||
|
SSECAlgorithm: []string{SSEAlgorithmAES256},
|
||||||
|
SSECKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedHeader: http.Header{
|
||||||
|
SSECAlgorithm: []string{SSEAlgorithmAES256},
|
||||||
|
SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // Standard SSE-C + SSE-C-copy request headers
|
||||||
|
Header: http.Header{
|
||||||
|
SSECAlgorithm: []string{SSEAlgorithmAES256},
|
||||||
|
SSECKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
SSECopyKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
SSECopyKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedHeader: http.Header{
|
||||||
|
SSECAlgorithm: []string{SSEAlgorithmAES256},
|
||||||
|
SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
SSECopyKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // Standard SSE-C + metadata request headers
|
||||||
|
Header: http.Header{
|
||||||
|
SSECAlgorithm: []string{SSEAlgorithmAES256},
|
||||||
|
SSECKey: []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
"X-Amz-Meta-Test-1": []string{"Test-1"},
|
||||||
|
},
|
||||||
|
ExpectedHeader: http.Header{
|
||||||
|
SSECAlgorithm: []string{SSEAlgorithmAES256},
|
||||||
|
SSECKeyMD5: []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
"X-Amz-Meta-Test-1": []string{"Test-1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveSensitiveHeaders(t *testing.T) {
|
||||||
|
isEqual := func(x, y http.Header) bool {
|
||||||
|
if len(x) != len(y) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for k, v := range x {
|
||||||
|
u, ok := y[k]
|
||||||
|
if !ok || len(v) != len(u) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sort.Strings(v)
|
||||||
|
sort.Strings(u)
|
||||||
|
for j := range v {
|
||||||
|
if v[j] != u[j] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
areKeysEqual := func(h http.Header, metadata map[string]string) bool {
|
||||||
|
if len(h) != len(metadata) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for k := range h {
|
||||||
|
if _, ok := metadata[k]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range removeSensitiveHeadersTests {
|
||||||
|
metadata := make(map[string]string, len(test.Header))
|
||||||
|
for k := range test.Header {
|
||||||
|
metadata[k] = "" // set metadata key - we don't care about the value
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveSensitiveHeaders(test.Header)
|
||||||
|
if !isEqual(test.ExpectedHeader, test.Header) {
|
||||||
|
t.Errorf("Test %d: filtered headers do not match expected headers - got: %v , want: %v", i, test.Header, test.ExpectedHeader)
|
||||||
|
}
|
||||||
|
RemoveSensitiveEntries(metadata)
|
||||||
|
if !areKeysEqual(test.ExpectedHeader, metadata) {
|
||||||
|
t.Errorf("Test %d: filtered headers do not match expected headers - got: %v , want: %v", i, test.Header, test.ExpectedHeader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+35
-1
@@ -124,7 +124,7 @@ func (key *ObjectKey) Unseal(extKey [32]byte, sealedKey SealedKey, domain, bucke
|
|||||||
}
|
}
|
||||||
|
|
||||||
if n, err := sio.Decrypt(&decryptedKey, bytes.NewReader(sealedKey.Key[:]), unsealConfig); n != 32 || err != nil {
|
if n, err := sio.Decrypt(&decryptedKey, bytes.NewReader(sealedKey.Key[:]), unsealConfig); n != 32 || err != nil {
|
||||||
return err // TODO(aead): upgrade sio to use sio.Error
|
return ErrSecretKeyMismatch
|
||||||
}
|
}
|
||||||
copy(key[:], decryptedKey.Bytes())
|
copy(key[:], decryptedKey.Bytes())
|
||||||
return nil
|
return nil
|
||||||
@@ -140,3 +140,37 @@ func (key ObjectKey) DerivePartKey(id uint32) (partKey [32]byte) {
|
|||||||
mac.Sum(partKey[:0])
|
mac.Sum(partKey[:0])
|
||||||
return partKey
|
return partKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SealETag seals the etag using the object key.
|
||||||
|
// It does not encrypt empty ETags because such ETags indicate
|
||||||
|
// that the S3 client hasn't sent an ETag = MD5(object) and
|
||||||
|
// the backend can pick an ETag value.
|
||||||
|
func (key ObjectKey) SealETag(etag []byte) []byte {
|
||||||
|
if len(etag) == 0 { // don't encrypt empty ETag - only if client sent ETag = MD5(object)
|
||||||
|
return etag
|
||||||
|
}
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
mac := hmac.New(sha256.New, key[:])
|
||||||
|
mac.Write([]byte("SSE-etag"))
|
||||||
|
if _, err := sio.Encrypt(&buffer, bytes.NewReader(etag), sio.Config{Key: mac.Sum(nil)}); err != nil {
|
||||||
|
logger.CriticalIf(context.Background(), errors.New("Unable to encrypt ETag using object key"))
|
||||||
|
}
|
||||||
|
return buffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsealETag unseals the etag using the provided object key.
|
||||||
|
// It does not try to decrypt the ETag if len(etag) == 16
|
||||||
|
// because such ETags indicate that the S3 client hasn't sent
|
||||||
|
// an ETag = MD5(object) and the backend has picked an ETag value.
|
||||||
|
func (key ObjectKey) UnsealETag(etag []byte) ([]byte, error) {
|
||||||
|
if !IsETagSealed(etag) {
|
||||||
|
return etag, nil
|
||||||
|
}
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
mac := hmac.New(sha256.New, key[:])
|
||||||
|
mac.Write([]byte("SSE-etag"))
|
||||||
|
if _, err := sio.Decrypt(&buffer, bytes.NewReader(etag), sio.Config{Key: mac.Sum(nil)}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -166,3 +166,31 @@ func TestDerivePartKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sealUnsealETagTests = []string{
|
||||||
|
"",
|
||||||
|
"90682b8e8cc7609c",
|
||||||
|
"90682b8e8cc7609c4671e1d64c73fc30",
|
||||||
|
"90682b8e8cc7609c4671e1d64c73fc307fb3104f",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSealETag(t *testing.T) {
|
||||||
|
var key ObjectKey
|
||||||
|
for i := range key {
|
||||||
|
key[i] = byte(i)
|
||||||
|
}
|
||||||
|
for i, etag := range sealUnsealETagTests {
|
||||||
|
tag, err := hex.DecodeString(etag)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: failed to decode etag: %s", i, err)
|
||||||
|
}
|
||||||
|
sealedETag := key.SealETag(tag)
|
||||||
|
unsealedETag, err := key.UnsealETag(sealedETag)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: failed to decrypt etag: %s", i, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(unsealedETag, tag) {
|
||||||
|
t.Errorf("Test %d: unsealed etag does not match: got %s - want %s", i, hex.EncodeToString(unsealedETag), etag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,26 @@ func IsMultiPart(metadata map[string]string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveSensitiveEntries removes confidential encryption
|
||||||
|
// information - e.g. the SSE-C key - from the metadata map.
|
||||||
|
// It has the same semantics as RemoveSensitiveHeaders.
|
||||||
|
func RemoveSensitiveEntries(metadata map[string]string) { // The functions is tested in TestRemoveSensitiveHeaders for compatibility reasons
|
||||||
|
delete(metadata, SSECKey)
|
||||||
|
delete(metadata, SSECopyKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveInternalEntries removes all crypto-specific internal
|
||||||
|
// metadata entries from the metadata map.
|
||||||
|
func RemoveInternalEntries(metadata map[string]string) {
|
||||||
|
delete(metadata, SSEMultipart)
|
||||||
|
delete(metadata, SSEIV)
|
||||||
|
delete(metadata, SSESealAlgorithm)
|
||||||
|
delete(metadata, SSECSealedKey)
|
||||||
|
delete(metadata, S3SealedKey)
|
||||||
|
delete(metadata, S3KMSKeyID)
|
||||||
|
delete(metadata, S3KMSSealedKey)
|
||||||
|
}
|
||||||
|
|
||||||
// IsEncrypted returns true if the object metadata indicates
|
// IsEncrypted returns true if the object metadata indicates
|
||||||
// that it was uploaded using some form of server-side-encryption.
|
// that it was uploaded using some form of server-side-encryption.
|
||||||
//
|
//
|
||||||
@@ -211,3 +231,6 @@ func (ssec) ParseMetadata(metadata map[string]string) (sealedKey SealedKey, err
|
|||||||
copy(sealedKey.Key[:], encryptedKey)
|
copy(sealedKey.Key[:], encryptedKey)
|
||||||
return sealedKey, nil
|
return sealedKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsETagSealed returns true if the etag seems to be encrypted.
|
||||||
|
func IsETagSealed(etag []byte) bool { return len(etag) > 16 }
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package crypto
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
@@ -326,15 +327,20 @@ func TestS3CreateMetadata(t *testing.T) {
|
|||||||
_ = S3.CreateMetadata(nil, "", []byte{}, SealedKey{Algorithm: InsecureSealAlgorithm})
|
_ = S3.CreateMetadata(nil, "", []byte{}, SealedKey{Algorithm: InsecureSealAlgorithm})
|
||||||
}
|
}
|
||||||
|
|
||||||
var ssecCreateMetadataTests = []SealedKey{
|
var ssecCreateMetadataTests = []struct {
|
||||||
{Algorithm: SealAlgorithm},
|
KeyID string
|
||||||
{IV: [32]byte{0xff}, Key: [64]byte{0x7e}, Algorithm: SealAlgorithm},
|
SealedDataKey []byte
|
||||||
|
SealedKey SealedKey
|
||||||
|
}{
|
||||||
|
{KeyID: "", SealedDataKey: make([]byte, 48), SealedKey: SealedKey{Algorithm: SealAlgorithm}},
|
||||||
|
{KeyID: "cafebabe", SealedDataKey: make([]byte, 48), SealedKey: SealedKey{Algorithm: SealAlgorithm}},
|
||||||
|
{KeyID: "deadbeef", SealedDataKey: make([]byte, 32), SealedKey: SealedKey{IV: [32]byte{0xf7}, Key: [64]byte{0xea}, Algorithm: SealAlgorithm}},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSSECCreateMetadata(t *testing.T) {
|
func TestSSECCreateMetadata(t *testing.T) {
|
||||||
defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
|
defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
|
||||||
logger.Disable = true
|
logger.Disable = true
|
||||||
for i, test := range s3CreateMetadataTests {
|
for i, test := range ssecCreateMetadataTests {
|
||||||
metadata := SSEC.CreateMetadata(nil, test.SealedKey)
|
metadata := SSEC.CreateMetadata(nil, test.SealedKey)
|
||||||
sealedKey, err := SSEC.ParseMetadata(metadata)
|
sealedKey, err := SSEC.ParseMetadata(metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -359,3 +365,75 @@ func TestSSECCreateMetadata(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
_ = SSEC.CreateMetadata(nil, SealedKey{Algorithm: InsecureSealAlgorithm})
|
_ = SSEC.CreateMetadata(nil, SealedKey{Algorithm: InsecureSealAlgorithm})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isETagSealedTests = []struct {
|
||||||
|
ETag string
|
||||||
|
IsSealed bool
|
||||||
|
}{
|
||||||
|
{ETag: "", IsSealed: false}, // 0
|
||||||
|
{ETag: "90682b8e8cc7609c4671e1d64c73fc30", IsSealed: false}, // 1
|
||||||
|
{ETag: "f201040c9dc593e39ea004dc1323699bcd", IsSealed: true}, // 2 not valid ciphertext but looks like sealed ETag
|
||||||
|
{ETag: "20000f00fba2ee2ae4845f725964eeb9e092edfabc7ab9f9239e8344341f769a51ce99b4801b0699b92b16a72fa94972", IsSealed: true}, // 3
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsETagSealed(t *testing.T) {
|
||||||
|
for i, test := range isETagSealedTests {
|
||||||
|
etag, err := hex.DecodeString(test.ETag)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: failed to decode etag: %s", i, err)
|
||||||
|
}
|
||||||
|
if sealed := IsETagSealed(etag); sealed != test.IsSealed {
|
||||||
|
t.Errorf("Test %d: got %v - want %v", i, sealed, test.IsSealed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeInternalEntriesTests = []struct {
|
||||||
|
Metadata, Expected map[string]string
|
||||||
|
}{
|
||||||
|
{ // 0
|
||||||
|
Metadata: map[string]string{
|
||||||
|
SSEMultipart: "",
|
||||||
|
SSEIV: "",
|
||||||
|
SSESealAlgorithm: "",
|
||||||
|
SSECSealedKey: "",
|
||||||
|
S3SealedKey: "",
|
||||||
|
S3KMSKeyID: "",
|
||||||
|
S3KMSSealedKey: "",
|
||||||
|
},
|
||||||
|
Expected: map[string]string{},
|
||||||
|
},
|
||||||
|
{ // 1
|
||||||
|
Metadata: map[string]string{
|
||||||
|
SSEMultipart: "",
|
||||||
|
SSEIV: "",
|
||||||
|
"X-Amz-Meta-A": "X",
|
||||||
|
"X-Minio-Internal-B": "Y",
|
||||||
|
},
|
||||||
|
Expected: map[string]string{
|
||||||
|
"X-Amz-Meta-A": "X",
|
||||||
|
"X-Minio-Internal-B": "Y",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveInternalEntries(t *testing.T) {
|
||||||
|
isEqual := func(x, y map[string]string) bool {
|
||||||
|
if len(x) != len(y) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for k, v := range x {
|
||||||
|
if u, ok := y[k]; !ok || v != u {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range removeInternalEntriesTests {
|
||||||
|
RemoveInternalEntries(test.Metadata)
|
||||||
|
if !isEqual(test.Metadata, test.Expected) {
|
||||||
|
t.Errorf("Test %d: got %v - want %v", i, test.Metadata, test.Expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/ioutil"
|
"github.com/minio/minio/pkg/ioutil"
|
||||||
@@ -69,10 +71,59 @@ const (
|
|||||||
// domain is "SSE-S3".
|
// domain is "SSE-S3".
|
||||||
func (s3) String() string { return "SSE-S3" }
|
func (s3) String() string { return "SSE-S3" }
|
||||||
|
|
||||||
|
// UnsealObjectKey extracts and decrypts the sealed object key
|
||||||
|
// from the metadata using KMS and returns the decrypted object
|
||||||
|
// key.
|
||||||
|
func (sse s3) UnsealObjectKey(kms KMS, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
||||||
|
keyID, kmsKey, sealedKey, err := sse.ParseMetadata(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
unsealKey, err := kms.UnsealKey(keyID, kmsKey, Context{bucket: path.Join(bucket, object)})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = key.Unseal(unsealKey, sealedKey, sse.String(), bucket, object)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// String returns the SSE domain as string. For SSE-C the
|
// String returns the SSE domain as string. For SSE-C the
|
||||||
// domain is "SSE-C".
|
// domain is "SSE-C".
|
||||||
func (ssec) String() string { return "SSE-C" }
|
func (ssec) String() string { return "SSE-C" }
|
||||||
|
|
||||||
|
// UnsealObjectKey extracts and decrypts the sealed object key
|
||||||
|
// from the metadata using the SSE-C client key of the HTTP headers
|
||||||
|
// and returns the decrypted object key.
|
||||||
|
func (sse ssec) UnsealObjectKey(h http.Header, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
||||||
|
clientKey, err := sse.ParseHTTP(h)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return unsealObjectKey(clientKey, metadata, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsealObjectKey extracts and decrypts the sealed object key
|
||||||
|
// from the metadata using the SSE-Copy client key of the HTTP headers
|
||||||
|
// and returns the decrypted object key.
|
||||||
|
func (sse ssecCopy) UnsealObjectKey(h http.Header, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
||||||
|
clientKey, err := sse.ParseHTTP(h)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return unsealObjectKey(clientKey, metadata, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsealObjectKey decrypts and returns the sealed object key
|
||||||
|
// from the metadata using the SSE-C client key.
|
||||||
|
func unsealObjectKey(clientKey [32]byte, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
||||||
|
sealedKey, err := SSEC.ParseMetadata(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = key.Unseal(clientKey, sealedKey, SSEC.String(), bucket, object)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// EncryptSinglePart encrypts an io.Reader which must be the
|
// EncryptSinglePart encrypts an io.Reader which must be the
|
||||||
// the body of a single-part PUT request.
|
// the body of a single-part PUT request.
|
||||||
func EncryptSinglePart(r io.Reader, key ObjectKey) io.Reader {
|
func EncryptSinglePart(r io.Reader, key ObjectKey) io.Reader {
|
||||||
|
|||||||
+196
-1
@@ -14,7 +14,10 @@
|
|||||||
|
|
||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestS3String(t *testing.T) {
|
func TestS3String(t *testing.T) {
|
||||||
const Domain = "SSE-S3"
|
const Domain = "SSE-S3"
|
||||||
@@ -29,3 +32,195 @@ func TestSSECString(t *testing.T) {
|
|||||||
t.Errorf("SSEC's string method returns wrong domain: got '%s' - want '%s'", domain, Domain)
|
t.Errorf("SSEC's string method returns wrong domain: got '%s' - want '%s'", domain, Domain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ssecUnsealObjectKeyTests = []struct {
|
||||||
|
Headers http.Header
|
||||||
|
Bucket, Object string
|
||||||
|
Metadata map[string]string
|
||||||
|
|
||||||
|
ExpectedErr error
|
||||||
|
}{
|
||||||
|
{ // 0 - Valid HTTP headers and valid metadata entries for bucket/object
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: nil,
|
||||||
|
},
|
||||||
|
{ // 1 - Valid HTTP headers but invalid metadata entries for bucket/object2
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object2",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrSecretKeyMismatch,
|
||||||
|
},
|
||||||
|
{ // 2 - Valid HTTP headers but invalid metadata entries for bucket/object
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: errMissingInternalSealAlgorithm,
|
||||||
|
},
|
||||||
|
{ // 3 - Invalid HTTP headers for valid metadata entries for bucket/object
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrMissingCustomerKeyMD5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSECUnsealObjectKey(t *testing.T) {
|
||||||
|
for i, test := range ssecUnsealObjectKeyTests {
|
||||||
|
if _, err := SSEC.UnsealObjectKey(test.Headers, test.Metadata, test.Bucket, test.Object); err != test.ExpectedErr {
|
||||||
|
t.Errorf("Test %d: got: %v - want: %v", i, err, test.ExpectedErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sseCopyUnsealObjectKeyTests = []struct {
|
||||||
|
Headers http.Header
|
||||||
|
Bucket, Object string
|
||||||
|
Metadata map[string]string
|
||||||
|
|
||||||
|
ExpectedErr error
|
||||||
|
}{
|
||||||
|
{ // 0 - Valid HTTP headers and valid metadata entries for bucket/object
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: nil,
|
||||||
|
},
|
||||||
|
{ // 1 - Valid HTTP headers but invalid metadata entries for bucket/object2
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object2",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrSecretKeyMismatch,
|
||||||
|
},
|
||||||
|
{ // 2 - Valid HTTP headers but invalid metadata entries for bucket/object
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: errMissingInternalSealAlgorithm,
|
||||||
|
},
|
||||||
|
{ // 3 - Invalid HTTP headers for valid metadata entries for bucket/object
|
||||||
|
Headers: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
},
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "IAAfAMBdYor5tf/UlVaQvwYlw5yKbPBeQqfygqsfHqhu1wHD9KDAP4bw38AhL12prFTS23JbbR9Re5Qv26ZnlQ==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "coVfGS3I/CTrqexX5vUN+PQPoP9aUFiPYYrSzqTWfBA=",
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrMissingCustomerKeyMD5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSECopyUnsealObjectKey(t *testing.T) {
|
||||||
|
for i, test := range sseCopyUnsealObjectKeyTests {
|
||||||
|
if _, err := SSECopy.UnsealObjectKey(test.Headers, test.Metadata, test.Bucket, test.Object); err != test.ExpectedErr {
|
||||||
|
t.Errorf("Test %d: got: %v - want: %v", i, err, test.ExpectedErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var s3UnsealObjectKeyTests = []struct {
|
||||||
|
KMS KMS
|
||||||
|
Bucket, Object string
|
||||||
|
Metadata map[string]string
|
||||||
|
|
||||||
|
ExpectedErr error
|
||||||
|
}{
|
||||||
|
{ // 0 - Valid KMS key-ID and valid metadata entries for bucket/object
|
||||||
|
KMS: NewKMS([32]byte{}),
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "hhVY0LKR1YtZbzAKxTWUfZt5enDfYX6Fxz1ma8Kiudc=",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key": "IAAfALhsOeD5AE3s5Zgq3DZ5VFGsOa3B0ksVC86veDcaj+fXv2U0VadhPaOKYr9Emd5ssOsO0uIhIIrKiOy9rA==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key": "IAAfAMRS2iw45FsfiF3QXajSYVWj1lxMpQm6DxDGPtADCX6fJQQ4atHBtfpgqJFyeQmIHsm0FBI+UlHw1Lv4ug==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id": "test-key-1",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm": "DAREv2-HMAC-SHA256",
|
||||||
|
},
|
||||||
|
ExpectedErr: nil,
|
||||||
|
},
|
||||||
|
{ // 1 - Valid KMS key-ID for invalid metadata entries for bucket/object
|
||||||
|
KMS: NewKMS([32]byte{}),
|
||||||
|
Bucket: "bucket",
|
||||||
|
Object: "object",
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-Iv": "hhVY0LKR1YtZbzAKxTWUfZt5enDfYX6Fxz1ma8Kiudc=",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key": "IAAfALhsOeD5AE3s5Zgq3DZ5VFGsOa3B0ksVC86veDcaj+fXv2U0VadhPaOKYr9Emd5ssOsO0uIhIIrKiOy9rA==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key": "IAAfAMRS2iw45FsfiF3QXajSYVWj1lxMpQm6DxDGPtADCX6fJQQ4atHBtfpgqJFyeQmIHsm0FBI+UlHw1Lv4ug==",
|
||||||
|
"X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id": "test-key-1",
|
||||||
|
},
|
||||||
|
ExpectedErr: errMissingInternalSealAlgorithm,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestS3UnsealObjectKey(t *testing.T) {
|
||||||
|
for i, test := range s3UnsealObjectKeyTests {
|
||||||
|
if _, err := S3.UnsealObjectKey(test.KMS, test.Metadata, test.Bucket, test.Object); err != test.ExpectedErr {
|
||||||
|
t.Errorf("Test %d: got: %v - want: %v", i, err, test.ExpectedErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+122
-169
@@ -19,186 +19,133 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
vault "github.com/hashicorp/vault/api"
|
vault "github.com/hashicorp/vault/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// VaultEndpointEnv Vault endpoint environment variable
|
|
||||||
VaultEndpointEnv = "MINIO_SSE_VAULT_ENDPOINT"
|
|
||||||
// vaultAuthTypeEnv type of vault auth to be used
|
|
||||||
vaultAuthTypeEnv = "MINIO_SSE_VAULT_AUTH_TYPE"
|
|
||||||
// vaultAppRoleIDEnv Vault AppRole ID environment variable
|
|
||||||
vaultAppRoleIDEnv = "MINIO_SSE_VAULT_APPROLE_ID"
|
|
||||||
// vaultAppSecretIDEnv Vault AppRole Secret environment variable
|
|
||||||
vaultAppSecretIDEnv = "MINIO_SSE_VAULT_APPROLE_SECRET"
|
|
||||||
// vaultKeyVersionEnv Vault Key Version environment variable
|
|
||||||
vaultKeyVersionEnv = "MINIO_SSE_VAULT_KEY_VERSION"
|
|
||||||
// vaultKeyNameEnv Vault Encryption Key Name environment variable
|
|
||||||
vaultKeyNameEnv = "MINIO_SSE_VAULT_KEY_NAME"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//ErrKMSAuthLogin is raised when there is a failure authenticating to KMS
|
//ErrKMSAuthLogin is raised when there is a failure authenticating to KMS
|
||||||
ErrKMSAuthLogin = errors.New("Vault service did not return auth info")
|
ErrKMSAuthLogin = errors.New("Vault service did not return auth info")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// VaultKey represents vault encryption key-ring.
|
||||||
|
type VaultKey struct {
|
||||||
|
Name string `json:"name"` // The name of the encryption key-ring
|
||||||
|
Version int `json:"version"` // The key version
|
||||||
|
}
|
||||||
|
|
||||||
|
// VaultAuth represents vault authentication type.
|
||||||
|
// Currently the only supported authentication type is AppRole.
|
||||||
|
type VaultAuth struct {
|
||||||
|
Type string `json:"type"` // The authentication type
|
||||||
|
AppRole VaultAppRole `json:"approle"` // The AppRole authentication credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
// VaultAppRole represents vault AppRole authentication credentials
|
||||||
|
type VaultAppRole struct {
|
||||||
|
ID string `json:"id"` // The AppRole access ID
|
||||||
|
Secret string `json:"secret"` // The AppRole secret
|
||||||
|
}
|
||||||
|
|
||||||
|
// VaultConfig represents vault configuration.
|
||||||
|
type VaultConfig struct {
|
||||||
|
Endpoint string `json:"endpoint"` // The vault API endpoint as URL
|
||||||
|
CAPath string `json:"-"` // The path to PEM-encoded certificate files used for mTLS. Currently not used in config file.
|
||||||
|
Auth VaultAuth `json:"auth"` // The vault authentication configuration
|
||||||
|
Key VaultKey `json:"key-id"` // The named key used for key-generation / decryption.
|
||||||
|
Namespace string `json:"-"` // The vault namespace of enterprise vault instances
|
||||||
|
}
|
||||||
|
|
||||||
|
// vaultService represents a connection to a vault KMS.
|
||||||
type vaultService struct {
|
type vaultService struct {
|
||||||
config *VaultConfig
|
config *VaultConfig
|
||||||
client *vault.Client
|
client *vault.Client
|
||||||
leaseDuration time.Duration
|
leaseDuration time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// return transit secret engine's path for generate data key operation
|
var _ KMS = (*vaultService)(nil) // compiler check that *vaultService implements KMS
|
||||||
func (v *vaultService) genDataKeyEndpoint(key string) string {
|
|
||||||
return "/transit/datakey/plaintext/" + key
|
// empty/default vault configuration used to check whether a particular is empty.
|
||||||
|
var emptyVaultConfig = VaultConfig{}
|
||||||
|
|
||||||
|
// IsEmpty returns true if the vault config struct is an
|
||||||
|
// empty configuration.
|
||||||
|
func (v *VaultConfig) IsEmpty() bool { return *v == emptyVaultConfig }
|
||||||
|
|
||||||
|
// Verify returns a nil error if the vault configuration
|
||||||
|
// is valid. A valid configuration is either empty or
|
||||||
|
// contains valid non-default values.
|
||||||
|
func (v *VaultConfig) Verify() (err error) {
|
||||||
|
if v.IsEmpty() {
|
||||||
|
return // an empty configuration is valid
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case v.Endpoint == "":
|
||||||
|
err = errors.New("crypto: missing hashicorp vault endpoint")
|
||||||
|
case strings.ToLower(v.Auth.Type) != "approle":
|
||||||
|
err = fmt.Errorf("crypto: invalid hashicorp vault authentication type: %s is not supported", v.Auth.Type)
|
||||||
|
case v.Auth.AppRole.ID == "":
|
||||||
|
err = errors.New("crypto: missing hashicorp vault AppRole ID")
|
||||||
|
case v.Auth.AppRole.Secret == "":
|
||||||
|
err = errors.New("crypto: missing hashicorp vault AppSecret ID")
|
||||||
|
case v.Key.Name == "":
|
||||||
|
err = errors.New("crypto: missing hashicorp vault key name")
|
||||||
|
case v.Key.Version < 0:
|
||||||
|
err = errors.New("crypto: invalid hashicorp vault key version: The key version must not be negative")
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// return transit secret engine's path for decrypt operation
|
// NewVault initializes Hashicorp Vault KMS by authenticating
|
||||||
func (v *vaultService) decryptEndpoint(key string) string {
|
// to Vault with the credentials in config and gets a client
|
||||||
return "/transit/decrypt/" + key
|
// token for future api calls.
|
||||||
}
|
func NewVault(config VaultConfig) (KMS, error) {
|
||||||
|
if config.IsEmpty() {
|
||||||
// VaultKey represents vault encryption key-id name & version
|
return nil, errors.New("crypto: the hashicorp vault configuration must not be empty")
|
||||||
type VaultKey struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Version int `json:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// VaultAuth represents vault auth type to use. For now, AppRole is the only supported
|
|
||||||
// auth type.
|
|
||||||
type VaultAuth struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
AppRole VaultAppRole `json:"approle"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// VaultAppRole represents vault approle credentials
|
|
||||||
type VaultAppRole struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Secret string `json:"secret"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// VaultConfig holds config required to start vault service
|
|
||||||
type VaultConfig struct {
|
|
||||||
Endpoint string `json:"endpoint"`
|
|
||||||
Auth VaultAuth `json:"auth"`
|
|
||||||
Key VaultKey `json:"key-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate whether all required env variables needed to start vault service have
|
|
||||||
// been set
|
|
||||||
func validateVaultConfig(c *VaultConfig) error {
|
|
||||||
if c.Endpoint == "" {
|
|
||||||
return fmt.Errorf("Missing hashicorp vault endpoint - %s is empty", VaultEndpointEnv)
|
|
||||||
}
|
}
|
||||||
if strings.ToLower(c.Auth.Type) != "approle" {
|
if err := config.Verify(); err != nil {
|
||||||
return fmt.Errorf("Unsupported hashicorp vault auth type - %s", vaultAuthTypeEnv)
|
return nil, err
|
||||||
}
|
|
||||||
if c.Auth.AppRole.ID == "" {
|
|
||||||
return fmt.Errorf("Missing hashicorp vault AppRole ID - %s is empty", vaultAppRoleIDEnv)
|
|
||||||
}
|
|
||||||
if c.Auth.AppRole.Secret == "" {
|
|
||||||
return fmt.Errorf("Missing hashicorp vault AppSecret ID - %s is empty", vaultAppSecretIDEnv)
|
|
||||||
}
|
|
||||||
if c.Key.Name == "" {
|
|
||||||
return fmt.Errorf("Invalid value set in environment variable %s", vaultKeyNameEnv)
|
|
||||||
}
|
|
||||||
if c.Key.Version < 0 {
|
|
||||||
return fmt.Errorf("Invalid value set in environment variable %s", vaultKeyVersionEnv)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
vaultCfg := vault.Config{Address: config.Endpoint}
|
||||||
}
|
if err := vaultCfg.ConfigureTLS(&vault.TLSConfig{CAPath: config.CAPath}); err != nil {
|
||||||
|
return nil, err
|
||||||
// authenticate to vault with app role id and app role secret, and get a client access token, lease duration
|
|
||||||
func getVaultAccessToken(client *vault.Client, appRoleID, appSecret string) (token string, duration int, err error) {
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"role_id": appRoleID,
|
|
||||||
"secret_id": appSecret,
|
|
||||||
}
|
}
|
||||||
resp, e := client.Logical().Write("auth/approle/login", data)
|
client, err := vault.NewClient(&vaultCfg)
|
||||||
if e != nil {
|
if err != nil {
|
||||||
return token, duration, e
|
return nil, err
|
||||||
|
}
|
||||||
|
if config.Namespace != "" {
|
||||||
|
client.SetNamespace(config.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"role_id": config.Auth.AppRole.ID,
|
||||||
|
"secret_id": config.Auth.AppRole.Secret,
|
||||||
|
}
|
||||||
|
resp, err := client.Logical().Write("auth/approle/login", payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.Auth == nil {
|
if resp.Auth == nil {
|
||||||
return token, duration, ErrKMSAuthLogin
|
return nil, ErrKMSAuthLogin
|
||||||
}
|
}
|
||||||
return resp.Auth.ClientToken, resp.Auth.LeaseDuration, nil
|
|
||||||
|
client.SetToken(resp.Auth.ClientToken)
|
||||||
|
v := &vaultService{client: client, config: &config, leaseDuration: time.Duration(resp.Auth.LeaseDuration)}
|
||||||
|
v.renewToken()
|
||||||
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewVaultConfig sets KMSConfig from environment
|
// renewToken starts a new go-routine which renews
|
||||||
// variables and performs validations.
|
// the vault authentication token periodically.
|
||||||
func NewVaultConfig() (KMSConfig, error) {
|
func (v *vaultService) renewToken() {
|
||||||
kc := KMSConfig{}
|
|
||||||
endpoint := os.Getenv(VaultEndpointEnv)
|
|
||||||
roleID := os.Getenv(vaultAppRoleIDEnv)
|
|
||||||
roleSecret := os.Getenv(vaultAppSecretIDEnv)
|
|
||||||
keyName := os.Getenv(vaultKeyNameEnv)
|
|
||||||
keyVersion := 0
|
|
||||||
authType := "approle"
|
|
||||||
if versionStr := os.Getenv(vaultKeyVersionEnv); versionStr != "" {
|
|
||||||
version, err := strconv.Atoi(versionStr)
|
|
||||||
if err != nil {
|
|
||||||
return kc, fmt.Errorf("Unable to parse %s value (`%s`)", vaultKeyVersionEnv, versionStr)
|
|
||||||
}
|
|
||||||
keyVersion = version
|
|
||||||
}
|
|
||||||
// return if none of the vault env variables are configured
|
|
||||||
if (endpoint == "") && (roleID == "") && (roleSecret == "") && (keyName == "") && (keyVersion == 0) {
|
|
||||||
return kc, nil
|
|
||||||
}
|
|
||||||
c := VaultConfig{
|
|
||||||
Endpoint: endpoint,
|
|
||||||
Auth: VaultAuth{
|
|
||||||
Type: authType,
|
|
||||||
AppRole: VaultAppRole{
|
|
||||||
ID: roleID,
|
|
||||||
Secret: roleSecret,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Key: VaultKey{
|
|
||||||
Version: keyVersion,
|
|
||||||
Name: keyName,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err := validateVaultConfig(&c); err != nil {
|
|
||||||
return kc, err
|
|
||||||
}
|
|
||||||
kc.Vault = c
|
|
||||||
return kc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewVault initializes Hashicorp Vault KMS by
|
|
||||||
// authenticating to Vault with the credentials in KMSConfig,
|
|
||||||
// and gets a client token for future api calls.
|
|
||||||
func NewVault(kmsConf KMSConfig) (KMS, error) {
|
|
||||||
config := kmsConf.Vault
|
|
||||||
c, err := vault.NewClient(&vault.Config{
|
|
||||||
Address: config.Endpoint,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
accessToken, leaseDuration, err := getVaultAccessToken(c, config.Auth.AppRole.ID, config.Auth.AppRole.Secret)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// authenticate and get the access token
|
|
||||||
c.SetToken(accessToken)
|
|
||||||
v := vaultService{client: c, config: &config, leaseDuration: time.Duration(leaseDuration)}
|
|
||||||
v.renewToken(c)
|
|
||||||
return &v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *vaultService) renewToken(c *vault.Client) {
|
|
||||||
retryDelay := 1 * time.Minute
|
retryDelay := 1 * time.Minute
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
s, err := c.Auth().Token().RenewSelf(int(v.leaseDuration))
|
s, err := v.client.Auth().Token().RenewSelf(int(v.leaseDuration))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
time.Sleep(retryDelay)
|
time.Sleep(retryDelay)
|
||||||
continue
|
continue
|
||||||
@@ -209,48 +156,54 @@ func (v *vaultService) renewToken(c *vault.Client) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates a random plain text key, sealed plain text key from
|
// GenerateKey returns a new plaintext key, generated by the KMS,
|
||||||
// Vault. It returns the plaintext key and sealed plaintext key on success
|
// and a sealed version of this plaintext key encrypted using the
|
||||||
|
// named key referenced by keyID. It also binds the generated key
|
||||||
|
// cryptographically to the provided context.
|
||||||
func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
|
func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
|
||||||
contextStream := new(bytes.Buffer)
|
var contextStream bytes.Buffer
|
||||||
ctx.WriteTo(contextStream)
|
ctx.WriteTo(&contextStream)
|
||||||
|
|
||||||
payload := map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
|
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
|
||||||
}
|
}
|
||||||
s, err1 := v.client.Logical().Write(v.genDataKeyEndpoint(keyID), payload)
|
s, err := v.client.Logical().Write(fmt.Sprintf("/transit/datakey/plaintext/%s", keyID), payload)
|
||||||
|
if err != nil {
|
||||||
if err1 != nil {
|
return key, sealedKey, err
|
||||||
return key, sealedKey, err1
|
|
||||||
}
|
}
|
||||||
sealKey := s.Data["ciphertext"].(string)
|
sealKey := s.Data["ciphertext"].(string)
|
||||||
plainKey, err := base64.StdEncoding.DecodeString(s.Data["plaintext"].(string))
|
plainKey, err := base64.StdEncoding.DecodeString(s.Data["plaintext"].(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return key, sealedKey, err1
|
return key, sealedKey, err
|
||||||
}
|
}
|
||||||
copy(key[:], []byte(plainKey))
|
copy(key[:], []byte(plainKey))
|
||||||
return key, []byte(sealKey), nil
|
return key, []byte(sealKey), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// unsealKMSKey unseals the sealedKey using the Vault master key
|
// UnsealKey returns the decrypted sealedKey as plaintext key.
|
||||||
// referenced by the keyID. The plain text key is returned on success.
|
// Therefore it sends the sealedKey to the KMS which decrypts
|
||||||
|
// it using the named key referenced by keyID and responses with
|
||||||
|
// the plaintext key.
|
||||||
|
//
|
||||||
|
// The context must be same context as the one provided while
|
||||||
|
// generating the plaintext key / sealedKey.
|
||||||
func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
|
func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
|
||||||
contextStream := new(bytes.Buffer)
|
var contextStream bytes.Buffer
|
||||||
ctx.WriteTo(contextStream)
|
ctx.WriteTo(&contextStream)
|
||||||
|
|
||||||
payload := map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
"ciphertext": string(sealedKey),
|
"ciphertext": string(sealedKey),
|
||||||
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
|
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
|
||||||
}
|
}
|
||||||
s, err1 := v.client.Logical().Write(v.decryptEndpoint(keyID), payload)
|
s, err := v.client.Logical().Write(fmt.Sprintf("/transit/decrypt/%s", keyID), payload)
|
||||||
if err1 != nil {
|
if err != nil {
|
||||||
return key, err1
|
return key, err
|
||||||
}
|
}
|
||||||
base64Key := s.Data["plaintext"].(string)
|
base64Key := s.Data["plaintext"].(string)
|
||||||
plainKey, err1 := base64.StdEncoding.DecodeString(base64Key)
|
plainKey, err := base64.StdEncoding.DecodeString(base64Key)
|
||||||
if err1 != nil {
|
if err != nil {
|
||||||
return key, err1
|
return key, err
|
||||||
}
|
}
|
||||||
copy(key[:], []byte(plainKey))
|
copy(key[:], []byte(plainKey))
|
||||||
|
|
||||||
return key, nil
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import (
|
|||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/disk"
|
"github.com/minio/minio/pkg/disk"
|
||||||
"github.com/minio/minio/pkg/hash"
|
|
||||||
"github.com/minio/minio/pkg/lock"
|
"github.com/minio/minio/pkg/lock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -258,7 +257,7 @@ func (cfs *cacheFSObjects) IsOnline() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Caches the object to disk
|
// Caches the object to disk
|
||||||
func (cfs *cacheFSObjects) Put(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string) error {
|
func (cfs *cacheFSObjects) Put(ctx context.Context, bucket, object string, data *PutObjReader, metadata map[string]string, opts ObjectOptions) error {
|
||||||
if cfs.diskUsageHigh() {
|
if cfs.diskUsageHigh() {
|
||||||
select {
|
select {
|
||||||
case cfs.purgeChan <- struct{}{}:
|
case cfs.purgeChan <- struct{}{}:
|
||||||
@@ -275,7 +274,7 @@ func (cfs *cacheFSObjects) Put(ctx context.Context, bucket, object string, data
|
|||||||
return pErr
|
return pErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err := cfs.PutObject(ctx, bucket, object, data, metadata)
|
_, err := cfs.PutObject(ctx, bucket, object, data, metadata, opts)
|
||||||
// if err is due to disk being offline , mark cache drive as offline
|
// if err is due to disk being offline , mark cache drive as offline
|
||||||
if IsErr(err, baseErrs...) {
|
if IsErr(err, baseErrs...) {
|
||||||
cfs.setOnline(false)
|
cfs.setOnline(false)
|
||||||
@@ -284,8 +283,8 @@ func (cfs *cacheFSObjects) Put(ctx context.Context, bucket, object string, data
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns the handle for the cached object
|
// Returns the handle for the cached object
|
||||||
func (cfs *cacheFSObjects) Get(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) (err error) {
|
func (cfs *cacheFSObjects) Get(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error) {
|
||||||
return cfs.GetObject(ctx, bucket, object, startOffset, length, writer, etag)
|
return cfs.GetObject(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deletes the cached object
|
// Deletes the cached object
|
||||||
@@ -295,13 +294,14 @@ func (cfs *cacheFSObjects) Delete(ctx context.Context, bucket, object string) (e
|
|||||||
|
|
||||||
// convenience function to check if object is cached on this cacheFSObjects
|
// convenience function to check if object is cached on this cacheFSObjects
|
||||||
func (cfs *cacheFSObjects) Exists(ctx context.Context, bucket, object string) bool {
|
func (cfs *cacheFSObjects) Exists(ctx context.Context, bucket, object string) bool {
|
||||||
_, err := cfs.GetObjectInfo(ctx, bucket, object)
|
_, err := cfs.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identical to fs PutObject operation except that it uses ETag in metadata
|
// Identical to fs PutObject operation except that it uses ETag in metadata
|
||||||
// headers.
|
// headers.
|
||||||
func (cfs *cacheFSObjects) PutObject(ctx context.Context, bucket string, object string, data *hash.Reader, metadata map[string]string) (objInfo ObjectInfo, retErr error) {
|
func (cfs *cacheFSObjects) PutObject(ctx context.Context, bucket string, object string, r *PutObjReader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, retErr error) {
|
||||||
|
data := r.Reader
|
||||||
fs := cfs.FSObjects
|
fs := cfs.FSObjects
|
||||||
// Lock the object.
|
// Lock the object.
|
||||||
objectLock := fs.nsMutex.NewNSLock(bucket, object)
|
objectLock := fs.nsMutex.NewNSLock(bucket, object)
|
||||||
@@ -354,7 +354,7 @@ func (cfs *cacheFSObjects) PutObject(ctx context.Context, bucket string, object
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate input data size and it can never be less than zero.
|
// Validate input data size and it can never be less than zero.
|
||||||
if data.Size() < 0 {
|
if data.Size() < -1 {
|
||||||
logger.LogIf(ctx, errInvalidArgument)
|
logger.LogIf(ctx, errInvalidArgument)
|
||||||
return ObjectInfo{}, errInvalidArgument
|
return ObjectInfo{}, errInvalidArgument
|
||||||
}
|
}
|
||||||
@@ -438,7 +438,7 @@ func (cfs *cacheFSObjects) PutObject(ctx context.Context, bucket string, object
|
|||||||
// Implements S3 compatible initiate multipart API. Operation here is identical
|
// Implements S3 compatible initiate multipart API. Operation here is identical
|
||||||
// to fs backend implementation - with the exception that cache FS uses the uploadID
|
// to fs backend implementation - with the exception that cache FS uses the uploadID
|
||||||
// generated on the backend
|
// generated on the backend
|
||||||
func (cfs *cacheFSObjects) NewMultipartUpload(ctx context.Context, bucket, object string, meta map[string]string, uploadID string) (string, error) {
|
func (cfs *cacheFSObjects) NewMultipartUpload(ctx context.Context, bucket, object string, meta map[string]string, uploadID string, opts ObjectOptions) (string, error) {
|
||||||
if cfs.diskUsageHigh() {
|
if cfs.diskUsageHigh() {
|
||||||
select {
|
select {
|
||||||
case cfs.purgeChan <- struct{}{}:
|
case cfs.purgeChan <- struct{}{}:
|
||||||
|
|||||||
+172
-83
@@ -31,14 +31,14 @@ import (
|
|||||||
|
|
||||||
"github.com/djherbis/atime"
|
"github.com/djherbis/atime"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
"github.com/minio/minio/pkg/wildcard"
|
"github.com/minio/minio/pkg/wildcard"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// disk cache needs to have cacheSizeMultiplier * object size space free for a cache entry to be created.
|
// disk cache needs to have object size space free for a cache entry to be created.
|
||||||
cacheSizeMultiplier = 100
|
|
||||||
cacheTrashDir = "trash"
|
cacheTrashDir = "trash"
|
||||||
cacheCleanupInterval = 10 // in minutes
|
cacheCleanupInterval = 10 // in minutes
|
||||||
)
|
)
|
||||||
@@ -57,18 +57,19 @@ type cacheObjects struct {
|
|||||||
// file path patterns to exclude from cache
|
// file path patterns to exclude from cache
|
||||||
exclude []string
|
exclude []string
|
||||||
// Object functions pointing to the corresponding functions of backend implementation.
|
// Object functions pointing to the corresponding functions of backend implementation.
|
||||||
GetObjectFn func(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) (err error)
|
GetObjectNInfoFn func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error)
|
||||||
GetObjectInfoFn func(ctx context.Context, bucket, object string) (objInfo ObjectInfo, err error)
|
GetObjectFn func(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error)
|
||||||
PutObjectFn func(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string) (objInfo ObjectInfo, err error)
|
GetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||||
|
PutObjectFn func(ctx context.Context, bucket, object string, data *PutObjReader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||||
DeleteObjectFn func(ctx context.Context, bucket, object string) error
|
DeleteObjectFn func(ctx context.Context, bucket, object string) error
|
||||||
ListObjectsFn func(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (result ListObjectsInfo, err error)
|
ListObjectsFn func(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (result ListObjectsInfo, err error)
|
||||||
ListObjectsV2Fn func(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result ListObjectsV2Info, err error)
|
ListObjectsV2Fn func(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result ListObjectsV2Info, err error)
|
||||||
ListBucketsFn func(ctx context.Context) (buckets []BucketInfo, err error)
|
ListBucketsFn func(ctx context.Context) (buckets []BucketInfo, err error)
|
||||||
GetBucketInfoFn func(ctx context.Context, bucket string) (bucketInfo BucketInfo, err error)
|
GetBucketInfoFn func(ctx context.Context, bucket string) (bucketInfo BucketInfo, err error)
|
||||||
NewMultipartUploadFn func(ctx context.Context, bucket, object string, metadata map[string]string) (uploadID string, err error)
|
NewMultipartUploadFn func(ctx context.Context, bucket, object string, metadata map[string]string, opts ObjectOptions) (uploadID string, err error)
|
||||||
PutObjectPartFn func(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader) (info PartInfo, err error)
|
PutObjectPartFn func(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error)
|
||||||
AbortMultipartUploadFn func(ctx context.Context, bucket, object, uploadID string) error
|
AbortMultipartUploadFn func(ctx context.Context, bucket, object, uploadID string) error
|
||||||
CompleteMultipartUploadFn func(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart) (objInfo ObjectInfo, err error)
|
CompleteMultipartUploadFn func(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||||
DeleteBucketFn func(ctx context.Context, bucket string) error
|
DeleteBucketFn func(ctx context.Context, bucket string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,21 +89,27 @@ type CacheObjectLayer interface {
|
|||||||
ListBuckets(ctx context.Context) (buckets []BucketInfo, err error)
|
ListBuckets(ctx context.Context) (buckets []BucketInfo, err error)
|
||||||
DeleteBucket(ctx context.Context, bucket string) error
|
DeleteBucket(ctx context.Context, bucket string) error
|
||||||
// Object operations.
|
// Object operations.
|
||||||
GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) (err error)
|
GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error)
|
||||||
GetObjectInfo(ctx context.Context, bucket, object string) (objInfo ObjectInfo, err error)
|
GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error)
|
||||||
PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string) (objInfo ObjectInfo, err error)
|
GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||||
|
PutObject(ctx context.Context, bucket, object string, data *PutObjReader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||||
DeleteObject(ctx context.Context, bucket, object string) error
|
DeleteObject(ctx context.Context, bucket, object string) error
|
||||||
|
|
||||||
// Multipart operations.
|
// Multipart operations.
|
||||||
NewMultipartUpload(ctx context.Context, bucket, object string, metadata map[string]string) (uploadID string, err error)
|
NewMultipartUpload(ctx context.Context, bucket, object string, metadata map[string]string, opts ObjectOptions) (uploadID string, err error)
|
||||||
PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader) (info PartInfo, err error)
|
PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error)
|
||||||
AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string) error
|
AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string) error
|
||||||
CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart) (objInfo ObjectInfo, err error)
|
CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||||
|
|
||||||
// Storage operations.
|
// Storage operations.
|
||||||
StorageInfo(ctx context.Context) CacheStorageInfo
|
StorageInfo(ctx context.Context) CacheStorageInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCacheable returns if the object should be saved in the cache.
|
||||||
|
func (o ObjectInfo) IsCacheable() bool {
|
||||||
|
return !crypto.IsEncrypted(o.UserDefined)
|
||||||
|
}
|
||||||
|
|
||||||
// backendDownError returns true if err is due to backend failure or faulty disk if in server mode
|
// backendDownError returns true if err is due to backend failure or faulty disk if in server mode
|
||||||
func backendDownError(err error) bool {
|
func backendDownError(err error) bool {
|
||||||
_, backendDown := err.(BackendDown)
|
_, backendDown := err.(BackendDown)
|
||||||
@@ -175,22 +182,99 @@ func (c cacheObjects) getMetadata(objInfo ObjectInfo) map[string]string {
|
|||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||||
|
if c.isCacheExclude(bucket, object) {
|
||||||
|
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, writeLock, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch cacheFSObjects if object is currently cached or nearest available cache drive
|
||||||
|
dcache, err := c.cache.getCachedFSLoc(ctx, bucket, object)
|
||||||
|
if err != nil {
|
||||||
|
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, writeLock, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheReader, cacheErr := dcache.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
||||||
|
|
||||||
|
objInfo, err := c.GetObjectInfoFn(ctx, bucket, object, opts)
|
||||||
|
if backendDownError(err) && cacheErr == nil {
|
||||||
|
return cacheReader, nil
|
||||||
|
} else if err != nil {
|
||||||
|
if _, ok := err.(ObjectNotFound); ok {
|
||||||
|
// Delete cached entry if backend object was deleted.
|
||||||
|
dcache.Delete(ctx, bucket, object)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !objInfo.IsCacheable() || filterFromCache(objInfo.UserDefined) {
|
||||||
|
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, writeLock, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cacheErr == nil {
|
||||||
|
if cacheReader.ObjInfo.ETag == objInfo.ETag && !isStaleCache(objInfo) {
|
||||||
|
// Object is not stale, so serve from cache
|
||||||
|
return cacheReader, nil
|
||||||
|
}
|
||||||
|
cacheReader.Close()
|
||||||
|
// Object is stale, so delete from cache
|
||||||
|
dcache.Delete(ctx, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we got here, we are serving the request from backend,
|
||||||
|
// and also adding the object to the cache.
|
||||||
|
|
||||||
|
if rs != nil {
|
||||||
|
// We don't cache partial objects.
|
||||||
|
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, writeLock, opts)
|
||||||
|
}
|
||||||
|
if !dcache.diskAvailable(objInfo.Size) {
|
||||||
|
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, writeLock, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
bkReader, bkErr := c.GetObjectNInfoFn(ctx, bucket, object, rs, h, writeLock, opts)
|
||||||
|
if bkErr != nil {
|
||||||
|
return nil, bkErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize pipe.
|
||||||
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
|
teeReader := io.TeeReader(bkReader, pipeWriter)
|
||||||
|
hashReader, herr := hash.NewReader(pipeReader, bkReader.ObjInfo.Size, "", "", bkReader.ObjInfo.Size)
|
||||||
|
if herr != nil {
|
||||||
|
bkReader.Close()
|
||||||
|
return nil, herr
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
opts := ObjectOptions{}
|
||||||
|
putErr := dcache.Put(ctx, bucket, object, NewPutObjReader(hashReader, nil, nil), c.getMetadata(bkReader.ObjInfo), opts)
|
||||||
|
// close the write end of the pipe, so the error gets
|
||||||
|
// propagated to getObjReader
|
||||||
|
pipeWriter.CloseWithError(putErr)
|
||||||
|
}()
|
||||||
|
|
||||||
|
cleanupBackend := func() { bkReader.Close() }
|
||||||
|
cleanupPipe := func() { pipeReader.Close() }
|
||||||
|
gr = NewGetObjectReaderFromReader(teeReader, bkReader.ObjInfo, cleanupBackend, cleanupPipe)
|
||||||
|
return gr, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Uses cached-object to serve the request. If object is not cached it serves the request from the backend and also
|
// Uses cached-object to serve the request. If object is not cached it serves the request from the backend and also
|
||||||
// stores it in the cache for serving subsequent requests.
|
// stores it in the cache for serving subsequent requests.
|
||||||
func (c cacheObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) (err error) {
|
func (c cacheObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error) {
|
||||||
GetObjectFn := c.GetObjectFn
|
GetObjectFn := c.GetObjectFn
|
||||||
GetObjectInfoFn := c.GetObjectInfoFn
|
GetObjectInfoFn := c.GetObjectInfoFn
|
||||||
|
|
||||||
if c.isCacheExclude(bucket, object) {
|
if c.isCacheExclude(bucket, object) {
|
||||||
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag)
|
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
}
|
}
|
||||||
// fetch cacheFSObjects if object is currently cached or nearest available cache drive
|
// fetch cacheFSObjects if object is currently cached or nearest available cache drive
|
||||||
dcache, err := c.cache.getCachedFSLoc(ctx, bucket, object)
|
dcache, err := c.cache.getCachedFSLoc(ctx, bucket, object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag)
|
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
}
|
}
|
||||||
// stat object on backend
|
// stat object on backend
|
||||||
objInfo, err := GetObjectInfoFn(ctx, bucket, object)
|
objInfo, err := GetObjectInfoFn(ctx, bucket, object, opts)
|
||||||
backendDown := backendDownError(err)
|
backendDown := backendDownError(err)
|
||||||
if err != nil && !backendDown {
|
if err != nil && !backendDown {
|
||||||
if _, ok := err.(ObjectNotFound); ok {
|
if _, ok := err.(ObjectNotFound); ok {
|
||||||
@@ -200,43 +284,43 @@ func (c cacheObjects) GetObject(ctx context.Context, bucket, object string, star
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !backendDown && filterFromCache(objInfo.UserDefined) {
|
if !backendDown && !objInfo.IsCacheable() {
|
||||||
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag)
|
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedObjInfo, err := dcache.GetObjectInfo(ctx, bucket, object)
|
if !backendDown && filterFromCache(objInfo.UserDefined) {
|
||||||
|
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedObjInfo, err := dcache.GetObjectInfo(ctx, bucket, object, opts)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if backendDown {
|
if backendDown {
|
||||||
// If the backend is down, serve the request from cache.
|
// If the backend is down, serve the request from cache.
|
||||||
return dcache.Get(ctx, bucket, object, startOffset, length, writer, etag)
|
return dcache.Get(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
}
|
}
|
||||||
if cachedObjInfo.ETag == objInfo.ETag && !isStaleCache(objInfo) {
|
if cachedObjInfo.ETag == objInfo.ETag && !isStaleCache(objInfo) {
|
||||||
return dcache.Get(ctx, bucket, object, startOffset, length, writer, etag)
|
return dcache.Get(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
}
|
}
|
||||||
dcache.Delete(ctx, bucket, object)
|
dcache.Delete(ctx, bucket, object)
|
||||||
}
|
}
|
||||||
if startOffset != 0 || length != objInfo.Size {
|
if startOffset != 0 || (length > 0 && length != objInfo.Size) {
|
||||||
// We don't cache partial objects.
|
// We don't cache partial objects.
|
||||||
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag)
|
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
}
|
}
|
||||||
if !dcache.diskAvailable(objInfo.Size * cacheSizeMultiplier) {
|
if !dcache.diskAvailable(objInfo.Size) {
|
||||||
// cache only objects < 1/100th of disk capacity
|
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag)
|
|
||||||
}
|
}
|
||||||
// Initialize pipe.
|
// Initialize pipe.
|
||||||
pipeReader, pipeWriter := io.Pipe()
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
hashReader, err := hash.NewReader(pipeReader, objInfo.Size, "", "")
|
hashReader, err := hash.NewReader(pipeReader, objInfo.Size, "", "", objInfo.Size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
if err = GetObjectFn(ctx, bucket, object, 0, objInfo.Size, io.MultiWriter(writer, pipeWriter), etag); err != nil {
|
gerr := GetObjectFn(ctx, bucket, object, 0, objInfo.Size, io.MultiWriter(writer, pipeWriter), etag, opts)
|
||||||
pipeWriter.CloseWithError(err)
|
pipeWriter.CloseWithError(gerr) // Close writer explicitly signaling we wrote all data.
|
||||||
return
|
|
||||||
}
|
|
||||||
pipeWriter.Close() // Close writer explicitly signaling we wrote all data.
|
|
||||||
}()
|
}()
|
||||||
err = dcache.Put(ctx, bucket, object, hashReader, c.getMetadata(objInfo))
|
err = dcache.Put(ctx, bucket, object, NewPutObjReader(hashReader, nil, nil), c.getMetadata(objInfo), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -245,17 +329,17 @@ func (c cacheObjects) GetObject(ctx context.Context, bucket, object string, star
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns ObjectInfo from cache if available.
|
// Returns ObjectInfo from cache if available.
|
||||||
func (c cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string) (ObjectInfo, error) {
|
func (c cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
|
||||||
getObjectInfoFn := c.GetObjectInfoFn
|
getObjectInfoFn := c.GetObjectInfoFn
|
||||||
if c.isCacheExclude(bucket, object) {
|
if c.isCacheExclude(bucket, object) {
|
||||||
return getObjectInfoFn(ctx, bucket, object)
|
return getObjectInfoFn(ctx, bucket, object, opts)
|
||||||
}
|
}
|
||||||
// fetch cacheFSObjects if object is currently cached or nearest available cache drive
|
// fetch cacheFSObjects if object is currently cached or nearest available cache drive
|
||||||
dcache, err := c.cache.getCachedFSLoc(ctx, bucket, object)
|
dcache, err := c.cache.getCachedFSLoc(ctx, bucket, object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return getObjectInfoFn(ctx, bucket, object)
|
return getObjectInfoFn(ctx, bucket, object, opts)
|
||||||
}
|
}
|
||||||
objInfo, err := getObjectInfoFn(ctx, bucket, object)
|
objInfo, err := getObjectInfoFn(ctx, bucket, object, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(ObjectNotFound); ok {
|
if _, ok := err.(ObjectNotFound); ok {
|
||||||
// Delete the cached entry if backend object was deleted.
|
// Delete the cached entry if backend object was deleted.
|
||||||
@@ -266,14 +350,14 @@ func (c cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string)
|
|||||||
return ObjectInfo{}, err
|
return ObjectInfo{}, err
|
||||||
}
|
}
|
||||||
// when backend is down, serve from cache.
|
// when backend is down, serve from cache.
|
||||||
cachedObjInfo, cerr := dcache.GetObjectInfo(ctx, bucket, object)
|
cachedObjInfo, cerr := dcache.GetObjectInfo(ctx, bucket, object, opts)
|
||||||
if cerr == nil {
|
if cerr == nil {
|
||||||
return cachedObjInfo, nil
|
return cachedObjInfo, nil
|
||||||
}
|
}
|
||||||
return ObjectInfo{}, BackendDown{}
|
return ObjectInfo{}, BackendDown{}
|
||||||
}
|
}
|
||||||
// when backend is up, do a sanity check on cached object
|
// when backend is up, do a sanity check on cached object
|
||||||
cachedObjInfo, err := dcache.GetObjectInfo(ctx, bucket, object)
|
cachedObjInfo, err := dcache.GetObjectInfo(ctx, bucket, object, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return objInfo, nil
|
return objInfo, nil
|
||||||
}
|
}
|
||||||
@@ -560,42 +644,43 @@ func (c cacheObjects) isCacheExclude(bucket, object string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PutObject - caches the uploaded object for single Put operations
|
// PutObject - caches the uploaded object for single Put operations
|
||||||
func (c cacheObjects) PutObject(ctx context.Context, bucket, object string, r *hash.Reader, metadata map[string]string) (objInfo ObjectInfo, err error) {
|
func (c cacheObjects) PutObject(ctx context.Context, bucket, object string, r *PutObjReader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
putObjectFn := c.PutObjectFn
|
putObjectFn := c.PutObjectFn
|
||||||
|
data := r.Reader
|
||||||
dcache, err := c.cache.getCacheFS(ctx, bucket, object)
|
dcache, err := c.cache.getCacheFS(ctx, bucket, object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// disk cache could not be located,execute backend call.
|
// disk cache could not be located,execute backend call.
|
||||||
return putObjectFn(ctx, bucket, object, r, metadata)
|
return putObjectFn(ctx, bucket, object, r, metadata, opts)
|
||||||
}
|
}
|
||||||
size := r.Size()
|
size := r.Size()
|
||||||
|
|
||||||
// fetch from backend if there is no space on cache drive
|
// fetch from backend if there is no space on cache drive
|
||||||
if !dcache.diskAvailable(size * cacheSizeMultiplier) {
|
if !dcache.diskAvailable(size) {
|
||||||
return putObjectFn(ctx, bucket, object, r, metadata)
|
return putObjectFn(ctx, bucket, object, r, metadata, opts)
|
||||||
}
|
}
|
||||||
// fetch from backend if cache exclude pattern or cache-control
|
// fetch from backend if cache exclude pattern or cache-control
|
||||||
// directive set to exclude
|
// directive set to exclude
|
||||||
if c.isCacheExclude(bucket, object) || filterFromCache(metadata) {
|
if c.isCacheExclude(bucket, object) || filterFromCache(metadata) {
|
||||||
dcache.Delete(ctx, bucket, object)
|
dcache.Delete(ctx, bucket, object)
|
||||||
return putObjectFn(ctx, bucket, object, r, metadata)
|
return putObjectFn(ctx, bucket, object, r, metadata, opts)
|
||||||
}
|
}
|
||||||
objInfo = ObjectInfo{}
|
objInfo = ObjectInfo{}
|
||||||
// Initialize pipe to stream data to backend
|
// Initialize pipe to stream data to backend
|
||||||
pipeReader, pipeWriter := io.Pipe()
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
hashReader, err := hash.NewReader(pipeReader, size, r.MD5HexString(), r.SHA256HexString())
|
hashReader, err := hash.NewReader(pipeReader, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, err
|
return ObjectInfo{}, err
|
||||||
}
|
}
|
||||||
// Initialize pipe to stream data to cache
|
// Initialize pipe to stream data to cache
|
||||||
rPipe, wPipe := io.Pipe()
|
rPipe, wPipe := io.Pipe()
|
||||||
cHashReader, err := hash.NewReader(rPipe, size, r.MD5HexString(), r.SHA256HexString())
|
cHashReader, err := hash.NewReader(rPipe, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, err
|
return ObjectInfo{}, err
|
||||||
}
|
}
|
||||||
oinfoCh := make(chan ObjectInfo)
|
oinfoCh := make(chan ObjectInfo)
|
||||||
errCh := make(chan error)
|
errCh := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
oinfo, perr := putObjectFn(ctx, bucket, object, hashReader, metadata)
|
oinfo, perr := putObjectFn(ctx, bucket, object, NewPutObjReader(hashReader, nil, nil), metadata, opts)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
pipeWriter.CloseWithError(perr)
|
pipeWriter.CloseWithError(perr)
|
||||||
wPipe.CloseWithError(perr)
|
wPipe.CloseWithError(perr)
|
||||||
@@ -608,14 +693,14 @@ func (c cacheObjects) PutObject(ctx context.Context, bucket, object string, r *h
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err = dcache.Put(ctx, bucket, object, cHashReader, metadata); err != nil {
|
if err = dcache.Put(ctx, bucket, object, NewPutObjReader(cHashReader, nil, nil), metadata, opts); err != nil {
|
||||||
wPipe.CloseWithError(err)
|
wPipe.CloseWithError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
mwriter := io.MultiWriter(pipeWriter, wPipe)
|
mwriter := io.MultiWriter(pipeWriter, wPipe)
|
||||||
_, err = io.Copy(mwriter, r)
|
_, err = io.Copy(mwriter, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = <-errCh
|
err = <-errCh
|
||||||
return objInfo, err
|
return objInfo, err
|
||||||
@@ -627,68 +712,69 @@ func (c cacheObjects) PutObject(ctx context.Context, bucket, object string, r *h
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewMultipartUpload - Starts a new multipart upload operation to backend and cache.
|
// NewMultipartUpload - Starts a new multipart upload operation to backend and cache.
|
||||||
func (c cacheObjects) NewMultipartUpload(ctx context.Context, bucket, object string, metadata map[string]string) (uploadID string, err error) {
|
func (c cacheObjects) NewMultipartUpload(ctx context.Context, bucket, object string, metadata map[string]string, opts ObjectOptions) (uploadID string, err error) {
|
||||||
newMultipartUploadFn := c.NewMultipartUploadFn
|
newMultipartUploadFn := c.NewMultipartUploadFn
|
||||||
|
|
||||||
if c.isCacheExclude(bucket, object) || filterFromCache(metadata) {
|
if c.isCacheExclude(bucket, object) || filterFromCache(metadata) {
|
||||||
return newMultipartUploadFn(ctx, bucket, object, metadata)
|
return newMultipartUploadFn(ctx, bucket, object, metadata, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
dcache, err := c.cache.getCacheFS(ctx, bucket, object)
|
dcache, err := c.cache.getCacheFS(ctx, bucket, object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// disk cache could not be located,execute backend call.
|
// disk cache could not be located,execute backend call.
|
||||||
return newMultipartUploadFn(ctx, bucket, object, metadata)
|
return newMultipartUploadFn(ctx, bucket, object, metadata, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadID, err = newMultipartUploadFn(ctx, bucket, object, metadata)
|
uploadID, err = newMultipartUploadFn(ctx, bucket, object, metadata, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// create new multipart upload in cache with same uploadID
|
// create new multipart upload in cache with same uploadID
|
||||||
dcache.NewMultipartUpload(ctx, bucket, object, metadata, uploadID)
|
dcache.NewMultipartUpload(ctx, bucket, object, metadata, uploadID, opts)
|
||||||
return uploadID, err
|
return uploadID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutObjectPart - uploads part to backend and cache simultaneously.
|
// PutObjectPart - uploads part to backend and cache simultaneously.
|
||||||
func (c cacheObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader) (info PartInfo, err error) {
|
func (c cacheObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, r *PutObjReader, opts ObjectOptions) (info PartInfo, err error) {
|
||||||
|
data := r.Reader
|
||||||
putObjectPartFn := c.PutObjectPartFn
|
putObjectPartFn := c.PutObjectPartFn
|
||||||
dcache, err := c.cache.getCacheFS(ctx, bucket, object)
|
dcache, err := c.cache.getCacheFS(ctx, bucket, object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// disk cache could not be located,execute backend call.
|
// disk cache could not be located,execute backend call.
|
||||||
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data)
|
return putObjectPartFn(ctx, bucket, object, uploadID, partID, r, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.isCacheExclude(bucket, object) {
|
if c.isCacheExclude(bucket, object) {
|
||||||
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data)
|
return putObjectPartFn(ctx, bucket, object, uploadID, partID, r, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure cache has at least cacheSizeMultiplier * size available
|
// make sure cache has at least size space available
|
||||||
size := data.Size()
|
size := data.Size()
|
||||||
if !dcache.diskAvailable(size * cacheSizeMultiplier) {
|
if !dcache.diskAvailable(size) {
|
||||||
select {
|
select {
|
||||||
case dcache.purgeChan <- struct{}{}:
|
case dcache.purgeChan <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data)
|
return putObjectPartFn(ctx, bucket, object, uploadID, partID, r, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
info = PartInfo{}
|
info = PartInfo{}
|
||||||
// Initialize pipe to stream data to backend
|
// Initialize pipe to stream data to backend
|
||||||
pipeReader, pipeWriter := io.Pipe()
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
hashReader, err := hash.NewReader(pipeReader, size, data.MD5HexString(), data.SHA256HexString())
|
hashReader, err := hash.NewReader(pipeReader, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Initialize pipe to stream data to cache
|
// Initialize pipe to stream data to cache
|
||||||
rPipe, wPipe := io.Pipe()
|
rPipe, wPipe := io.Pipe()
|
||||||
cHashReader, err := hash.NewReader(rPipe, size, data.MD5HexString(), data.SHA256HexString())
|
cHashReader, err := hash.NewReader(rPipe, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pinfoCh := make(chan PartInfo)
|
pinfoCh := make(chan PartInfo)
|
||||||
errorCh := make(chan error)
|
errorCh := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
info, err = putObjectPartFn(ctx, bucket, object, uploadID, partID, hashReader)
|
info, err = putObjectPartFn(ctx, bucket, object, uploadID, partID, NewPutObjReader(hashReader, nil, nil), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
close(pinfoCh)
|
close(pinfoCh)
|
||||||
pipeWriter.CloseWithError(err)
|
pipeWriter.CloseWithError(err)
|
||||||
@@ -700,7 +786,7 @@ func (c cacheObjects) PutObjectPart(ctx context.Context, bucket, object, uploadI
|
|||||||
pinfoCh <- info
|
pinfoCh <- info
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
if _, perr := dcache.PutObjectPart(ctx, bucket, object, uploadID, partID, cHashReader); perr != nil {
|
if _, perr := dcache.PutObjectPart(ctx, bucket, object, uploadID, partID, NewPutObjReader(cHashReader, nil, nil), opts); perr != nil {
|
||||||
wPipe.CloseWithError(perr)
|
wPipe.CloseWithError(perr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -742,25 +828,25 @@ func (c cacheObjects) AbortMultipartUpload(ctx context.Context, bucket, object,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CompleteMultipartUpload - completes multipart upload operation on backend and cache.
|
// CompleteMultipartUpload - completes multipart upload operation on backend and cache.
|
||||||
func (c cacheObjects) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart) (objInfo ObjectInfo, err error) {
|
func (c cacheObjects) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
completeMultipartUploadFn := c.CompleteMultipartUploadFn
|
completeMultipartUploadFn := c.CompleteMultipartUploadFn
|
||||||
|
|
||||||
if c.isCacheExclude(bucket, object) {
|
if c.isCacheExclude(bucket, object) {
|
||||||
return completeMultipartUploadFn(ctx, bucket, object, uploadID, uploadedParts)
|
return completeMultipartUploadFn(ctx, bucket, object, uploadID, uploadedParts, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
dcache, err := c.cache.getCacheFS(ctx, bucket, object)
|
dcache, err := c.cache.getCacheFS(ctx, bucket, object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// disk cache could not be located,execute backend call.
|
// disk cache could not be located,execute backend call.
|
||||||
return completeMultipartUploadFn(ctx, bucket, object, uploadID, uploadedParts)
|
return completeMultipartUploadFn(ctx, bucket, object, uploadID, uploadedParts, opts)
|
||||||
}
|
}
|
||||||
// perform backend operation
|
// perform backend operation
|
||||||
objInfo, err = completeMultipartUploadFn(ctx, bucket, object, uploadID, uploadedParts)
|
objInfo, err = completeMultipartUploadFn(ctx, bucket, object, uploadID, uploadedParts, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// create new multipart upload in cache with same uploadID
|
// create new multipart upload in cache with same uploadID
|
||||||
dcache.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts)
|
dcache.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -876,14 +962,17 @@ func newServerCacheObjects(config CacheConfig) (CacheObjectLayer, error) {
|
|||||||
cache: dcache,
|
cache: dcache,
|
||||||
exclude: config.Exclude,
|
exclude: config.Exclude,
|
||||||
listPool: newTreeWalkPool(globalLookupTimeout),
|
listPool: newTreeWalkPool(globalLookupTimeout),
|
||||||
GetObjectFn: func(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) error {
|
GetObjectFn: func(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) error {
|
||||||
return newObjectLayerFn().GetObject(ctx, bucket, object, startOffset, length, writer, etag)
|
return newObjectLayerFn().GetObject(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||||
},
|
},
|
||||||
GetObjectInfoFn: func(ctx context.Context, bucket, object string) (ObjectInfo, error) {
|
GetObjectInfoFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
|
||||||
return newObjectLayerFn().GetObjectInfo(ctx, bucket, object)
|
return newObjectLayerFn().GetObjectInfo(ctx, bucket, object, opts)
|
||||||
},
|
},
|
||||||
PutObjectFn: func(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string) (objInfo ObjectInfo, err error) {
|
GetObjectNInfoFn: func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||||
return newObjectLayerFn().PutObject(ctx, bucket, object, data, metadata)
|
return newObjectLayerFn().GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
||||||
|
},
|
||||||
|
PutObjectFn: func(ctx context.Context, bucket, object string, data *PutObjReader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
|
return newObjectLayerFn().PutObject(ctx, bucket, object, data, metadata, opts)
|
||||||
},
|
},
|
||||||
DeleteObjectFn: func(ctx context.Context, bucket, object string) error {
|
DeleteObjectFn: func(ctx context.Context, bucket, object string) error {
|
||||||
return newObjectLayerFn().DeleteObject(ctx, bucket, object)
|
return newObjectLayerFn().DeleteObject(ctx, bucket, object)
|
||||||
@@ -900,17 +989,17 @@ func newServerCacheObjects(config CacheConfig) (CacheObjectLayer, error) {
|
|||||||
GetBucketInfoFn: func(ctx context.Context, bucket string) (bucketInfo BucketInfo, err error) {
|
GetBucketInfoFn: func(ctx context.Context, bucket string) (bucketInfo BucketInfo, err error) {
|
||||||
return newObjectLayerFn().GetBucketInfo(ctx, bucket)
|
return newObjectLayerFn().GetBucketInfo(ctx, bucket)
|
||||||
},
|
},
|
||||||
NewMultipartUploadFn: func(ctx context.Context, bucket, object string, metadata map[string]string) (uploadID string, err error) {
|
NewMultipartUploadFn: func(ctx context.Context, bucket, object string, metadata map[string]string, opts ObjectOptions) (uploadID string, err error) {
|
||||||
return newObjectLayerFn().NewMultipartUpload(ctx, bucket, object, metadata)
|
return newObjectLayerFn().NewMultipartUpload(ctx, bucket, object, metadata, opts)
|
||||||
},
|
},
|
||||||
PutObjectPartFn: func(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader) (info PartInfo, err error) {
|
PutObjectPartFn: func(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error) {
|
||||||
return newObjectLayerFn().PutObjectPart(ctx, bucket, object, uploadID, partID, data)
|
return newObjectLayerFn().PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
|
||||||
},
|
},
|
||||||
AbortMultipartUploadFn: func(ctx context.Context, bucket, object, uploadID string) error {
|
AbortMultipartUploadFn: func(ctx context.Context, bucket, object, uploadID string) error {
|
||||||
return newObjectLayerFn().AbortMultipartUpload(ctx, bucket, object, uploadID)
|
return newObjectLayerFn().AbortMultipartUpload(ctx, bucket, object, uploadID)
|
||||||
},
|
},
|
||||||
CompleteMultipartUploadFn: func(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart) (objInfo ObjectInfo, err error) {
|
CompleteMultipartUploadFn: func(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
return newObjectLayerFn().CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts)
|
return newObjectLayerFn().CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
|
||||||
},
|
},
|
||||||
DeleteBucketFn: func(ctx context.Context, bucket string) error {
|
DeleteBucketFn: func(ctx context.Context, bucket string) error {
|
||||||
return newObjectLayerFn().DeleteBucket(ctx, bucket)
|
return newObjectLayerFn().DeleteBucket(ctx, bucket)
|
||||||
|
|||||||
+11
-9
@@ -192,17 +192,18 @@ func TestDiskCache(t *testing.T) {
|
|||||||
objInfo.ContentType = contentType
|
objInfo.ContentType = contentType
|
||||||
objInfo.ETag = etag
|
objInfo.ETag = etag
|
||||||
objInfo.UserDefined = httpMeta
|
objInfo.UserDefined = httpMeta
|
||||||
|
opts := ObjectOptions{}
|
||||||
|
|
||||||
byteReader := bytes.NewReader([]byte(content))
|
byteReader := bytes.NewReader([]byte(content))
|
||||||
hashReader, err := hash.NewReader(byteReader, int64(size), "", "")
|
hashReader, err := hash.NewReader(byteReader, int64(size), "", "", int64(size))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
err = cache.Put(ctx, bucketName, objectName, hashReader, httpMeta)
|
err = cache.Put(ctx, bucketName, objectName, NewPutObjReader(hashReader, nil, nil), httpMeta, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
cachedObjInfo, err := cache.GetObjectInfo(ctx, bucketName, objectName)
|
cachedObjInfo, err := cache.GetObjectInfo(ctx, bucketName, objectName, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -219,7 +220,7 @@ func TestDiskCache(t *testing.T) {
|
|||||||
t.Fatal("Cached content-type does not match")
|
t.Fatal("Cached content-type does not match")
|
||||||
}
|
}
|
||||||
writer := bytes.NewBuffer(nil)
|
writer := bytes.NewBuffer(nil)
|
||||||
err = cache.Get(ctx, bucketName, objectName, 0, int64(size), writer, "")
|
err = cache.Get(ctx, bucketName, objectName, 0, int64(size), writer, "", opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -266,23 +267,24 @@ func TestDiskCacheMaxUse(t *testing.T) {
|
|||||||
objInfo.ContentType = contentType
|
objInfo.ContentType = contentType
|
||||||
objInfo.ETag = etag
|
objInfo.ETag = etag
|
||||||
objInfo.UserDefined = httpMeta
|
objInfo.UserDefined = httpMeta
|
||||||
|
opts := ObjectOptions{}
|
||||||
|
|
||||||
byteReader := bytes.NewReader([]byte(content))
|
byteReader := bytes.NewReader([]byte(content))
|
||||||
hashReader, err := hash.NewReader(byteReader, int64(size), "", "")
|
hashReader, err := hash.NewReader(byteReader, int64(size), "", "", int64(size))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !cache.diskAvailable(int64(size)) {
|
if !cache.diskAvailable(int64(size)) {
|
||||||
err = cache.Put(ctx, bucketName, objectName, hashReader, httpMeta)
|
err = cache.Put(ctx, bucketName, objectName, NewPutObjReader(hashReader, nil, nil), httpMeta, opts)
|
||||||
if err != errDiskFull {
|
if err != errDiskFull {
|
||||||
t.Fatal("Cache max-use limit violated.")
|
t.Fatal("Cache max-use limit violated.")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = cache.Put(ctx, bucketName, objectName, hashReader, httpMeta)
|
err = cache.Put(ctx, bucketName, objectName, NewPutObjReader(hashReader, nil, nil), httpMeta, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
cachedObjInfo, err := cache.GetObjectInfo(ctx, bucketName, objectName)
|
cachedObjInfo, err := cache.GetObjectInfo(ctx, bucketName, objectName, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -299,7 +301,7 @@ func TestDiskCacheMaxUse(t *testing.T) {
|
|||||||
t.Fatal("Cached content-type does not match")
|
t.Fatal("Cached content-type does not match")
|
||||||
}
|
}
|
||||||
writer := bytes.NewBuffer(nil)
|
writer := bytes.NewBuffer(nil)
|
||||||
err = cache.Get(ctx, bucketName, objectName, 0, int64(size), writer, "")
|
err = cache.Get(ctx, bucketName, objectName, 0, int64(size), writer, "", opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var alphabets = []byte("abcdefghijklmnopqrstuvwxyz0123456789")
|
||||||
|
|
||||||
|
// DummyDataGen returns a reader that repeats the bytes in `alphabets`
|
||||||
|
// upto the desired length.
|
||||||
|
type DummyDataGen struct {
|
||||||
|
b []byte
|
||||||
|
idx, length int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDummyDataGen returns a ReadSeeker over the first `totalLength`
|
||||||
|
// bytes from the infinite stream consisting of repeated
|
||||||
|
// concatenations of `alphabets`.
|
||||||
|
//
|
||||||
|
// The skipOffset (generally = 0) can be used to skip a given number
|
||||||
|
// of bytes from the beginning of the infinite stream. This is useful
|
||||||
|
// to compare such streams of bytes that may be split up, because:
|
||||||
|
//
|
||||||
|
// Given the function:
|
||||||
|
//
|
||||||
|
// f := func(r io.Reader) string {
|
||||||
|
// b, _ := ioutil.ReadAll(r)
|
||||||
|
// return string(b)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for example, the following is true:
|
||||||
|
//
|
||||||
|
// f(NewDummyDataGen(100, 0)) == f(NewDummyDataGen(50, 0)) + f(NewDummyDataGen(50, 50))
|
||||||
|
func NewDummyDataGen(totalLength, skipOffset int64) io.ReadSeeker {
|
||||||
|
if totalLength < 0 {
|
||||||
|
panic("Negative length passed to DummyDataGen!")
|
||||||
|
}
|
||||||
|
if skipOffset < 0 {
|
||||||
|
panic("Negative rotations are not allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
skipOffset = skipOffset % int64(len(alphabets))
|
||||||
|
as := make([]byte, 2*len(alphabets))
|
||||||
|
copy(as, alphabets)
|
||||||
|
copy(as[len(alphabets):], alphabets)
|
||||||
|
b := as[skipOffset : skipOffset+int64(len(alphabets))]
|
||||||
|
return &DummyDataGen{
|
||||||
|
length: totalLength,
|
||||||
|
b: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DummyDataGen) Read(b []byte) (n int, err error) {
|
||||||
|
k := len(b)
|
||||||
|
numLetters := int64(len(d.b))
|
||||||
|
for k > 0 && d.idx < d.length {
|
||||||
|
w := copy(b[len(b)-k:], d.b[d.idx%numLetters:])
|
||||||
|
k -= w
|
||||||
|
d.idx += int64(w)
|
||||||
|
n += w
|
||||||
|
}
|
||||||
|
if d.idx >= d.length {
|
||||||
|
extraBytes := d.idx - d.length
|
||||||
|
n -= int(extraBytes)
|
||||||
|
if n < 0 {
|
||||||
|
n = 0
|
||||||
|
}
|
||||||
|
err = io.EOF
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DummyDataGen) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
switch whence {
|
||||||
|
case io.SeekStart:
|
||||||
|
if offset < 0 {
|
||||||
|
return 0, errors.New("Invalid offset")
|
||||||
|
}
|
||||||
|
d.idx = offset
|
||||||
|
case io.SeekCurrent:
|
||||||
|
if d.idx+offset < 0 {
|
||||||
|
return 0, errors.New("Invalid offset")
|
||||||
|
}
|
||||||
|
d.idx += offset
|
||||||
|
case io.SeekEnd:
|
||||||
|
if d.length+offset < 0 {
|
||||||
|
return 0, errors.New("Invalid offset")
|
||||||
|
}
|
||||||
|
d.idx = d.length + offset
|
||||||
|
}
|
||||||
|
return d.idx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDummyDataGenerator(t *testing.T) {
|
||||||
|
readAll := func(r io.Reader) string {
|
||||||
|
b, _ := ioutil.ReadAll(r)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
checkEq := func(a, b string) {
|
||||||
|
if a != b {
|
||||||
|
t.Fatalf("Unexpected equality failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEq(readAll(NewDummyDataGen(0, 0)), "")
|
||||||
|
|
||||||
|
checkEq(readAll(NewDummyDataGen(10, 0)), readAll(NewDummyDataGen(10, int64(len(alphabets)))))
|
||||||
|
|
||||||
|
checkEq(readAll(NewDummyDataGen(100, 0)), readAll(NewDummyDataGen(50, 0))+readAll(NewDummyDataGen(50, 50)))
|
||||||
|
|
||||||
|
r := NewDummyDataGen(100, 0)
|
||||||
|
r.Seek(int64(len(alphabets)), 0)
|
||||||
|
checkEq(readAll(r), readAll(NewDummyDataGen(100-int64(len(alphabets)), 0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares all the bytes returned by the given readers. Any Read
|
||||||
|
// errors cause a `false` result. A string describing the error is
|
||||||
|
// also returned.
|
||||||
|
func cmpReaders(r1, r2 io.Reader) (bool, string) {
|
||||||
|
bufLen := 32 * 1024
|
||||||
|
b1, b2 := make([]byte, bufLen), make([]byte, bufLen)
|
||||||
|
for i := 0; true; i++ {
|
||||||
|
n1, e1 := io.ReadFull(r1, b1)
|
||||||
|
n2, e2 := io.ReadFull(r2, b2)
|
||||||
|
if n1 != n2 {
|
||||||
|
return false, fmt.Sprintf("Read %d != %d bytes from the readers", n1, n2)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(b1[:n1], b2[:n2]) {
|
||||||
|
return false, fmt.Sprintf("After reading %d equal buffers (32Kib each), we got the following two strings:\n%v\n%v\n",
|
||||||
|
i, b1, b2)
|
||||||
|
}
|
||||||
|
// Check if stream has ended
|
||||||
|
if (e1 == io.ErrUnexpectedEOF && e2 == io.ErrUnexpectedEOF) || (e1 == io.EOF && e2 == io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if e1 != nil || e2 != nil {
|
||||||
|
return false, fmt.Sprintf("Got unexpected error values: %v == %v", e1, e2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmpReaders(t *testing.T) {
|
||||||
|
{
|
||||||
|
r1 := bytes.NewBuffer([]byte("abc"))
|
||||||
|
r2 := bytes.NewBuffer([]byte("abc"))
|
||||||
|
ok, msg := cmpReaders(r1, r2)
|
||||||
|
if !(ok && msg == "") {
|
||||||
|
t.Fatalf("unexpected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
r1 := bytes.NewBuffer([]byte("abc"))
|
||||||
|
r2 := bytes.NewBuffer([]byte("abcd"))
|
||||||
|
ok, _ := cmpReaders(r1, r2)
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("unexpected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,8 +19,8 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/hash"
|
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
)
|
)
|
||||||
@@ -59,19 +59,23 @@ func (api *DummyObjectLayer) ListObjectsV2(ctx context.Context, bucket, prefix,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *DummyObjectLayer) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) (err error) {
|
func (api *DummyObjectLayer) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lock LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *DummyObjectLayer) GetObjectInfo(ctx context.Context, bucket, object string) (objInfo ObjectInfo, err error) {
|
func (api *DummyObjectLayer) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *DummyObjectLayer) PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string) (objInfo ObjectInfo, err error) {
|
func (api *DummyObjectLayer) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *DummyObjectLayer) CopyObject(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo) (objInfo ObjectInfo, err error) {
|
func (api *DummyObjectLayer) PutObject(ctx context.Context, bucket, object string, data *PutObjReader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *DummyObjectLayer) CopyObject(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,15 +87,15 @@ func (api *DummyObjectLayer) ListMultipartUploads(ctx context.Context, bucket, p
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *DummyObjectLayer) NewMultipartUpload(ctx context.Context, bucket, object string, metadata map[string]string) (uploadID string, err error) {
|
func (api *DummyObjectLayer) NewMultipartUpload(ctx context.Context, bucket, object string, metadata map[string]string, opts ObjectOptions) (uploadID string, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *DummyObjectLayer) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, uploadID string, partID int, startOffset int64, length int64, srcInfo ObjectInfo) (info PartInfo, err error) {
|
func (api *DummyObjectLayer) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, uploadID string, partID int, startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (info PartInfo, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *DummyObjectLayer) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader) (info PartInfo, err error) {
|
func (api *DummyObjectLayer) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +107,7 @@ func (api *DummyObjectLayer) AbortMultipartUpload(ctx context.Context, bucket, o
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *DummyObjectLayer) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart) (objInfo ObjectInfo, err error) {
|
func (api *DummyObjectLayer) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +154,14 @@ func (api *DummyObjectLayer) DeleteBucketPolicy(context.Context, string) (err er
|
|||||||
func (api *DummyObjectLayer) IsNotificationSupported() (b bool) {
|
func (api *DummyObjectLayer) IsNotificationSupported() (b bool) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
func (api *DummyObjectLayer) IsListenBucketSupported() (b bool) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (api *DummyObjectLayer) IsEncryptionSupported() (b bool) {
|
func (api *DummyObjectLayer) IsEncryptionSupported() (b bool) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *DummyObjectLayer) IsCompressionSupported() (b bool) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
+475
-56
@@ -22,6 +22,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -37,12 +38,13 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// AWS errors for invalid SSE-C requests.
|
// AWS errors for invalid SSE-C requests.
|
||||||
errInsecureSSERequest = errors.New("SSE-C requests require TLS connections")
|
|
||||||
errEncryptedObject = errors.New("The object was stored using a form of SSE")
|
errEncryptedObject = errors.New("The object was stored using a form of SSE")
|
||||||
errInvalidSSEParameters = errors.New("The SSE-C key for key-rotation is not correct") // special access denied
|
errInvalidSSEParameters = errors.New("The SSE-C key for key-rotation is not correct") // special access denied
|
||||||
errKMSNotConfigured = errors.New("KMS not configured for a server side encrypted object")
|
errKMSNotConfigured = errors.New("KMS not configured for a server side encrypted object")
|
||||||
// Additional Minio errors for SSE-C requests.
|
// Additional Minio errors for SSE-C requests.
|
||||||
errObjectTampered = errors.New("The requested object was modified and may be compromised")
|
errObjectTampered = errors.New("The requested object was modified and may be compromised")
|
||||||
|
// error returned when invalid encryption parameters are specified
|
||||||
|
errInvalidEncryptionParameters = errors.New("The encryption parameters are not applicable to this object")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -78,20 +80,35 @@ func hasServerSideEncryptionHeader(header http.Header) bool {
|
|||||||
return crypto.S3.IsRequested(header) || crypto.SSEC.IsRequested(header)
|
return crypto.S3.IsRequested(header) || crypto.SSEC.IsRequested(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isEncryptedMultipart returns true if the current object is
|
||||||
|
// uploaded by the user using multipart mechanism:
|
||||||
|
// initiate new multipart, upload part, complete upload
|
||||||
|
func isEncryptedMultipart(objInfo ObjectInfo) bool {
|
||||||
|
if len(objInfo.Parts) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !crypto.IsMultiPart(objInfo.UserDefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, part := range objInfo.Parts {
|
||||||
|
_, err := sio.DecryptedSize(uint64(part.Size))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Further check if this object is uploaded using multipart mechanism
|
||||||
|
// by the user and it is not about XL internally splitting the
|
||||||
|
// object into parts in PutObject()
|
||||||
|
return !(objInfo.backendType == BackendErasure && len(objInfo.ETag) == 32)
|
||||||
|
}
|
||||||
|
|
||||||
// ParseSSECopyCustomerRequest parses the SSE-C header fields of the provided request.
|
// ParseSSECopyCustomerRequest parses the SSE-C header fields of the provided request.
|
||||||
// It returns the client provided key on success.
|
// It returns the client provided key on success.
|
||||||
func ParseSSECopyCustomerRequest(r *http.Request, metadata map[string]string) (key []byte, err error) {
|
func ParseSSECopyCustomerRequest(h http.Header, metadata map[string]string) (key []byte, err error) {
|
||||||
if !globalIsSSL { // minio only supports HTTP or HTTPS requests not both at the same time
|
if crypto.S3.IsEncrypted(metadata) && crypto.SSECopy.IsRequested(h) {
|
||||||
// we cannot use r.TLS == nil here because Go's http implementation reflects on
|
|
||||||
// the net.Conn and sets the TLS field of http.Request only if it's an tls.Conn.
|
|
||||||
// Minio uses a BufConn (wrapping a tls.Conn) so the type check within the http package
|
|
||||||
// will always fail -> r.TLS is always nil even for TLS requests.
|
|
||||||
return nil, errInsecureSSERequest
|
|
||||||
}
|
|
||||||
if crypto.S3.IsEncrypted(metadata) && crypto.SSECopy.IsRequested(r.Header) {
|
|
||||||
return nil, crypto.ErrIncompatibleEncryptionMethod
|
return nil, crypto.ErrIncompatibleEncryptionMethod
|
||||||
}
|
}
|
||||||
k, err := crypto.SSECopy.ParseHTTP(r.Header)
|
k, err := crypto.SSECopy.ParseHTTP(h)
|
||||||
return k[:], err
|
return k[:], err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,13 +121,6 @@ func ParseSSECustomerRequest(r *http.Request) (key []byte, err error) {
|
|||||||
// ParseSSECustomerHeader parses the SSE-C header fields and returns
|
// ParseSSECustomerHeader parses the SSE-C header fields and returns
|
||||||
// the client provided key on success.
|
// the client provided key on success.
|
||||||
func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
|
func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
|
||||||
if !globalIsSSL { // minio only supports HTTP or HTTPS requests not both at the same time
|
|
||||||
// we cannot use r.TLS == nil here because Go's http implementation reflects on
|
|
||||||
// the net.Conn and sets the TLS field of http.Request only if it's an tls.Conn.
|
|
||||||
// Minio uses a BufConn (wrapping a tls.Conn) so the type check within the http package
|
|
||||||
// will always fail -> r.TLS is always nil even for TLS requests.
|
|
||||||
return nil, errInsecureSSERequest
|
|
||||||
}
|
|
||||||
if crypto.S3.IsRequested(header) && crypto.SSEC.IsRequested(header) {
|
if crypto.S3.IsRequested(header) && crypto.SSEC.IsRequested(header) {
|
||||||
return key, crypto.ErrIncompatibleEncryptionMethod
|
return key, crypto.ErrIncompatibleEncryptionMethod
|
||||||
}
|
}
|
||||||
@@ -121,8 +131,6 @@ func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
|
|||||||
|
|
||||||
// This function rotates old to new key.
|
// This function rotates old to new key.
|
||||||
func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map[string]string) error {
|
func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map[string]string) error {
|
||||||
delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
default:
|
default:
|
||||||
return errObjectTampered
|
return errObjectTampered
|
||||||
@@ -147,13 +155,36 @@ func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map
|
|||||||
}
|
}
|
||||||
copy(extKey[:], newKey)
|
copy(extKey[:], newKey)
|
||||||
sealedKey = objectKey.Seal(extKey, sealedKey.IV, crypto.SSEC.String(), bucket, object)
|
sealedKey = objectKey.Seal(extKey, sealedKey.IV, crypto.SSEC.String(), bucket, object)
|
||||||
|
crypto.SSEC.CreateMetadata(metadata, sealedKey)
|
||||||
|
return nil
|
||||||
|
case crypto.S3.IsEncrypted(metadata):
|
||||||
|
if globalKMS == nil {
|
||||||
|
return errKMSNotConfigured
|
||||||
|
}
|
||||||
|
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
oldKey, err := globalKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var objectKey crypto.ObjectKey
|
||||||
|
if err = objectKey.Unseal(oldKey, sealedKey, crypto.S3.String(), bucket, object); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newKey, encKey, err := globalKMS.GenerateKey(globalKMSKeyID, crypto.Context{bucket: path.Join(bucket, object)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sealedKey = objectKey.Seal(newKey, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
|
||||||
|
crypto.S3.CreateMetadata(metadata, globalKMSKeyID, encKey, sealedKey)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]string, sseS3 bool) ([]byte, error) {
|
func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]string, sseS3 bool) ([]byte, error) {
|
||||||
delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
|
|
||||||
|
|
||||||
var sealedKey crypto.SealedKey
|
var sealedKey crypto.SealedKey
|
||||||
if sseS3 {
|
if sseS3 {
|
||||||
if globalKMS == nil {
|
if globalKMS == nil {
|
||||||
@@ -175,21 +206,20 @@ func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]s
|
|||||||
sealedKey = objectKey.Seal(extKey, crypto.GenerateIV(rand.Reader), crypto.SSEC.String(), bucket, object)
|
sealedKey = objectKey.Seal(extKey, crypto.GenerateIV(rand.Reader), crypto.SSEC.String(), bucket, object)
|
||||||
crypto.SSEC.CreateMetadata(metadata, sealedKey)
|
crypto.SSEC.CreateMetadata(metadata, sealedKey)
|
||||||
return objectKey[:], nil
|
return objectKey[:], nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEncryptReader(content io.Reader, key []byte, bucket, object string, metadata map[string]string, sseS3 bool) (io.Reader, error) {
|
func newEncryptReader(content io.Reader, key []byte, bucket, object string, metadata map[string]string, sseS3 bool) (r io.Reader, encKey []byte, err error) {
|
||||||
objectEncryptionKey, err := newEncryptMetadata(key, bucket, object, metadata, sseS3)
|
objectEncryptionKey, err := newEncryptMetadata(key, bucket, object, metadata, sseS3)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, encKey, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey[:], MinVersion: sio.Version20})
|
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey[:], MinVersion: sio.Version20})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, crypto.ErrInvalidCustomerKey
|
return nil, encKey, crypto.ErrInvalidCustomerKey
|
||||||
}
|
}
|
||||||
|
|
||||||
return reader, nil
|
return reader, objectEncryptionKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// set new encryption metadata from http request headers for SSE-C and generated key from KMS in the case of
|
// set new encryption metadata from http request headers for SSE-C and generated key from KMS in the case of
|
||||||
@@ -211,19 +241,18 @@ func setEncryptionMetadata(r *http.Request, bucket, object string, metadata map[
|
|||||||
// EncryptRequest takes the client provided content and encrypts the data
|
// EncryptRequest takes the client provided content and encrypts the data
|
||||||
// with the client provided key. It also marks the object as client-side-encrypted
|
// with the client provided key. It also marks the object as client-side-encrypted
|
||||||
// and sets the correct headers.
|
// and sets the correct headers.
|
||||||
func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, metadata map[string]string) (io.Reader, error) {
|
func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, metadata map[string]string) (reader io.Reader, objEncKey []byte, err error) {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
key []byte
|
key []byte
|
||||||
err error
|
|
||||||
)
|
)
|
||||||
if crypto.S3.IsRequested(r.Header) && crypto.SSEC.IsRequested(r.Header) {
|
if crypto.S3.IsRequested(r.Header) && crypto.SSEC.IsRequested(r.Header) {
|
||||||
return nil, crypto.ErrIncompatibleEncryptionMethod
|
return nil, objEncKey, crypto.ErrIncompatibleEncryptionMethod
|
||||||
}
|
}
|
||||||
if crypto.SSEC.IsRequested(r.Header) {
|
if crypto.SSEC.IsRequested(r.Header) {
|
||||||
key, err = ParseSSECustomerRequest(r)
|
key, err = ParseSSECustomerRequest(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, objEncKey, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newEncryptReader(content, key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
|
return newEncryptReader(content, key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
|
||||||
@@ -237,12 +266,11 @@ func DecryptCopyRequest(client io.Writer, r *http.Request, bucket, object string
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if crypto.SSECopy.IsRequested(r.Header) {
|
if crypto.SSECopy.IsRequested(r.Header) {
|
||||||
key, err = ParseSSECopyCustomerRequest(r, metadata)
|
key, err = ParseSSECopyCustomerRequest(r.Header, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete(metadata, crypto.SSECopyKey) // make sure we do not save the key by accident
|
|
||||||
return newDecryptWriter(client, key, bucket, object, 0, metadata)
|
return newDecryptWriter(client, key, bucket, object, 0, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,6 +337,114 @@ func newDecryptWriterWithObjectKey(client io.Writer, objectEncryptionKey []byte,
|
|||||||
return writer, nil
|
return writer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adding support for reader based interface
|
||||||
|
|
||||||
|
// DecryptRequestWithSequenceNumberR - same as
|
||||||
|
// DecryptRequestWithSequenceNumber but with a reader
|
||||||
|
func DecryptRequestWithSequenceNumberR(client io.Reader, h http.Header, bucket, object string, seqNumber uint32, metadata map[string]string) (io.Reader, error) {
|
||||||
|
if crypto.S3.IsEncrypted(metadata) {
|
||||||
|
return newDecryptReader(client, nil, bucket, object, seqNumber, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := ParseSSECustomerHeader(h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newDecryptReader(client, key, bucket, object, seqNumber, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptCopyRequestR - same as DecryptCopyRequest, but with a
|
||||||
|
// Reader
|
||||||
|
func DecryptCopyRequestR(client io.Reader, h http.Header, bucket, object string, seqNumber uint32, metadata map[string]string) (io.Reader, error) {
|
||||||
|
var (
|
||||||
|
key []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if crypto.SSECopy.IsRequested(h) {
|
||||||
|
key, err = ParseSSECopyCustomerRequest(h, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newDecryptReader(client, key, bucket, object, seqNumber, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDecryptReader(client io.Reader, key []byte, bucket, object string, seqNumber uint32, metadata map[string]string) (io.Reader, error) {
|
||||||
|
objectEncryptionKey, err := decryptObjectInfo(key, bucket, object, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newDecryptReaderWithObjectKey(client, objectEncryptionKey, seqNumber, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDecryptReaderWithObjectKey(client io.Reader, objectEncryptionKey []byte, seqNumber uint32, metadata map[string]string) (io.Reader, error) {
|
||||||
|
reader, err := sio.DecryptReader(client, sio.Config{
|
||||||
|
Key: objectEncryptionKey,
|
||||||
|
SequenceNumber: seqNumber,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, crypto.ErrInvalidCustomerKey
|
||||||
|
}
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptBlocksRequestR - same as DecryptBlocksRequest but with a
|
||||||
|
// reader
|
||||||
|
func DecryptBlocksRequestR(inputReader io.Reader, h http.Header, offset,
|
||||||
|
length int64, seqNumber uint32, partStart int, oi ObjectInfo, copySource bool) (
|
||||||
|
io.Reader, error) {
|
||||||
|
|
||||||
|
bucket, object := oi.Bucket, oi.Name
|
||||||
|
|
||||||
|
// Single part case
|
||||||
|
if !isEncryptedMultipart(oi) {
|
||||||
|
var reader io.Reader
|
||||||
|
var err error
|
||||||
|
if copySource {
|
||||||
|
reader, err = DecryptCopyRequestR(inputReader, h, bucket, object, seqNumber, oi.UserDefined)
|
||||||
|
} else {
|
||||||
|
reader, err = DecryptRequestWithSequenceNumberR(inputReader, h, bucket, object, seqNumber, oi.UserDefined)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
partDecRelOffset := int64(seqNumber) * sseDAREPackageBlockSize
|
||||||
|
partEncRelOffset := int64(seqNumber) * (sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
||||||
|
|
||||||
|
w := &DecryptBlocksReader{
|
||||||
|
reader: inputReader,
|
||||||
|
startSeqNum: seqNumber,
|
||||||
|
partDecRelOffset: partDecRelOffset,
|
||||||
|
partEncRelOffset: partEncRelOffset,
|
||||||
|
parts: oi.Parts,
|
||||||
|
partIndex: partStart,
|
||||||
|
header: h,
|
||||||
|
bucket: bucket,
|
||||||
|
object: object,
|
||||||
|
customerKeyHeader: h.Get(crypto.SSECKey),
|
||||||
|
copySource: copySource,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.metadata = map[string]string{}
|
||||||
|
// Copy encryption metadata for internal use.
|
||||||
|
for k, v := range oi.UserDefined {
|
||||||
|
w.metadata[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.copySource {
|
||||||
|
w.customerKeyHeader = h.Get(crypto.SSECopyKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.buildDecrypter(w.parts[w.partIndex].Number); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DecryptRequestWithSequenceNumber decrypts the object with the client provided key. It also removes
|
// DecryptRequestWithSequenceNumber decrypts the object with the client provided key. It also removes
|
||||||
// the client-side-encryption metadata from the object and sets the correct headers.
|
// the client-side-encryption metadata from the object and sets the correct headers.
|
||||||
func DecryptRequestWithSequenceNumber(client io.Writer, r *http.Request, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
func DecryptRequestWithSequenceNumber(client io.Writer, r *http.Request, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||||
@@ -320,7 +456,6 @@ func DecryptRequestWithSequenceNumber(client io.Writer, r *http.Request, bucket,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
|
|
||||||
return newDecryptWriter(client, key, bucket, object, seqNumber, metadata)
|
return newDecryptWriter(client, key, bucket, object, seqNumber, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +465,123 @@ func DecryptRequest(client io.Writer, r *http.Request, bucket, object string, me
|
|||||||
return DecryptRequestWithSequenceNumber(client, r, bucket, object, 0, metadata)
|
return DecryptRequestWithSequenceNumber(client, r, bucket, object, 0, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptBlocksWriter - decrypts multipart parts, while implementing a io.Writer compatible interface.
|
// DecryptBlocksReader - decrypts multipart parts, while implementing
|
||||||
|
// a io.Reader compatible interface.
|
||||||
|
type DecryptBlocksReader struct {
|
||||||
|
// Source of the encrypted content that will be decrypted
|
||||||
|
reader io.Reader
|
||||||
|
// Current decrypter for the current encrypted data block
|
||||||
|
decrypter io.Reader
|
||||||
|
// Start sequence number
|
||||||
|
startSeqNum uint32
|
||||||
|
// Current part index
|
||||||
|
partIndex int
|
||||||
|
// Parts information
|
||||||
|
parts []objectPartInfo
|
||||||
|
header http.Header
|
||||||
|
bucket, object string
|
||||||
|
metadata map[string]string
|
||||||
|
|
||||||
|
partDecRelOffset, partEncRelOffset int64
|
||||||
|
|
||||||
|
copySource bool
|
||||||
|
// Customer Key
|
||||||
|
customerKeyHeader string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DecryptBlocksReader) buildDecrypter(partID int) error {
|
||||||
|
m := make(map[string]string)
|
||||||
|
for k, v := range d.metadata {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
// Initialize the first decrypter; new decrypters will be
|
||||||
|
// initialized in Read() operation as needed.
|
||||||
|
var key []byte
|
||||||
|
var err error
|
||||||
|
if d.copySource {
|
||||||
|
if crypto.SSEC.IsEncrypted(d.metadata) {
|
||||||
|
d.header.Set(crypto.SSECopyKey, d.customerKeyHeader)
|
||||||
|
key, err = ParseSSECopyCustomerRequest(d.header, d.metadata)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if crypto.SSEC.IsEncrypted(d.metadata) {
|
||||||
|
d.header.Set(crypto.SSECKey, d.customerKeyHeader)
|
||||||
|
key, err = ParseSSECustomerHeader(d.header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
objectEncryptionKey, err := decryptObjectInfo(key, d.bucket, d.object, m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var partIDbin [4]byte
|
||||||
|
binary.LittleEndian.PutUint32(partIDbin[:], uint32(partID)) // marshal part ID
|
||||||
|
|
||||||
|
mac := hmac.New(sha256.New, objectEncryptionKey) // derive part encryption key from part ID and object key
|
||||||
|
mac.Write(partIDbin[:])
|
||||||
|
partEncryptionKey := mac.Sum(nil)
|
||||||
|
|
||||||
|
// Limit the reader, so the decryptor doesnt receive bytes
|
||||||
|
// from the next part (different DARE stream)
|
||||||
|
encLenToRead := d.parts[d.partIndex].Size - d.partEncRelOffset
|
||||||
|
decrypter, err := newDecryptReaderWithObjectKey(io.LimitReader(d.reader, encLenToRead), partEncryptionKey, d.startSeqNum, m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.decrypter = decrypter
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DecryptBlocksReader) Read(p []byte) (int, error) {
|
||||||
|
var err error
|
||||||
|
var n1 int
|
||||||
|
decPartSize, _ := sio.DecryptedSize(uint64(d.parts[d.partIndex].Size))
|
||||||
|
unreadPartLen := int64(decPartSize) - d.partDecRelOffset
|
||||||
|
if int64(len(p)) < unreadPartLen {
|
||||||
|
n1, err = d.decrypter.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
d.partDecRelOffset += int64(n1)
|
||||||
|
} else {
|
||||||
|
n1, err = io.ReadFull(d.decrypter, p[:unreadPartLen])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should now proceed to next part, reset all
|
||||||
|
// values appropriately.
|
||||||
|
d.partEncRelOffset = 0
|
||||||
|
d.partDecRelOffset = 0
|
||||||
|
d.startSeqNum = 0
|
||||||
|
|
||||||
|
d.partIndex++
|
||||||
|
if d.partIndex == len(d.parts) {
|
||||||
|
return n1, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.buildDecrypter(d.parts[d.partIndex].Number)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
n1, err = d.decrypter.Read(p[n1:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.partDecRelOffset += int64(n1)
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptBlocksWriter - decrypts multipart parts, while implementing
|
||||||
|
// a io.Writer compatible interface.
|
||||||
type DecryptBlocksWriter struct {
|
type DecryptBlocksWriter struct {
|
||||||
// Original writer where the plain data will be written
|
// Original writer where the plain data will be written
|
||||||
writer io.Writer
|
writer io.Writer
|
||||||
@@ -364,7 +615,7 @@ func (w *DecryptBlocksWriter) buildDecrypter(partID int) error {
|
|||||||
if w.copySource {
|
if w.copySource {
|
||||||
if crypto.SSEC.IsEncrypted(w.metadata) {
|
if crypto.SSEC.IsEncrypted(w.metadata) {
|
||||||
w.req.Header.Set(crypto.SSECopyKey, w.customerKeyHeader)
|
w.req.Header.Set(crypto.SSECopyKey, w.customerKeyHeader)
|
||||||
key, err = ParseSSECopyCustomerRequest(w.req, w.metadata)
|
key, err = ParseSSECopyCustomerRequest(w.req.Header, w.metadata)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if crypto.SSEC.IsEncrypted(w.metadata) {
|
if crypto.SSEC.IsEncrypted(w.metadata) {
|
||||||
@@ -388,13 +639,6 @@ func (w *DecryptBlocksWriter) buildDecrypter(partID int) error {
|
|||||||
mac.Write(partIDbin[:])
|
mac.Write(partIDbin[:])
|
||||||
partEncryptionKey := mac.Sum(nil)
|
partEncryptionKey := mac.Sum(nil)
|
||||||
|
|
||||||
// make sure we do not save the key by accident
|
|
||||||
if w.copySource {
|
|
||||||
delete(m, crypto.SSECopyKey)
|
|
||||||
} else {
|
|
||||||
delete(m, crypto.SSECKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure to provide a NopCloser such that a Close
|
// make sure to provide a NopCloser such that a Close
|
||||||
// on sio.decryptWriter doesn't close the underlying writer's
|
// on sio.decryptWriter doesn't close the underlying writer's
|
||||||
// close which perhaps can close the stream prematurely.
|
// close which perhaps can close the stream prematurely.
|
||||||
@@ -482,7 +726,7 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object stri
|
|||||||
var seqNumber uint32
|
var seqNumber uint32
|
||||||
var encStartOffset, encLength int64
|
var encStartOffset, encLength int64
|
||||||
|
|
||||||
if len(objInfo.Parts) == 0 || !crypto.IsMultiPart(objInfo.UserDefined) {
|
if !isEncryptedMultipart(objInfo) {
|
||||||
seqNumber, encStartOffset, encLength = getEncryptedSinglePartOffsetLength(startOffset, length, objInfo)
|
seqNumber, encStartOffset, encLength = getEncryptedSinglePartOffsetLength(startOffset, length, objInfo)
|
||||||
|
|
||||||
var writer io.WriteCloser
|
var writer io.WriteCloser
|
||||||
@@ -567,7 +811,6 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object stri
|
|||||||
|
|
||||||
// getEncryptedMultipartsOffsetLength - fetch sequence number, encrypted start offset and encrypted length.
|
// getEncryptedMultipartsOffsetLength - fetch sequence number, encrypted start offset and encrypted length.
|
||||||
func getEncryptedMultipartsOffsetLength(offset, length int64, obj ObjectInfo) (uint32, int64, int64) {
|
func getEncryptedMultipartsOffsetLength(offset, length int64, obj ObjectInfo) (uint32, int64, int64) {
|
||||||
|
|
||||||
// Calculate encrypted offset of a multipart object
|
// Calculate encrypted offset of a multipart object
|
||||||
computeEncOffset := func(off int64, obj ObjectInfo) (seqNumber uint32, encryptedOffset int64, err error) {
|
computeEncOffset := func(off int64, obj ObjectInfo) (seqNumber uint32, encryptedOffset int64, err error) {
|
||||||
var curPartEndOffset uint64
|
var curPartEndOffset uint64
|
||||||
@@ -644,7 +887,7 @@ func (o *ObjectInfo) DecryptedSize() (int64, error) {
|
|||||||
if !crypto.IsEncrypted(o.UserDefined) {
|
if !crypto.IsEncrypted(o.UserDefined) {
|
||||||
return 0, errors.New("Cannot compute decrypted size of an unencrypted object")
|
return 0, errors.New("Cannot compute decrypted size of an unencrypted object")
|
||||||
}
|
}
|
||||||
if len(o.Parts) == 0 || !crypto.IsMultiPart(o.UserDefined) {
|
if !isEncryptedMultipart(*o) {
|
||||||
size, err := sio.DecryptedSize(uint64(o.Size))
|
size, err := sio.DecryptedSize(uint64(o.Size))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errObjectTampered // assign correct error type
|
err = errObjectTampered // assign correct error type
|
||||||
@@ -663,6 +906,180 @@ func (o *ObjectInfo) DecryptedSize() (int64, error) {
|
|||||||
return size, nil
|
return size, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For encrypted objects, the ETag sent by client if available
|
||||||
|
// is stored in encrypted form in the backend. Decrypt the ETag
|
||||||
|
// if ETag was previously encrypted.
|
||||||
|
func getDecryptedETag(headers http.Header, objInfo ObjectInfo, copySource bool) (decryptedETag string) {
|
||||||
|
var (
|
||||||
|
key [32]byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
// If ETag is contentMD5Sum return it as is.
|
||||||
|
if len(objInfo.ETag) == 32 {
|
||||||
|
return objInfo.ETag
|
||||||
|
}
|
||||||
|
|
||||||
|
if crypto.IsMultiPart(objInfo.UserDefined) {
|
||||||
|
return objInfo.ETag
|
||||||
|
}
|
||||||
|
if crypto.SSECopy.IsRequested(headers) {
|
||||||
|
key, err = crypto.SSECopy.ParseHTTP(headers)
|
||||||
|
if err != nil {
|
||||||
|
return objInfo.ETag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// As per AWS S3 Spec, ETag for SSE-C encrypted objects need not be MD5Sum of the data.
|
||||||
|
// Since server side copy with same source and dest just replaces the ETag, we save
|
||||||
|
// encrypted content MD5Sum as ETag for both SSE-C and SSE-S3, we standardize the ETag
|
||||||
|
//encryption across SSE-C and SSE-S3, and only return last 32 bytes for SSE-C
|
||||||
|
if crypto.SSEC.IsEncrypted(objInfo.UserDefined) && !copySource {
|
||||||
|
return objInfo.ETag[len(objInfo.ETag)-32:]
|
||||||
|
}
|
||||||
|
|
||||||
|
objectEncryptionKey, err := decryptObjectInfo(key[:], objInfo.Bucket, objInfo.Name, objInfo.UserDefined)
|
||||||
|
if err != nil {
|
||||||
|
return objInfo.ETag
|
||||||
|
}
|
||||||
|
return tryDecryptETag(objectEncryptionKey, objInfo.ETag, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper to decrypt Etag given object encryption key and encrypted ETag
|
||||||
|
func tryDecryptETag(key []byte, encryptedETag string, ssec bool) string {
|
||||||
|
// ETag for SSE-C encrypted objects need not be content MD5Sum.While encrypted
|
||||||
|
// md5sum is stored internally, return just the last 32 bytes of hex-encoded and
|
||||||
|
// encrypted md5sum string for SSE-C
|
||||||
|
if ssec {
|
||||||
|
return encryptedETag[len(encryptedETag)-32:]
|
||||||
|
}
|
||||||
|
var objectKey crypto.ObjectKey
|
||||||
|
copy(objectKey[:], key)
|
||||||
|
encBytes, err := hex.DecodeString(encryptedETag)
|
||||||
|
if err != nil {
|
||||||
|
return encryptedETag
|
||||||
|
}
|
||||||
|
etagBytes, err := objectKey.UnsealETag(encBytes)
|
||||||
|
if err != nil {
|
||||||
|
return encryptedETag
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(etagBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDecryptedRange - To decrypt the range (off, length) of the
|
||||||
|
// decrypted object stream, we need to read the range (encOff,
|
||||||
|
// encLength) of the encrypted object stream to decrypt it, and
|
||||||
|
// compute skipLen, the number of bytes to skip in the beginning of
|
||||||
|
// the encrypted range.
|
||||||
|
//
|
||||||
|
// In addition we also compute the object part number for where the
|
||||||
|
// requested range starts, along with the DARE sequence number within
|
||||||
|
// that part. For single part objects, the partStart will be 0.
|
||||||
|
func (o *ObjectInfo) GetDecryptedRange(rs *HTTPRangeSpec) (encOff, encLength, skipLen int64, seqNumber uint32, partStart int, err error) {
|
||||||
|
if !crypto.IsEncrypted(o.UserDefined) {
|
||||||
|
err = errors.New("Object is not encrypted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rs == nil {
|
||||||
|
// No range, so offsets refer to the whole object.
|
||||||
|
return 0, int64(o.Size), 0, 0, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble slice of (decrypted) part sizes in `sizes`
|
||||||
|
var sizes []int64
|
||||||
|
var decObjSize int64 // decrypted total object size
|
||||||
|
if isEncryptedMultipart(*o) {
|
||||||
|
sizes = make([]int64, len(o.Parts))
|
||||||
|
for i, part := range o.Parts {
|
||||||
|
var partSize uint64
|
||||||
|
partSize, err = sio.DecryptedSize(uint64(part.Size))
|
||||||
|
if err != nil {
|
||||||
|
err = errObjectTampered
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sizes[i] = int64(partSize)
|
||||||
|
decObjSize += int64(partSize)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var partSize uint64
|
||||||
|
partSize, err = sio.DecryptedSize(uint64(o.Size))
|
||||||
|
if err != nil {
|
||||||
|
err = errObjectTampered
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sizes = []int64{int64(partSize)}
|
||||||
|
decObjSize = sizes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var off, length int64
|
||||||
|
off, length, err = rs.GetOffsetLength(decObjSize)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, we have:
|
||||||
|
//
|
||||||
|
// 1. the decrypted part sizes in `sizes` (single element for
|
||||||
|
// single part object) and total decrypted object size `decObjSize`
|
||||||
|
//
|
||||||
|
// 2. the (decrypted) start offset `off` and (decrypted)
|
||||||
|
// length to read `length`
|
||||||
|
//
|
||||||
|
// These are the inputs to the rest of the algorithm below.
|
||||||
|
|
||||||
|
// Locate the part containing the start of the required range
|
||||||
|
var partEnd int
|
||||||
|
var cumulativeSum, encCumulativeSum int64
|
||||||
|
for i, size := range sizes {
|
||||||
|
if off < cumulativeSum+size {
|
||||||
|
partStart = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cumulativeSum += size
|
||||||
|
encPartSize, _ := sio.EncryptedSize(uint64(size))
|
||||||
|
encCumulativeSum += int64(encPartSize)
|
||||||
|
}
|
||||||
|
// partStart is always found in the loop above,
|
||||||
|
// because off is validated.
|
||||||
|
|
||||||
|
sseDAREEncPackageBlockSize := int64(sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
||||||
|
startPkgNum := (off - cumulativeSum) / sseDAREPackageBlockSize
|
||||||
|
|
||||||
|
// Now we can calculate the number of bytes to skip
|
||||||
|
skipLen = (off - cumulativeSum) % sseDAREPackageBlockSize
|
||||||
|
|
||||||
|
encOff = encCumulativeSum + startPkgNum*sseDAREEncPackageBlockSize
|
||||||
|
// Locate the part containing the end of the required range
|
||||||
|
endOffset := off + length - 1
|
||||||
|
for i1, size := range sizes[partStart:] {
|
||||||
|
i := partStart + i1
|
||||||
|
if endOffset < cumulativeSum+size {
|
||||||
|
partEnd = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cumulativeSum += size
|
||||||
|
encPartSize, _ := sio.EncryptedSize(uint64(size))
|
||||||
|
encCumulativeSum += int64(encPartSize)
|
||||||
|
}
|
||||||
|
// partEnd is always found in the loop above, because off and
|
||||||
|
// length are validated.
|
||||||
|
endPkgNum := (endOffset - cumulativeSum) / sseDAREPackageBlockSize
|
||||||
|
// Compute endEncOffset with one additional DARE package (so
|
||||||
|
// we read the package containing the last desired byte).
|
||||||
|
endEncOffset := encCumulativeSum + (endPkgNum+1)*sseDAREEncPackageBlockSize
|
||||||
|
// Check if the DARE package containing the end offset is a
|
||||||
|
// full sized package (as the last package in the part may be
|
||||||
|
// smaller)
|
||||||
|
lastPartSize, _ := sio.EncryptedSize(uint64(sizes[partEnd]))
|
||||||
|
if endEncOffset > encCumulativeSum+int64(lastPartSize) {
|
||||||
|
endEncOffset = encCumulativeSum + int64(lastPartSize)
|
||||||
|
}
|
||||||
|
encLength = endEncOffset - encOff
|
||||||
|
// Set the sequence number as the starting package number of
|
||||||
|
// the requested block
|
||||||
|
seqNumber = uint32(startPkgNum)
|
||||||
|
return encOff, encLength, skipLen, seqNumber, partStart, nil
|
||||||
|
}
|
||||||
|
|
||||||
// EncryptedSize returns the size of the object after encryption.
|
// EncryptedSize returns the size of the object after encryption.
|
||||||
// An encrypted object is always larger than a plain object
|
// An encrypted object is always larger than a plain object
|
||||||
// except for zero size objects.
|
// except for zero size objects.
|
||||||
@@ -700,7 +1117,7 @@ func DecryptCopyObjectInfo(info *ObjectInfo, headers http.Header) (apiErr APIErr
|
|||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
if info.Size, err = info.DecryptedSize(); err != nil {
|
if info.Size, err = info.DecryptedSize(); err != nil {
|
||||||
apiErr = toAPIErrorCode(err)
|
apiErr = toAPIErrorCode(context.Background(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -713,28 +1130,30 @@ func DecryptCopyObjectInfo(info *ObjectInfo, headers http.Header) (apiErr APIErr
|
|||||||
// decryption succeeded.
|
// decryption succeeded.
|
||||||
//
|
//
|
||||||
// DecryptObjectInfo also returns whether the object is encrypted or not.
|
// DecryptObjectInfo also returns whether the object is encrypted or not.
|
||||||
func DecryptObjectInfo(info *ObjectInfo, headers http.Header) (apiErr APIErrorCode, encrypted bool) {
|
func DecryptObjectInfo(info *ObjectInfo, headers http.Header) (encrypted bool, err error) {
|
||||||
// Directories are never encrypted.
|
// Directories are never encrypted.
|
||||||
if info.IsDir {
|
if info.IsDir {
|
||||||
return ErrNone, false
|
return false, nil
|
||||||
}
|
}
|
||||||
// disallow X-Amz-Server-Side-Encryption header on HEAD and GET
|
// disallow X-Amz-Server-Side-Encryption header on HEAD and GET
|
||||||
if crypto.S3.IsRequested(headers) {
|
if crypto.S3.IsRequested(headers) {
|
||||||
apiErr = ErrInvalidEncryptionParameters
|
err = errInvalidEncryptionParameters
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if apiErr, encrypted = ErrNone, crypto.IsEncrypted(info.UserDefined); !encrypted && crypto.SSEC.IsRequested(headers) {
|
if err, encrypted = nil, crypto.IsEncrypted(info.UserDefined); !encrypted && crypto.SSEC.IsRequested(headers) {
|
||||||
apiErr = ErrInvalidEncryptionParameters
|
err = errInvalidEncryptionParameters
|
||||||
} else if encrypted {
|
} else if encrypted {
|
||||||
if (crypto.SSEC.IsEncrypted(info.UserDefined) && !crypto.SSEC.IsRequested(headers)) ||
|
if (crypto.SSEC.IsEncrypted(info.UserDefined) && !crypto.SSEC.IsRequested(headers)) ||
|
||||||
(crypto.S3.IsEncrypted(info.UserDefined) && crypto.SSEC.IsRequested(headers)) {
|
(crypto.S3.IsEncrypted(info.UserDefined) && crypto.SSEC.IsRequested(headers)) {
|
||||||
apiErr = ErrSSEEncryptedObject
|
err = errEncryptedObject
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var err error
|
_, err = info.DecryptedSize()
|
||||||
if info.Size, err = info.DecryptedSize(); err != nil {
|
|
||||||
apiErr = toAPIErrorCode(err)
|
if crypto.IsEncrypted(info.UserDefined) && !crypto.IsMultiPart(info.UserDefined) {
|
||||||
|
info.ETag = getDecryptedETag(headers, *info, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+356
-247
@@ -18,11 +18,12 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
humanize "github.com/dustin/go-humanize"
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
|
"github.com/minio/sio"
|
||||||
)
|
)
|
||||||
|
|
||||||
var hasServerSideEncryptionHeaderTests = []struct {
|
var hasServerSideEncryptionHeaderTests = []struct {
|
||||||
@@ -36,7 +37,7 @@ var hasServerSideEncryptionHeaderTests = []struct {
|
|||||||
{headers: map[string]string{}, sseRequest: false}, // 4
|
{headers: map[string]string{}, sseRequest: false}, // 4
|
||||||
{headers: map[string]string{crypto.SSECopyAlgorithm + " ": "AES256", " " + crypto.SSECopyKey: "key", crypto.SSECopyKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
|
{headers: map[string]string{crypto.SSECopyAlgorithm + " ": "AES256", " " + crypto.SSECopyKey: "key", crypto.SSECopyKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
|
||||||
{headers: map[string]string{crypto.SSECopyAlgorithm: "", crypto.SSECopyKey: "", crypto.SSECopyKeyMD5: ""}, sseRequest: false}, // 6
|
{headers: map[string]string{crypto.SSECopyAlgorithm: "", crypto.SSECopyKey: "", crypto.SSECopyKeyMD5: ""}, sseRequest: false}, // 6
|
||||||
{headers: map[string]string{crypto.SSEHeader: ""}, sseRequest: true}, // 6
|
{headers: map[string]string{crypto.SSEHeader: ""}, sseRequest: true}, // 7
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHasServerSideEncryptionHeader(t *testing.T) {
|
func TestHasServerSideEncryptionHeader(t *testing.T) {
|
||||||
@@ -88,12 +89,12 @@ var hasSSECustomerHeaderTests = []struct {
|
|||||||
{headers: map[string]string{crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 3
|
{headers: map[string]string{crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 3
|
||||||
{headers: map[string]string{}, sseRequest: false}, // 4
|
{headers: map[string]string{}, sseRequest: false}, // 4
|
||||||
{headers: map[string]string{crypto.SSECAlgorithm + " ": "AES256", " " + crypto.SSECKey: "key", crypto.SSECKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
|
{headers: map[string]string{crypto.SSECAlgorithm + " ": "AES256", " " + crypto.SSECKey: "key", crypto.SSECKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
|
||||||
{headers: map[string]string{crypto.SSECAlgorithm: "", crypto.SSECKey: "", crypto.SSECKeyMD5: ""}, sseRequest: false}, // 6
|
{headers: map[string]string{crypto.SSECAlgorithm: "", crypto.SSECKey: "", crypto.SSECKeyMD5: ""}, sseRequest: true}, // 6
|
||||||
{headers: map[string]string{crypto.SSEHeader: ""}, sseRequest: false}, // 7
|
{headers: map[string]string{crypto.SSEHeader: ""}, sseRequest: false}, // 7
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TesthasSSECustomerHeader(t *testing.T) {
|
func TestHasSSECustomerHeader(t *testing.T) {
|
||||||
for i, test := range hasSSECustomerHeaderTests {
|
for i, test := range hasSSECustomerHeaderTests {
|
||||||
headers := http.Header{}
|
headers := http.Header{}
|
||||||
for k, v := range test.headers {
|
for k, v := range test.headers {
|
||||||
@@ -105,237 +106,6 @@ func TesthasSSECustomerHeader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var parseSSECustomerRequestTests = []struct {
|
|
||||||
headers map[string]string
|
|
||||||
useTLS bool
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
|
||||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
|
|
||||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
useTLS: false, err: errInsecureSSERequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES 256",
|
|
||||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
|
|
||||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrInvalidCustomerAlgorithm,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
|
|
||||||
crypto.SSECKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
|
|
||||||
crypto.SSECKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
|
|
||||||
crypto.SSECKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
|
|
||||||
crypto.SSECKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "", // 7
|
|
||||||
crypto.SSECKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrMissingCustomerKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
|
||||||
crypto.SSECKeyMD5: "",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrMissingCustomerKeyMD5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECAlgorithm: "AES256",
|
|
||||||
crypto.SSECKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
|
||||||
crypto.SSECKeyMD5: "",
|
|
||||||
crypto.SSEHeader: "",
|
|
||||||
},
|
|
||||||
useTLS: true, err: crypto.ErrIncompatibleEncryptionMethod,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseSSECustomerRequest(t *testing.T) {
|
|
||||||
defer func(flag bool) { globalIsSSL = flag }(globalIsSSL)
|
|
||||||
for i, test := range parseSSECustomerRequestTests {
|
|
||||||
headers := http.Header{}
|
|
||||||
for k, v := range test.headers {
|
|
||||||
headers.Set(k, v)
|
|
||||||
}
|
|
||||||
request := &http.Request{}
|
|
||||||
request.Header = headers
|
|
||||||
globalIsSSL = test.useTLS
|
|
||||||
|
|
||||||
_, err := ParseSSECustomerRequest(request)
|
|
||||||
if err != test.err {
|
|
||||||
t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err)
|
|
||||||
}
|
|
||||||
key := request.Header.Get(crypto.SSECKey)
|
|
||||||
if (err == nil || err == crypto.ErrCustomerKeyMD5Mismatch) && key != "" {
|
|
||||||
t.Errorf("Test %d: Client key survived parsing - found key: %v", i, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var parseSSECopyCustomerRequestTests = []struct {
|
|
||||||
headers map[string]string
|
|
||||||
metadata map[string]string
|
|
||||||
useTLS bool
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
|
||||||
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
|
||||||
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key": base64.StdEncoding.EncodeToString(make([]byte, 64))},
|
|
||||||
useTLS: true, err: crypto.ErrIncompatibleEncryptionMethod,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
|
|
||||||
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: false, err: errInsecureSSERequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES 256",
|
|
||||||
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
|
|
||||||
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrInvalidCustomerAlgorithm,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
|
|
||||||
crypto.SSECopyKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
|
|
||||||
crypto.SSECopyKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
|
|
||||||
crypto.SSECopyKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
|
|
||||||
crypto.SSECopyKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "", // 7
|
|
||||||
crypto.SSECopyKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrMissingCustomerKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: map[string]string{
|
|
||||||
crypto.SSECopyAlgorithm: "AES256",
|
|
||||||
crypto.SSECopyKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
|
||||||
crypto.SSECopyKeyMD5: "",
|
|
||||||
},
|
|
||||||
metadata: map[string]string{},
|
|
||||||
useTLS: true, err: crypto.ErrMissingCustomerKeyMD5,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseSSECopyCustomerRequest(t *testing.T) {
|
|
||||||
defer func(flag bool) { globalIsSSL = flag }(globalIsSSL)
|
|
||||||
for i, test := range parseSSECopyCustomerRequestTests {
|
|
||||||
headers := http.Header{}
|
|
||||||
for k, v := range test.headers {
|
|
||||||
headers.Set(k, v)
|
|
||||||
}
|
|
||||||
request := &http.Request{}
|
|
||||||
request.Header = headers
|
|
||||||
globalIsSSL = test.useTLS
|
|
||||||
|
|
||||||
_, err := ParseSSECopyCustomerRequest(request, test.metadata)
|
|
||||||
if err != test.err {
|
|
||||||
t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err)
|
|
||||||
}
|
|
||||||
key := request.Header.Get(crypto.SSECopyKey)
|
|
||||||
if (err == nil || err == crypto.ErrCustomerKeyMD5Mismatch) && key != "" {
|
|
||||||
t.Errorf("Test %d: Client key survived parsing - found key: %v", i, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var encryptRequestTests = []struct {
|
var encryptRequestTests = []struct {
|
||||||
header map[string]string
|
header map[string]string
|
||||||
metadata map[string]string
|
metadata map[string]string
|
||||||
@@ -369,14 +139,11 @@ func TestEncryptRequest(t *testing.T) {
|
|||||||
for k, v := range test.header {
|
for k, v := range test.header {
|
||||||
req.Header.Set(k, v)
|
req.Header.Set(k, v)
|
||||||
}
|
}
|
||||||
_, err := EncryptRequest(content, req, "bucket", "object", test.metadata)
|
_, _, err := EncryptRequest(content, req, "bucket", "object", test.metadata)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
||||||
}
|
}
|
||||||
if key, ok := test.metadata[crypto.SSECKey]; ok {
|
|
||||||
t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key)
|
|
||||||
}
|
|
||||||
if kdf, ok := test.metadata[crypto.SSESealAlgorithm]; !ok {
|
if kdf, ok := test.metadata[crypto.SSESealAlgorithm]; !ok {
|
||||||
t.Errorf("Test %d: ServerSideEncryptionKDF must be part of metadata: %v", i, kdf)
|
t.Errorf("Test %d: ServerSideEncryptionKDF must be part of metadata: %v", i, kdf)
|
||||||
}
|
}
|
||||||
@@ -521,43 +288,43 @@ func TestDecryptRequest(t *testing.T) {
|
|||||||
var decryptObjectInfoTests = []struct {
|
var decryptObjectInfoTests = []struct {
|
||||||
info ObjectInfo
|
info ObjectInfo
|
||||||
headers http.Header
|
headers http.Header
|
||||||
expErr APIErrorCode
|
expErr error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 100},
|
info: ObjectInfo{Size: 100},
|
||||||
headers: http.Header{},
|
headers: http.Header{},
|
||||||
expErr: ErrNone,
|
expErr: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||||
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||||
expErr: ErrNone,
|
expErr: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 0, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
info: ObjectInfo{Size: 0, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||||
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||||
expErr: ErrNone,
|
expErr: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.SSECSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA=="}},
|
info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.SSECSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA=="}},
|
||||||
headers: http.Header{},
|
headers: http.Header{},
|
||||||
expErr: ErrSSEEncryptedObject,
|
expErr: errEncryptedObject,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{}},
|
info: ObjectInfo{Size: 100, UserDefined: map[string]string{}},
|
||||||
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||||
expErr: ErrInvalidEncryptionParameters,
|
expErr: errInvalidEncryptionParameters,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 31, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
info: ObjectInfo{Size: 31, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||||
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||||
expErr: ErrObjectTampered,
|
expErr: errObjectTampered,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecryptObjectInfo(t *testing.T) {
|
func TestDecryptObjectInfo(t *testing.T) {
|
||||||
for i, test := range decryptObjectInfoTests {
|
for i, test := range decryptObjectInfoTests {
|
||||||
if err, encrypted := DecryptObjectInfo(&test.info, test.headers); err != test.expErr {
|
if encrypted, err := DecryptObjectInfo(&test.info, test.headers); err != test.expErr {
|
||||||
t.Errorf("Test %d: Decryption returned wrong error code: got %d , want %d", i, err, test.expErr)
|
t.Errorf("Test %d: Decryption returned wrong error code: got %d , want %d", i, err, test.expErr)
|
||||||
} else if enc := crypto.IsEncrypted(test.info.UserDefined); encrypted && enc != encrypted {
|
} else if enc := crypto.IsEncrypted(test.info.UserDefined); encrypted && enc != encrypted {
|
||||||
t.Errorf("Test %d: Decryption thinks object is encrypted but it is not", i)
|
t.Errorf("Test %d: Decryption thinks object is encrypted but it is not", i)
|
||||||
@@ -566,3 +333,345 @@ func TestDecryptObjectInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests for issue reproduced when getting the right encrypted
|
||||||
|
// offset of the object.
|
||||||
|
func TestGetDecryptedRange_Issue50(t *testing.T) {
|
||||||
|
rs, err := parseRequestRangeSpec("bytes=594870256-594870263")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
objInfo := ObjectInfo{
|
||||||
|
Bucket: "bucket",
|
||||||
|
Name: "object",
|
||||||
|
Size: 595160760,
|
||||||
|
UserDefined: map[string]string{
|
||||||
|
crypto.SSEMultipart: "",
|
||||||
|
crypto.SSEIV: "HTexa=",
|
||||||
|
crypto.SSESealAlgorithm: "DAREv2-HMAC-SHA256",
|
||||||
|
crypto.SSECSealedKey: "IAA8PGAA==",
|
||||||
|
ReservedMetadataPrefix + "actual-size": "594870264",
|
||||||
|
"content-type": "application/octet-stream",
|
||||||
|
"etag": "166b1545b4c1535294ee0686678bea8c-2",
|
||||||
|
},
|
||||||
|
Parts: []objectPartInfo{
|
||||||
|
{
|
||||||
|
Number: 1,
|
||||||
|
Name: "part.1",
|
||||||
|
ETag: "etag1",
|
||||||
|
Size: 297580380,
|
||||||
|
ActualSize: 297435132,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Number: 2,
|
||||||
|
Name: "part.2",
|
||||||
|
ETag: "etag2",
|
||||||
|
Size: 297580380,
|
||||||
|
ActualSize: 297435132,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
encOff, encLength, skipLen, seqNumber, partStart, err := objInfo.GetDecryptedRange(rs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test: failed %s", err)
|
||||||
|
}
|
||||||
|
if encOff != 595127964 {
|
||||||
|
t.Fatalf("Test: expected %d, got %d", 595127964, encOff)
|
||||||
|
}
|
||||||
|
if encLength != 32796 {
|
||||||
|
t.Fatalf("Test: expected %d, got %d", 32796, encLength)
|
||||||
|
}
|
||||||
|
if skipLen != 32756 {
|
||||||
|
t.Fatalf("Test: expected %d, got %d", 32756, skipLen)
|
||||||
|
}
|
||||||
|
if seqNumber != 4538 {
|
||||||
|
t.Fatalf("Test: expected %d, got %d", 4538, seqNumber)
|
||||||
|
}
|
||||||
|
if partStart != 1 {
|
||||||
|
t.Fatalf("Test: expected %d, got %d", 1, partStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDecryptedRange(t *testing.T) {
|
||||||
|
var (
|
||||||
|
pkgSz = int64(64) * humanize.KiByte
|
||||||
|
minPartSz = int64(5) * humanize.MiByte
|
||||||
|
maxPartSz = int64(5) * humanize.GiByte
|
||||||
|
|
||||||
|
getEncSize = func(s int64) int64 {
|
||||||
|
v, _ := sio.EncryptedSize(uint64(s))
|
||||||
|
return int64(v)
|
||||||
|
}
|
||||||
|
udMap = func(isMulti bool) map[string]string {
|
||||||
|
m := map[string]string{
|
||||||
|
crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
|
||||||
|
crypto.SSEMultipart: "1",
|
||||||
|
}
|
||||||
|
if !isMulti {
|
||||||
|
delete(m, crypto.SSEMultipart)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Single part object tests
|
||||||
|
var (
|
||||||
|
mkSPObj = func(s int64) ObjectInfo {
|
||||||
|
return ObjectInfo{
|
||||||
|
Size: getEncSize(s),
|
||||||
|
UserDefined: udMap(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
testSP := []struct {
|
||||||
|
decSz int64
|
||||||
|
oi ObjectInfo
|
||||||
|
}{
|
||||||
|
{0, mkSPObj(0)},
|
||||||
|
{1, mkSPObj(1)},
|
||||||
|
{pkgSz - 1, mkSPObj(pkgSz - 1)},
|
||||||
|
{pkgSz, mkSPObj(pkgSz)},
|
||||||
|
{2*pkgSz - 1, mkSPObj(2*pkgSz - 1)},
|
||||||
|
{minPartSz, mkSPObj(minPartSz)},
|
||||||
|
{maxPartSz, mkSPObj(maxPartSz)},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range testSP {
|
||||||
|
{
|
||||||
|
// nil range
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
if skip != 0 || sn != 0 || ps != 0 || o != 0 || l != getEncSize(test.decSz) {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.decSz >= 10 {
|
||||||
|
// first 10 bytes
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{false, 0, 9})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
var rLen = pkgSz + 32
|
||||||
|
if test.decSz < pkgSz {
|
||||||
|
rLen = test.decSz + 32
|
||||||
|
}
|
||||||
|
if skip != 0 || sn != 0 || ps != 0 || o != 0 || l != rLen {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kb32 := int64(32) * humanize.KiByte
|
||||||
|
if test.decSz >= (64+32)*humanize.KiByte {
|
||||||
|
// Skip the first 32Kib, and read the next 64Kib
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{false, kb32, 3*kb32 - 1})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
var rLen = (pkgSz + 32) * 2
|
||||||
|
if test.decSz < 2*pkgSz {
|
||||||
|
rLen = (pkgSz + 32) + (test.decSz - pkgSz + 32)
|
||||||
|
}
|
||||||
|
if skip != kb32 || sn != 0 || ps != 0 || o != 0 || l != rLen {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.decSz >= (64*2+32)*humanize.KiByte {
|
||||||
|
// Skip the first 96Kib and read the next 64Kib
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{false, 3 * kb32, 5*kb32 - 1})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
var rLen = (pkgSz + 32) * 2
|
||||||
|
if test.decSz-pkgSz < 2*pkgSz {
|
||||||
|
rLen = (pkgSz + 32) + (test.decSz - pkgSz + 32*2)
|
||||||
|
}
|
||||||
|
if skip != kb32 || sn != 1 || ps != 0 || o != pkgSz+32 || l != rLen {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multipart object tests
|
||||||
|
var (
|
||||||
|
// make a multipart object-info given part sizes
|
||||||
|
mkMPObj = func(sizes []int64) ObjectInfo {
|
||||||
|
r := make([]objectPartInfo, len(sizes))
|
||||||
|
sum := int64(0)
|
||||||
|
for i, s := range sizes {
|
||||||
|
r[i].Number = i
|
||||||
|
r[i].Size = int64(getEncSize(s))
|
||||||
|
sum += r[i].Size
|
||||||
|
}
|
||||||
|
return ObjectInfo{
|
||||||
|
Size: sum,
|
||||||
|
UserDefined: udMap(true),
|
||||||
|
Parts: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Simple useful utilities
|
||||||
|
repeat = func(k int64, n int) []int64 {
|
||||||
|
a := []int64{}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
a = append(a, k)
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
lsum = func(s []int64) int64 {
|
||||||
|
sum := int64(0)
|
||||||
|
for _, i := range s {
|
||||||
|
if i < 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
sum += i
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
esum = func(oi ObjectInfo) int64 {
|
||||||
|
sum := int64(0)
|
||||||
|
for _, i := range oi.Parts {
|
||||||
|
sum += i.Size
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
s1 := []int64{5487701, 5487799, 3}
|
||||||
|
s2 := repeat(5487701, 5)
|
||||||
|
s3 := repeat(maxPartSz, 10000)
|
||||||
|
testMPs := []struct {
|
||||||
|
decSizes []int64
|
||||||
|
oi ObjectInfo
|
||||||
|
}{
|
||||||
|
{s1, mkMPObj(s1)},
|
||||||
|
{s2, mkMPObj(s2)},
|
||||||
|
{s3, mkMPObj(s3)},
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is a reference (re-)implementation of
|
||||||
|
// decrypted range computation, written solely for the purpose
|
||||||
|
// of the unit tests.
|
||||||
|
//
|
||||||
|
// `s` gives the decrypted part sizes, and the other
|
||||||
|
// parameters describe the desired read segment. When
|
||||||
|
// `isFromEnd` is true, `skipLen` argument is ignored.
|
||||||
|
decryptedRangeRef := func(s []int64, skipLen, readLen int64, isFromEnd bool) (o, l, skip int64, sn uint32, ps int) {
|
||||||
|
oSize := lsum(s)
|
||||||
|
if isFromEnd {
|
||||||
|
skipLen = oSize - readLen
|
||||||
|
}
|
||||||
|
if skipLen < 0 || readLen < 0 || oSize < 0 || skipLen+readLen > oSize {
|
||||||
|
t.Fatalf("Impossible read specified: %d %d %d", skipLen, readLen, oSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cumulativeSum, cumulativeEncSum int64
|
||||||
|
toRead := readLen
|
||||||
|
readStart := false
|
||||||
|
for i, v := range s {
|
||||||
|
partOffset := int64(0)
|
||||||
|
partDarePkgOffset := int64(0)
|
||||||
|
if !readStart && cumulativeSum+v > skipLen {
|
||||||
|
// Read starts at the current part
|
||||||
|
readStart = true
|
||||||
|
|
||||||
|
partOffset = skipLen - cumulativeSum
|
||||||
|
|
||||||
|
// All return values except `l` are
|
||||||
|
// calculated here.
|
||||||
|
sn = uint32(partOffset / pkgSz)
|
||||||
|
skip = partOffset % pkgSz
|
||||||
|
ps = i
|
||||||
|
o = cumulativeEncSum + int64(sn)*(pkgSz+32)
|
||||||
|
|
||||||
|
partDarePkgOffset = partOffset - skip
|
||||||
|
}
|
||||||
|
if readStart {
|
||||||
|
currentPartBytes := v - partOffset
|
||||||
|
currentPartDareBytes := v - partDarePkgOffset
|
||||||
|
if currentPartBytes < toRead {
|
||||||
|
toRead -= currentPartBytes
|
||||||
|
l += getEncSize(currentPartDareBytes)
|
||||||
|
} else {
|
||||||
|
// current part has the last
|
||||||
|
// byte required
|
||||||
|
lbPartOffset := partOffset + toRead - 1
|
||||||
|
|
||||||
|
// round up the lbPartOffset
|
||||||
|
// to the end of the
|
||||||
|
// corresponding DARE package
|
||||||
|
lbPkgEndOffset := lbPartOffset - (lbPartOffset % pkgSz) + pkgSz
|
||||||
|
if lbPkgEndOffset > v {
|
||||||
|
lbPkgEndOffset = v
|
||||||
|
}
|
||||||
|
bytesToDrop := v - lbPkgEndOffset
|
||||||
|
|
||||||
|
// Last segment to update `l`
|
||||||
|
l += getEncSize(currentPartDareBytes - bytesToDrop)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cumulativeSum += v
|
||||||
|
cumulativeEncSum += getEncSize(v)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range testMPs {
|
||||||
|
{
|
||||||
|
// nil range
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
if o != 0 || l != esum(test.oi) || skip != 0 || sn != 0 || ps != 0 {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip 1Mib and read 1Mib (in the decrypted object)
|
||||||
|
//
|
||||||
|
// The check below ensures the object is large enough
|
||||||
|
// for the read.
|
||||||
|
if lsum(test.decSizes) >= 2*humanize.MiByte {
|
||||||
|
skipLen, readLen := int64(1)*humanize.MiByte, int64(1)*humanize.MiByte
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{false, skipLen, skipLen + readLen - 1})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oRef, lRef, skipRef, snRef, psRef := decryptedRangeRef(test.decSizes, skipLen, readLen, false)
|
||||||
|
if o != oRef || l != lRef || skip != skipRef || sn != snRef || ps != psRef {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d (Ref: %d %d %d %d %d)",
|
||||||
|
i, o, l, skip, sn, ps, oRef, lRef, skipRef, snRef, psRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the last 6Mib+1 bytes of the (decrypted)
|
||||||
|
// object
|
||||||
|
//
|
||||||
|
// The check below ensures the object is large enough
|
||||||
|
// for the read.
|
||||||
|
readLen := int64(6)*humanize.MiByte + 1
|
||||||
|
if lsum(test.decSizes) >= readLen {
|
||||||
|
o, l, skip, sn, ps, err := test.oi.GetDecryptedRange(&HTTPRangeSpec{true, -readLen, -1})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Case %d: unexpected err: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oRef, lRef, skipRef, snRef, psRef := decryptedRangeRef(test.decSizes, 0, readLen, true)
|
||||||
|
if o != oRef || l != lRef || skip != skipRef || sn != snRef || ps != psRef {
|
||||||
|
t.Errorf("Case %d: test failed: %d %d %d %d %d (Ref: %d %d %d %d %d)",
|
||||||
|
i, o, l, skip, sn, ps, oRef, lRef, skipRef, snRef, psRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+102
-45
@@ -226,6 +226,17 @@ func TestGetSetIndexes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getHexSequences(start int, number int, paddinglen int) (seq []string) {
|
||||||
|
for i := start; i <= number; i++ {
|
||||||
|
if paddinglen == 0 {
|
||||||
|
seq = append(seq, fmt.Sprintf("%x", i))
|
||||||
|
} else {
|
||||||
|
seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dx", paddinglen), i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seq
|
||||||
|
}
|
||||||
|
|
||||||
func getSequences(start int, number int, paddinglen int) (seq []string) {
|
func getSequences(start int, number int, paddinglen int) (seq []string) {
|
||||||
for i := start; i <= number; i++ {
|
for i := start; i <= number; i++ {
|
||||||
if paddinglen == 0 {
|
if paddinglen == 0 {
|
||||||
@@ -287,9 +298,9 @@ func TestParseEndpointSet(t *testing.T) {
|
|||||||
[]ellipses.ArgPattern{
|
[]ellipses.ArgPattern{
|
||||||
[]ellipses.Pattern{
|
[]ellipses.Pattern{
|
||||||
{
|
{
|
||||||
"/export/set",
|
Prefix: "/export/set",
|
||||||
"",
|
Suffix: "",
|
||||||
getSequences(1, 64, 0),
|
Seq: getSequences(1, 64, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -305,14 +316,14 @@ func TestParseEndpointSet(t *testing.T) {
|
|||||||
[]ellipses.ArgPattern{
|
[]ellipses.ArgPattern{
|
||||||
[]ellipses.Pattern{
|
[]ellipses.Pattern{
|
||||||
{
|
{
|
||||||
"",
|
Prefix: "",
|
||||||
"",
|
Suffix: "",
|
||||||
getSequences(1, 64, 0),
|
Seq: getSequences(1, 64, 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"http://minio",
|
Prefix: "http://minio",
|
||||||
"/export/set",
|
Suffix: "/export/set",
|
||||||
getSequences(2, 3, 0),
|
Seq: getSequences(2, 3, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -328,9 +339,9 @@ func TestParseEndpointSet(t *testing.T) {
|
|||||||
[]ellipses.ArgPattern{
|
[]ellipses.ArgPattern{
|
||||||
[]ellipses.Pattern{
|
[]ellipses.Pattern{
|
||||||
{
|
{
|
||||||
"http://minio",
|
Prefix: "http://minio",
|
||||||
".mydomain.net/data",
|
Suffix: ".mydomain.net/data",
|
||||||
getSequences(1, 64, 0),
|
Seq: getSequences(1, 64, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -345,14 +356,14 @@ func TestParseEndpointSet(t *testing.T) {
|
|||||||
[]ellipses.ArgPattern{
|
[]ellipses.ArgPattern{
|
||||||
[]ellipses.Pattern{
|
[]ellipses.Pattern{
|
||||||
{
|
{
|
||||||
"",
|
Prefix: "",
|
||||||
"/data",
|
Suffix: "/data",
|
||||||
getSequences(1, 16, 0),
|
Seq: getSequences(1, 16, 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"http://rack",
|
Prefix: "http://rack",
|
||||||
".mydomain.minio",
|
Suffix: ".mydomain.minio",
|
||||||
getSequences(1, 4, 0),
|
Seq: getSequences(1, 4, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -368,14 +379,14 @@ func TestParseEndpointSet(t *testing.T) {
|
|||||||
[]ellipses.ArgPattern{
|
[]ellipses.ArgPattern{
|
||||||
[]ellipses.Pattern{
|
[]ellipses.Pattern{
|
||||||
{
|
{
|
||||||
"",
|
Prefix: "",
|
||||||
"",
|
Suffix: "",
|
||||||
getSequences(0, 1, 0),
|
Seq: getSequences(0, 1, 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"http://minio",
|
Prefix: "http://minio",
|
||||||
".mydomain.net/data",
|
Suffix: ".mydomain.net/data",
|
||||||
getSequences(0, 15, 0),
|
Seq: getSequences(0, 15, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -391,9 +402,9 @@ func TestParseEndpointSet(t *testing.T) {
|
|||||||
[]ellipses.ArgPattern{
|
[]ellipses.ArgPattern{
|
||||||
[]ellipses.Pattern{
|
[]ellipses.Pattern{
|
||||||
{
|
{
|
||||||
"http://server1/data",
|
Prefix: "http://server1/data",
|
||||||
"",
|
Suffix: "",
|
||||||
getSequences(1, 32, 0),
|
Seq: getSequences(1, 32, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -409,9 +420,9 @@ func TestParseEndpointSet(t *testing.T) {
|
|||||||
[]ellipses.ArgPattern{
|
[]ellipses.ArgPattern{
|
||||||
[]ellipses.Pattern{
|
[]ellipses.Pattern{
|
||||||
{
|
{
|
||||||
"http://server1/data",
|
Prefix: "http://server1/data",
|
||||||
"",
|
Suffix: "",
|
||||||
getSequences(1, 32, 2),
|
Seq: getSequences(1, 32, 2),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -427,19 +438,19 @@ func TestParseEndpointSet(t *testing.T) {
|
|||||||
[]ellipses.ArgPattern{
|
[]ellipses.ArgPattern{
|
||||||
[]ellipses.Pattern{
|
[]ellipses.Pattern{
|
||||||
{
|
{
|
||||||
"",
|
Prefix: "",
|
||||||
"",
|
Suffix: "",
|
||||||
getSequences(1, 2, 0),
|
Seq: getSequences(1, 2, 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"",
|
Prefix: "",
|
||||||
"/test",
|
Suffix: "/test",
|
||||||
getSequences(1, 64, 0),
|
Seq: getSequences(1, 64, 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"http://minio",
|
Prefix: "http://minio",
|
||||||
"/export/set",
|
Suffix: "/export/set",
|
||||||
getSequences(2, 3, 0),
|
Seq: getSequences(2, 3, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -456,14 +467,60 @@ func TestParseEndpointSet(t *testing.T) {
|
|||||||
[]ellipses.ArgPattern{
|
[]ellipses.ArgPattern{
|
||||||
[]ellipses.Pattern{
|
[]ellipses.Pattern{
|
||||||
{
|
{
|
||||||
"",
|
Prefix: "",
|
||||||
"",
|
Suffix: "",
|
||||||
getSequences(1, 10, 0),
|
Seq: getSequences(1, 10, 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"/export",
|
Prefix: "/export",
|
||||||
"/disk",
|
Suffix: "/disk",
|
||||||
getSequences(1, 10, 0),
|
Seq: getSequences(1, 10, 0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
[][]uint64{{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}},
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
// IPv6 ellipses with hexadecimal expansion
|
||||||
|
{
|
||||||
|
"http://[2001:3984:3989::{1...a}]/disk{1...10}",
|
||||||
|
endpointSet{
|
||||||
|
[]ellipses.ArgPattern{
|
||||||
|
[]ellipses.Pattern{
|
||||||
|
{
|
||||||
|
Prefix: "",
|
||||||
|
Suffix: "",
|
||||||
|
Seq: getSequences(1, 10, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prefix: "http://[2001:3984:3989::",
|
||||||
|
Suffix: "]/disk",
|
||||||
|
Seq: getHexSequences(1, 10, 0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
[][]uint64{{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}},
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
// IPv6 ellipses with hexadecimal expansion with 3 position numerics.
|
||||||
|
{
|
||||||
|
"http://[2001:3984:3989::{001...00a}]/disk{1...10}",
|
||||||
|
endpointSet{
|
||||||
|
[]ellipses.ArgPattern{
|
||||||
|
[]ellipses.Pattern{
|
||||||
|
{
|
||||||
|
Prefix: "",
|
||||||
|
Suffix: "",
|
||||||
|
Seq: getSequences(1, 10, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Prefix: "http://[2001:3984:3989::",
|
||||||
|
Suffix: "]/disk",
|
||||||
|
Seq: getHexSequences(1, 10, 3),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+42
-23
@@ -112,6 +112,9 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
|||||||
return ep, 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 i := strings.Index(host, "%"); i > -1 {
|
||||||
|
host = host[:i]
|
||||||
|
}
|
||||||
|
|
||||||
if host == "" {
|
if host == "" {
|
||||||
return ep, fmt.Errorf("invalid URL endpoint format: empty host name")
|
return ep, fmt.Errorf("invalid URL endpoint format: empty host name")
|
||||||
@@ -152,7 +155,7 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
|||||||
// Only check if the arg is an ip address and ask for scheme since its absent.
|
// Only check if the arg is an ip address and ask for scheme since its absent.
|
||||||
// localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as
|
// localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as
|
||||||
// /mnt/export1. So we go ahead and start the minio server in FS modes in these cases.
|
// /mnt/export1. So we go ahead and start the minio server in FS modes in these cases.
|
||||||
if isHostIPv4(arg) {
|
if isHostIP(arg) {
|
||||||
return ep, fmt.Errorf("invalid URL endpoint format: missing scheme http or https")
|
return ep, fmt.Errorf("invalid URL endpoint format: missing scheme http or https")
|
||||||
}
|
}
|
||||||
u = &url.URL{Path: path.Clean(arg)}
|
u = &url.URL{Path: path.Clean(arg)}
|
||||||
@@ -168,6 +171,18 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
|||||||
// EndpointList - list of same type of endpoint.
|
// EndpointList - list of same type of endpoint.
|
||||||
type EndpointList []Endpoint
|
type EndpointList []Endpoint
|
||||||
|
|
||||||
|
// Nodes - returns number of unique servers.
|
||||||
|
func (endpoints EndpointList) Nodes() int {
|
||||||
|
uniqueNodes := set.NewStringSet()
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
if uniqueNodes.Contains(endpoint.Host) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uniqueNodes.Add(endpoint.Host)
|
||||||
|
}
|
||||||
|
return len(uniqueNodes)
|
||||||
|
}
|
||||||
|
|
||||||
// IsHTTPS - returns true if secure for URLEndpointType.
|
// IsHTTPS - returns true if secure for URLEndpointType.
|
||||||
func (endpoints EndpointList) IsHTTPS() bool {
|
func (endpoints EndpointList) IsHTTPS() bool {
|
||||||
return endpoints[0].IsHTTPS()
|
return endpoints[0].IsHTTPS()
|
||||||
@@ -212,7 +227,6 @@ func NewEndpointList(args ...string) (endpoints EndpointList, err error) {
|
|||||||
uniqueArgs.Add(arg)
|
uniqueArgs.Add(arg)
|
||||||
endpoints = append(endpoints, endpoint)
|
endpoints = append(endpoints, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
return endpoints, nil
|
return endpoints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +343,7 @@ func CreateEndpoints(serverAddr string, args ...[]string) (string, EndpointList,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
host = endpoint.Host
|
host = endpoint.Host
|
||||||
}
|
}
|
||||||
hostIPSet, _ := getHostIP4(host)
|
hostIPSet, _ := getHostIP(host)
|
||||||
if IPSet, ok := pathIPMap[endpoint.Path]; ok {
|
if IPSet, ok := pathIPMap[endpoint.Path]; ok {
|
||||||
if !IPSet.Intersection(hostIPSet).IsEmpty() {
|
if !IPSet.Intersection(hostIPSet).IsEmpty() {
|
||||||
return serverAddr, endpoints, setupType,
|
return serverAddr, endpoints, setupType,
|
||||||
@@ -399,12 +413,12 @@ func CreateEndpoints(serverAddr string, args ...[]string) (string, EndpointList,
|
|||||||
host = localServerAddr
|
host = localServerAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
ipList, err := getHostIP4(host)
|
ipList, err := getHostIP(host)
|
||||||
logger.FatalIf(err, "unexpected error when resolving host '%s'", host)
|
logger.FatalIf(err, "unexpected error when resolving host '%s'", host)
|
||||||
|
|
||||||
// Filter ipList by IPs those start with '127.'.
|
// Filter ipList by IPs those start with '127.' or '::1'
|
||||||
loopBackIPs := ipList.FuncMatch(func(ip string, matchString string) bool {
|
loopBackIPs := ipList.FuncMatch(func(ip string, matchString string) bool {
|
||||||
return strings.HasPrefix(ip, "127.")
|
return strings.HasPrefix(ip, "127.") || strings.HasPrefix(ip, "::1")
|
||||||
}, "")
|
}, "")
|
||||||
|
|
||||||
// If loop back IP is found and ipList contains only loop back IPs, then error out.
|
// If loop back IP is found and ipList contains only loop back IPs, then error out.
|
||||||
@@ -443,7 +457,12 @@ func CreateEndpoints(serverAddr string, args ...[]string) (string, EndpointList,
|
|||||||
return serverAddr, endpoints, setupType, err
|
return serverAddr, endpoints, setupType, err
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDomainIPs(uniqueArgs)
|
_, dok := os.LookupEnv("MINIO_DOMAIN")
|
||||||
|
_, eok := os.LookupEnv("MINIO_ETCD_ENDPOINTS")
|
||||||
|
_, iok := os.LookupEnv("MINIO_PUBLIC_IPS")
|
||||||
|
if dok && eok && !iok {
|
||||||
|
updateDomainIPs(uniqueArgs)
|
||||||
|
}
|
||||||
|
|
||||||
setupType = DistXLSetupType
|
setupType = DistXLSetupType
|
||||||
return serverAddr, endpoints, setupType, nil
|
return serverAddr, endpoints, setupType, nil
|
||||||
@@ -468,10 +487,10 @@ func GetLocalPeer(endpoints EndpointList) (localPeer string) {
|
|||||||
// Local peer can be empty in FS or Erasure coded mode.
|
// Local peer can be empty in FS or Erasure coded mode.
|
||||||
// If so, return globalMinioHost + globalMinioPort value.
|
// If so, return globalMinioHost + globalMinioPort value.
|
||||||
if globalMinioHost != "" {
|
if globalMinioHost != "" {
|
||||||
return globalMinioHost + ":" + globalMinioPort
|
return net.JoinHostPort(globalMinioHost, globalMinioPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "127.0.0.1:" + globalMinioPort
|
return net.JoinHostPort("127.0.0.1", globalMinioPort)
|
||||||
}
|
}
|
||||||
return peerSet.ToSlice()[0]
|
return peerSet.ToSlice()[0]
|
||||||
}
|
}
|
||||||
@@ -497,21 +516,21 @@ func GetRemotePeers(endpoints EndpointList) []string {
|
|||||||
return peerSet.ToSlice()
|
return peerSet.ToSlice()
|
||||||
}
|
}
|
||||||
|
|
||||||
// In federated and distributed setup, update IP addresses of the hosts passed in command line
|
|
||||||
// if MINIO_PUBLIC_IPS are not set manually
|
|
||||||
func updateDomainIPs(endPoints set.StringSet) {
|
func updateDomainIPs(endPoints set.StringSet) {
|
||||||
_, dok := os.LookupEnv("MINIO_DOMAIN")
|
ipList := set.NewStringSet()
|
||||||
_, eok := os.LookupEnv("MINIO_ETCD_ENDPOINTS")
|
for e := range endPoints {
|
||||||
_, iok := os.LookupEnv("MINIO_PUBLIC_IPS")
|
host, _, err := net.SplitHostPort(e)
|
||||||
if dok && eok && !iok {
|
if err != nil {
|
||||||
globalDomainIPs = set.NewStringSet()
|
if strings.Contains(err.Error(), "missing port in address") {
|
||||||
for e := range endPoints {
|
host = e
|
||||||
host, _, _ := net.SplitHostPort(e)
|
} else {
|
||||||
ipList, _ := getHostIP4(host)
|
continue
|
||||||
remoteIPList := ipList.FuncMatch(func(ip string, matchString string) bool {
|
}
|
||||||
return !strings.HasPrefix(ip, "127.")
|
|
||||||
}, "")
|
|
||||||
globalDomainIPs.Add(remoteIPList.ToSlice()[0])
|
|
||||||
}
|
}
|
||||||
|
IPs, _ := getHostIP(host)
|
||||||
|
ipList = ipList.Union(IPs)
|
||||||
}
|
}
|
||||||
|
globalDomainIPs = ipList.FuncMatch(func(ip string, matchString string) bool {
|
||||||
|
return !strings.HasPrefix(ip, "127.") || strings.HasPrefix(ip, "::1")
|
||||||
|
}, "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,27 +304,27 @@ func TestCreateEndpoints(t *testing.T) {
|
|||||||
}, DistXLSetupType, nil},
|
}, DistXLSetupType, nil},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
serverAddr, endpoints, setupType, err := CreateEndpoints(testCase.serverAddr, testCase.args...)
|
serverAddr, endpoints, setupType, err := CreateEndpoints(testCase.serverAddr, testCase.args...)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if testCase.expectedErr != nil {
|
if testCase.expectedErr != nil {
|
||||||
t.Fatalf("error: expected = %v, got = <nil>", testCase.expectedErr)
|
t.Fatalf("Test (%d) error: expected = %v, got = <nil>", i+1, testCase.expectedErr)
|
||||||
} else {
|
} else {
|
||||||
if serverAddr != testCase.expectedServerAddr {
|
if serverAddr != testCase.expectedServerAddr {
|
||||||
t.Fatalf("serverAddr: expected = %v, got = %v", testCase.expectedServerAddr, serverAddr)
|
t.Fatalf("Test (%d) serverAddr: expected = %v, got = %v", i+1, testCase.expectedServerAddr, serverAddr)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(endpoints, testCase.expectedEndpoints) {
|
if !reflect.DeepEqual(endpoints, testCase.expectedEndpoints) {
|
||||||
t.Fatalf("endpoints: expected = %v, got = %v", testCase.expectedEndpoints, endpoints)
|
t.Fatalf("Test (%d) endpoints: expected = %v, got = %v", i+1, testCase.expectedEndpoints, endpoints)
|
||||||
}
|
}
|
||||||
if setupType != testCase.expectedSetupType {
|
if setupType != testCase.expectedSetupType {
|
||||||
t.Fatalf("setupType: expected = %v, got = %v", testCase.expectedSetupType, setupType)
|
t.Fatalf("Test (%d) setupType: expected = %v, got = %v", i+1, testCase.expectedSetupType, setupType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if testCase.expectedErr == nil {
|
} else if testCase.expectedErr == nil {
|
||||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
t.Fatalf("Test (%d) error: expected = <nil>, got = %v", i+1, err)
|
||||||
} else if err.Error() != testCase.expectedErr.Error() {
|
} else if err.Error() != testCase.expectedErr.Error() {
|
||||||
t.Fatalf("error: expected = %v, got = %v", testCase.expectedErr, err)
|
t.Fatalf("Test (%d) error: expected = %v, got = %v", i+1, testCase.expectedErr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EnvKMSMasterKey is the environment variable used to specify
|
||||||
|
// a KMS master key used to protect SSE-S3 per-object keys.
|
||||||
|
// Valid values must be of the from: "KEY_ID:32_BYTE_HEX_VALUE".
|
||||||
|
EnvKMSMasterKey = "MINIO_SSE_MASTER_KEY"
|
||||||
|
|
||||||
|
// EnvAutoEncryption is the environment variable used to en/disable
|
||||||
|
// SSE-S3 auto-encryption. SSE-S3 auto-encryption, if enabled,
|
||||||
|
// requires a valid KMS configuration and turns any non-SSE-C
|
||||||
|
// request into an SSE-S3 request.
|
||||||
|
// If present EnvAutoEncryption must be either "on" or "off".
|
||||||
|
EnvAutoEncryption = "MINIO_SSE_AUTO_ENCRYPTION"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EnvVaultEndpoint is the environment variable used to specify
|
||||||
|
// the vault HTTPS endpoint.
|
||||||
|
EnvVaultEndpoint = "MINIO_SSE_VAULT_ENDPOINT"
|
||||||
|
|
||||||
|
// EnvVaultAuthType is the environment variable used to specify
|
||||||
|
// the authentication type for vault.
|
||||||
|
EnvVaultAuthType = "MINIO_SSE_VAULT_AUTH_TYPE"
|
||||||
|
|
||||||
|
// EnvVaultAppRoleID is the environment variable used to specify
|
||||||
|
// the vault AppRole ID.
|
||||||
|
EnvVaultAppRoleID = "MINIO_SSE_VAULT_APPROLE_ID"
|
||||||
|
|
||||||
|
// EnvVaultAppSecretID is the environment variable used to specify
|
||||||
|
// the vault AppRole secret corresponding to the AppRole ID.
|
||||||
|
EnvVaultAppSecretID = "MINIO_SSE_VAULT_APPROLE_SECRET"
|
||||||
|
|
||||||
|
// EnvVaultKeyVersion is the environment variable used to specify
|
||||||
|
// the vault key version.
|
||||||
|
EnvVaultKeyVersion = "MINIO_SSE_VAULT_KEY_VERSION"
|
||||||
|
|
||||||
|
// EnvVaultKeyName is the environment variable used to specify
|
||||||
|
// the vault named key-ring. In the S3 context it's referred as
|
||||||
|
// customer master key ID (CMK-ID).
|
||||||
|
EnvVaultKeyName = "MINIO_SSE_VAULT_KEY_NAME"
|
||||||
|
|
||||||
|
// EnvVaultCAPath is the environment variable used to specify the
|
||||||
|
// path to a directory of PEM-encoded CA cert files. These CA cert
|
||||||
|
// files are used to authenticate Minio to Vault over mTLS.
|
||||||
|
EnvVaultCAPath = "MINIO_SSE_VAULT_CAPATH"
|
||||||
|
|
||||||
|
// EnvVaultNamespace is the environment variable used to specify
|
||||||
|
// vault namespace. The vault namespace is used if the enterprise
|
||||||
|
// version of Hashicorp Vault is used.
|
||||||
|
EnvVaultNamespace = "MINIO_SSE_VAULT_NAMESPACE"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Environment provides functions for accessing environment
|
||||||
|
// variables.
|
||||||
|
var Environment = environment{}
|
||||||
|
|
||||||
|
type environment struct{}
|
||||||
|
|
||||||
|
// Get retrieves the value of the environment variable named
|
||||||
|
// by the key. If the variable is present in the environment the
|
||||||
|
// value (which may be empty) is returned. Otherwise it returns
|
||||||
|
// the specified default value.
|
||||||
|
func (environment) Get(key, defaultValue string) string {
|
||||||
|
if v, ok := os.LookupEnv(key); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup retrieves the value of the environment variable named
|
||||||
|
// by the key. If the variable is present in the environment the
|
||||||
|
// value (which may be empty) is returned and the boolean is true.
|
||||||
|
// Otherwise the returned value will be empty and the boolean will
|
||||||
|
// be false.
|
||||||
|
func (environment) Lookup(key string) (string, bool) { return os.LookupEnv(key) }
|
||||||
|
|
||||||
|
// LookupKMSConfig extracts the KMS configuration provided by environment
|
||||||
|
// variables and merge them with the provided KMS configuration. The
|
||||||
|
// merging follows the following rules:
|
||||||
|
//
|
||||||
|
// 1. A valid value provided as environment variable is higher prioritized
|
||||||
|
// than the provided configuration and overwrites the value from the
|
||||||
|
// configuration file.
|
||||||
|
//
|
||||||
|
// 2. A value specified as environment variable never changes the configuration
|
||||||
|
// file. So it is never made a persistent setting.
|
||||||
|
//
|
||||||
|
// It sets the global KMS configuration according to the merged configuration
|
||||||
|
// on success.
|
||||||
|
func (env environment) LookupKMSConfig(config crypto.KMSConfig) (err error) {
|
||||||
|
// Lookup Hashicorp-Vault configuration & overwrite config entry if ENV var is present
|
||||||
|
config.Vault.Endpoint = env.Get(EnvVaultEndpoint, config.Vault.Endpoint)
|
||||||
|
config.Vault.CAPath = env.Get(EnvVaultCAPath, config.Vault.CAPath)
|
||||||
|
config.Vault.Auth.Type = env.Get(EnvVaultAuthType, config.Vault.Auth.Type)
|
||||||
|
config.Vault.Auth.AppRole.ID = env.Get(EnvVaultAppRoleID, config.Vault.Auth.AppRole.ID)
|
||||||
|
config.Vault.Auth.AppRole.Secret = env.Get(EnvVaultAppSecretID, config.Vault.Auth.AppRole.Secret)
|
||||||
|
config.Vault.Key.Name = env.Get(EnvVaultKeyName, config.Vault.Key.Name)
|
||||||
|
config.Vault.Namespace = env.Get(EnvVaultNamespace, config.Vault.Namespace)
|
||||||
|
keyVersion := env.Get(EnvVaultKeyVersion, strconv.Itoa(config.Vault.Key.Version))
|
||||||
|
config.Vault.Key.Version, err = strconv.Atoi(keyVersion)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Invalid ENV variable: Unable to parse %s value (`%s`)", EnvVaultKeyVersion, keyVersion)
|
||||||
|
}
|
||||||
|
if err = config.Vault.Verify(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup KMS master keys - only available through ENV.
|
||||||
|
if masterKey, ok := env.Lookup(EnvKMSMasterKey); ok {
|
||||||
|
if !config.Vault.IsEmpty() { // Vault and KMS master key provided
|
||||||
|
return errors.New("Ambiguous KMS configuration: vault configuration and a master key are provided at the same time")
|
||||||
|
}
|
||||||
|
globalKMSKeyID, globalKMS, err = parseKMSMasterKey(masterKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !config.Vault.IsEmpty() {
|
||||||
|
globalKMS, err = crypto.NewVault(config.Vault)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
globalKMSKeyID = config.Vault.Key.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
autoEncryption, err := ParseBoolFlag(env.Get(EnvAutoEncryption, "off"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
globalAutoEncryption = bool(autoEncryption)
|
||||||
|
if globalAutoEncryption && globalKMS == nil { // auto-encryption enabled but no KMS
|
||||||
|
return errors.New("Invalid KMS configuration: auto-encryption is enabled but no valid KMS configuration is present")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseKMSMasterKey parses the value of the environment variable
|
||||||
|
// `EnvKMSMasterKey` and returns a key-ID and a master-key KMS on success.
|
||||||
|
func parseKMSMasterKey(envArg string) (string, crypto.KMS, error) {
|
||||||
|
values := strings.SplitN(envArg, ":", 2)
|
||||||
|
if len(values) != 2 {
|
||||||
|
return "", nil, fmt.Errorf("Invalid KMS master key: %s does not contain a ':'", envArg)
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
keyID = values[0]
|
||||||
|
hexKey = values[1]
|
||||||
|
)
|
||||||
|
if len(hexKey) != 64 { // 2 hex bytes = 1 byte
|
||||||
|
return "", nil, fmt.Errorf("Invalid KMS master key: %s not a 32 bytes long HEX value", hexKey)
|
||||||
|
}
|
||||||
|
var masterKey [32]byte
|
||||||
|
if _, err := hex.Decode(masterKey[:], []byte(hexKey)); err != nil {
|
||||||
|
return "", nil, fmt.Errorf("Invalid KMS master key: %s not a 32 bytes long HEX value", hexKey)
|
||||||
|
}
|
||||||
|
return keyID, crypto.NewKMS(masterKey), nil
|
||||||
|
}
|
||||||
@@ -127,8 +127,8 @@ func (p *parallelReader) Read() ([][]byte, error) {
|
|||||||
return nil, errXLReadQuorum
|
return nil, errXLReadQuorum
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFile reads from readers, reconstructs data if needed and writes the data to the writer.
|
// Decode reads from readers, reconstructs data if needed and writes the data to the writer.
|
||||||
func (s ErasureStorage) ReadFile(ctx context.Context, writer io.Writer, readers []*bitrotReader, offset, length, totalLength int64) error {
|
func (e Erasure) Decode(ctx context.Context, writer io.Writer, readers []*bitrotReader, offset, length, totalLength int64) error {
|
||||||
if offset < 0 || length < 0 {
|
if offset < 0 || length < 0 {
|
||||||
logger.LogIf(ctx, errInvalidArgument)
|
logger.LogIf(ctx, errInvalidArgument)
|
||||||
return errInvalidArgument
|
return errInvalidArgument
|
||||||
@@ -141,27 +141,27 @@ func (s ErasureStorage) ReadFile(ctx context.Context, writer io.Writer, readers
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := newParallelReader(readers, s.dataBlocks, offset, totalLength, s.blockSize)
|
reader := newParallelReader(readers, e.dataBlocks, offset, totalLength, e.blockSize)
|
||||||
|
|
||||||
startBlock := offset / s.blockSize
|
startBlock := offset / e.blockSize
|
||||||
endBlock := (offset + length) / s.blockSize
|
endBlock := (offset + length) / e.blockSize
|
||||||
|
|
||||||
var bytesWritten int64
|
var bytesWritten int64
|
||||||
for block := startBlock; block <= endBlock; block++ {
|
for block := startBlock; block <= endBlock; block++ {
|
||||||
var blockOffset, blockLength int64
|
var blockOffset, blockLength int64
|
||||||
switch {
|
switch {
|
||||||
case startBlock == endBlock:
|
case startBlock == endBlock:
|
||||||
blockOffset = offset % s.blockSize
|
blockOffset = offset % e.blockSize
|
||||||
blockLength = length
|
blockLength = length
|
||||||
case block == startBlock:
|
case block == startBlock:
|
||||||
blockOffset = offset % s.blockSize
|
blockOffset = offset % e.blockSize
|
||||||
blockLength = s.blockSize - blockOffset
|
blockLength = e.blockSize - blockOffset
|
||||||
case block == endBlock:
|
case block == endBlock:
|
||||||
blockOffset = 0
|
blockOffset = 0
|
||||||
blockLength = (offset + length) % s.blockSize
|
blockLength = (offset + length) % e.blockSize
|
||||||
default:
|
default:
|
||||||
blockOffset = 0
|
blockOffset = 0
|
||||||
blockLength = s.blockSize
|
blockLength = e.blockSize
|
||||||
}
|
}
|
||||||
if blockLength == 0 {
|
if blockLength == 0 {
|
||||||
break
|
break
|
||||||
@@ -170,11 +170,11 @@ func (s ErasureStorage) ReadFile(ctx context.Context, writer io.Writer, readers
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = s.ErasureDecodeDataBlocks(bufs); err != nil {
|
if err = e.DecodeDataBlocks(bufs); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
n, err := writeDataBlocks(ctx, writer, bufs, s.dataBlocks, blockOffset, blockLength)
|
n, err := writeDataBlocks(ctx, writer, bufs, e.dataBlocks, blockOffset, blockLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ func (d badDisk) ReadFile(volume string, path string, offset int64, buf []byte,
|
|||||||
return 0, errFaultyDisk
|
return 0, errFaultyDisk
|
||||||
}
|
}
|
||||||
|
|
||||||
var erasureReadFileTests = []struct {
|
var erasureDecodeTests = []struct {
|
||||||
dataBlocks int
|
dataBlocks int
|
||||||
onDisks, offDisks int
|
onDisks, offDisks int
|
||||||
blocksize, data int64
|
blocksize, data int64
|
||||||
@@ -81,13 +81,13 @@ var erasureReadFileTests = []struct {
|
|||||||
{dataBlocks: 8, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 37
|
{dataBlocks: 8, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 37
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErasureReadFile(t *testing.T) {
|
func TestErasureDecode(t *testing.T) {
|
||||||
for i, test := range erasureReadFileTests {
|
for i, test := range erasureDecodeTests {
|
||||||
setup, err := newErasureTestSetup(test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
setup, err := newErasureTestSetup(test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
||||||
}
|
}
|
||||||
storage, err := NewErasureStorage(context.Background(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
erasure, err := NewErasure(context.Background(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
setup.Remove()
|
setup.Remove()
|
||||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||||
@@ -108,7 +108,7 @@ func TestErasureReadFile(t *testing.T) {
|
|||||||
for i, disk := range disks {
|
for i, disk := range disks {
|
||||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", writeAlgorithm)
|
writers[i] = newBitrotWriter(disk, "testbucket", "object", writeAlgorithm)
|
||||||
}
|
}
|
||||||
n, err := storage.CreateFile(context.Background(), bytes.NewReader(data[:]), writers, buffer, storage.dataBlocks+1)
|
n, err := erasure.Encode(context.Background(), bytes.NewReader(data[:]), writers, buffer, erasure.dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
setup.Remove()
|
setup.Remove()
|
||||||
t.Fatalf("Test %d: failed to create erasure test file: %v", i, err)
|
t.Fatalf("Test %d: failed to create erasure test file: %v", i, err)
|
||||||
@@ -129,12 +129,12 @@ func TestErasureReadFile(t *testing.T) {
|
|||||||
if disk == OfflineDisk {
|
if disk == OfflineDisk {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
endOffset := getErasureShardFileEndOffset(test.offset, test.length, test.data, test.blocksize, storage.dataBlocks)
|
endOffset := getErasureShardFileEndOffset(test.offset, test.length, test.data, test.blocksize, erasure.dataBlocks)
|
||||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", writeAlgorithm, endOffset, writers[index].Sum())
|
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", writeAlgorithm, endOffset, writers[index].Sum())
|
||||||
}
|
}
|
||||||
|
|
||||||
writer := bytes.NewBuffer(nil)
|
writer := bytes.NewBuffer(nil)
|
||||||
err = storage.ReadFile(context.Background(), writer, bitrotReaders, test.offset, test.length, test.data)
|
err = erasure.Decode(context.Background(), writer, bitrotReaders, test.offset, test.length, test.data)
|
||||||
if err != nil && !test.shouldFail {
|
if err != nil && !test.shouldFail {
|
||||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ func TestErasureReadFile(t *testing.T) {
|
|||||||
if disk == OfflineDisk {
|
if disk == OfflineDisk {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
endOffset := getErasureShardFileEndOffset(test.offset, test.length, test.data, test.blocksize, storage.dataBlocks)
|
endOffset := getErasureShardFileEndOffset(test.offset, test.length, test.data, test.blocksize, erasure.dataBlocks)
|
||||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", writeAlgorithm, endOffset, writers[index].Sum())
|
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", writeAlgorithm, endOffset, writers[index].Sum())
|
||||||
}
|
}
|
||||||
for j := range disks[:test.offDisks] {
|
for j := range disks[:test.offDisks] {
|
||||||
@@ -167,7 +167,7 @@ func TestErasureReadFile(t *testing.T) {
|
|||||||
bitrotReaders[0] = nil
|
bitrotReaders[0] = nil
|
||||||
}
|
}
|
||||||
writer.Reset()
|
writer.Reset()
|
||||||
err = storage.ReadFile(context.Background(), writer, bitrotReaders, test.offset, test.length, test.data)
|
err = erasure.Decode(context.Background(), writer, bitrotReaders, test.offset, test.length, test.data)
|
||||||
if err != nil && !test.shouldFailQuorum {
|
if err != nil && !test.shouldFailQuorum {
|
||||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -184,10 +184,10 @@ func TestErasureReadFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test erasureReadFile with random offset and lengths.
|
// Test erasureDecode with random offset and lengths.
|
||||||
// This test is t.Skip()ed as it a long time to run, hence should be run
|
// This test is t.Skip()ed as it a long time to run, hence should be run
|
||||||
// explicitly after commenting out t.Skip()
|
// explicitly after commenting out t.Skip()
|
||||||
func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
func TestErasureDecodeRandomOffsetLength(t *testing.T) {
|
||||||
// Comment the following line to run this test.
|
// Comment the following line to run this test.
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
// Initialize environment needed for the test.
|
// Initialize environment needed for the test.
|
||||||
@@ -201,7 +201,7 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer setup.Remove()
|
defer setup.Remove()
|
||||||
disks := setup.disks
|
disks := setup.disks
|
||||||
storage, err := NewErasureStorage(context.Background(), dataBlocks, parityBlocks, blockSize)
|
erasure, err := NewErasure(context.Background(), dataBlocks, parityBlocks, blockSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create ErasureStorage: %v", err)
|
t.Fatalf("failed to create ErasureStorage: %v", err)
|
||||||
}
|
}
|
||||||
@@ -226,7 +226,7 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
|||||||
|
|
||||||
// Create a test file to read from.
|
// Create a test file to read from.
|
||||||
buffer := make([]byte, blockSize, 2*blockSize)
|
buffer := make([]byte, blockSize, 2*blockSize)
|
||||||
n, err := storage.CreateFile(context.Background(), bytes.NewReader(data), writers, buffer, storage.dataBlocks+1)
|
n, err := erasure.Encode(context.Background(), bytes.NewReader(data), writers, buffer, erasure.dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
|||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
// Verify erasureReadFile() for random offsets and lengths.
|
// Verify erasure.Decode() for random offsets and lengths.
|
||||||
for i := 0; i < iterations; i++ {
|
for i := 0; i < iterations; i++ {
|
||||||
offset := r.Int63n(length)
|
offset := r.Int63n(length)
|
||||||
readLen := r.Int63n(length - offset)
|
readLen := r.Int63n(length - offset)
|
||||||
@@ -252,10 +252,10 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
|||||||
if disk == OfflineDisk {
|
if disk == OfflineDisk {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
endOffset := getErasureShardFileEndOffset(offset, readLen, length, blockSize, storage.dataBlocks)
|
endOffset := getErasureShardFileEndOffset(offset, readLen, length, blockSize, erasure.dataBlocks)
|
||||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", DefaultBitrotAlgorithm, endOffset, writers[index].Sum())
|
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", DefaultBitrotAlgorithm, endOffset, writers[index].Sum())
|
||||||
}
|
}
|
||||||
err = storage.ReadFile(context.Background(), buf, bitrotReaders, offset, readLen, length)
|
err = erasure.Decode(context.Background(), buf, bitrotReaders, offset, readLen, length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err, offset, readLen)
|
t.Fatal(err, offset, readLen)
|
||||||
}
|
}
|
||||||
@@ -269,14 +269,14 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
|||||||
|
|
||||||
// Benchmarks
|
// Benchmarks
|
||||||
|
|
||||||
func benchmarkErasureRead(data, parity, dataDown, parityDown int, size int64, b *testing.B) {
|
func benchmarkErasureDecode(data, parity, dataDown, parityDown int, size int64, b *testing.B) {
|
||||||
setup, err := newErasureTestSetup(data, parity, blockSizeV1)
|
setup, err := newErasureTestSetup(data, parity, blockSizeV1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("failed to create test setup: %v", err)
|
b.Fatalf("failed to create test setup: %v", err)
|
||||||
}
|
}
|
||||||
defer setup.Remove()
|
defer setup.Remove()
|
||||||
disks := setup.disks
|
disks := setup.disks
|
||||||
storage, err := NewErasureStorage(context.Background(), data, parity, blockSizeV1)
|
erasure, err := NewErasure(context.Background(), data, parity, blockSizeV1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("failed to create ErasureStorage: %v", err)
|
b.Fatalf("failed to create ErasureStorage: %v", err)
|
||||||
}
|
}
|
||||||
@@ -291,7 +291,7 @@ func benchmarkErasureRead(data, parity, dataDown, parityDown int, size int64, b
|
|||||||
|
|
||||||
content := make([]byte, size)
|
content := make([]byte, size)
|
||||||
buffer := make([]byte, blockSizeV1, 2*blockSizeV1)
|
buffer := make([]byte, blockSizeV1, 2*blockSizeV1)
|
||||||
_, err = storage.CreateFile(context.Background(), bytes.NewReader(content), writers, buffer, storage.dataBlocks+1)
|
_, err = erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("failed to create erasure test file: %v", err)
|
b.Fatalf("failed to create erasure test file: %v", err)
|
||||||
}
|
}
|
||||||
@@ -312,62 +312,62 @@ func benchmarkErasureRead(data, parity, dataDown, parityDown int, size int64, b
|
|||||||
if writers[index] == nil {
|
if writers[index] == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
endOffset := getErasureShardFileEndOffset(0, size, size, storage.blockSize, storage.dataBlocks)
|
endOffset := getErasureShardFileEndOffset(0, size, size, erasure.blockSize, erasure.dataBlocks)
|
||||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", DefaultBitrotAlgorithm, endOffset, writers[index].Sum())
|
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", DefaultBitrotAlgorithm, endOffset, writers[index].Sum())
|
||||||
}
|
}
|
||||||
if err = storage.ReadFile(context.Background(), bytes.NewBuffer(content[:0]), bitrotReaders, 0, size, size); err != nil {
|
if err = erasure.Decode(context.Background(), bytes.NewBuffer(content[:0]), bitrotReaders, 0, size, size); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkErasureReadQuick(b *testing.B) {
|
func BenchmarkErasureDecodeQuick(b *testing.B) {
|
||||||
const size = 12 * 1024 * 1024
|
const size = 12 * 1024 * 1024
|
||||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 0, size, b) })
|
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 0, 0, size, b) })
|
||||||
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 1, size, b) })
|
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 0, 1, size, b) })
|
||||||
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 1, 0, size, b) })
|
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 1, 0, size, b) })
|
||||||
b.Run(" X0|X0 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 1, 1, size, b) })
|
b.Run(" X0|X0 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 1, 1, size, b) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkErasureRead_4_64KB(b *testing.B) {
|
func BenchmarkErasureDecode_4_64KB(b *testing.B) {
|
||||||
const size = 64 * 1024
|
const size = 64 * 1024
|
||||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 0, size, b) })
|
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 0, 0, size, b) })
|
||||||
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 1, size, b) })
|
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 0, 1, size, b) })
|
||||||
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 1, 0, size, b) })
|
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 1, 0, size, b) })
|
||||||
b.Run(" X0|X0 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 1, 1, size, b) })
|
b.Run(" X0|X0 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 1, 1, size, b) })
|
||||||
b.Run(" 00|XX ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 2, size, b) })
|
b.Run(" 00|XX ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 0, 2, size, b) })
|
||||||
b.Run(" XX|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 2, 0, size, b) })
|
b.Run(" XX|00 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 2, 0, size, b) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkErasureRead_8_20MB(b *testing.B) {
|
func BenchmarkErasureDecode_8_20MB(b *testing.B) {
|
||||||
const size = 20 * 1024 * 1024
|
const size = 20 * 1024 * 1024
|
||||||
b.Run(" 0000|0000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 0, 0, size, b) })
|
b.Run(" 0000|0000 ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 0, 0, size, b) })
|
||||||
b.Run(" 0000|X000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 0, 1, size, b) })
|
b.Run(" 0000|X000 ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 0, 1, size, b) })
|
||||||
b.Run(" X000|0000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 1, 0, size, b) })
|
b.Run(" X000|0000 ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 1, 0, size, b) })
|
||||||
b.Run(" X000|X000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 1, 1, size, b) })
|
b.Run(" X000|X000 ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 1, 1, size, b) })
|
||||||
b.Run(" 0000|XXXX ", func(b *testing.B) { benchmarkErasureRead(4, 4, 0, 4, size, b) })
|
b.Run(" 0000|XXXX ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 0, 4, size, b) })
|
||||||
b.Run(" XX00|XX00 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 2, 2, size, b) })
|
b.Run(" XX00|XX00 ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 2, 2, size, b) })
|
||||||
b.Run(" XXXX|0000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 4, 0, size, b) })
|
b.Run(" XXXX|0000 ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 4, 0, size, b) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkErasureRead_12_30MB(b *testing.B) {
|
func BenchmarkErasureDecode_12_30MB(b *testing.B) {
|
||||||
const size = 30 * 1024 * 1024
|
const size = 30 * 1024 * 1024
|
||||||
b.Run(" 000000|000000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 0, 0, size, b) })
|
b.Run(" 000000|000000 ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 0, 0, size, b) })
|
||||||
b.Run(" 000000|X00000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 0, 1, size, b) })
|
b.Run(" 000000|X00000 ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 0, 1, size, b) })
|
||||||
b.Run(" X00000|000000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 1, 0, size, b) })
|
b.Run(" X00000|000000 ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 1, 0, size, b) })
|
||||||
b.Run(" X00000|X00000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 1, 1, size, b) })
|
b.Run(" X00000|X00000 ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 1, 1, size, b) })
|
||||||
b.Run(" 000000|XXXXXX ", func(b *testing.B) { benchmarkErasureRead(6, 6, 0, 6, size, b) })
|
b.Run(" 000000|XXXXXX ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 0, 6, size, b) })
|
||||||
b.Run(" XXX000|XXX000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 3, 3, size, b) })
|
b.Run(" XXX000|XXX000 ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 3, 3, size, b) })
|
||||||
b.Run(" XXXXXX|000000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 6, 0, size, b) })
|
b.Run(" XXXXXX|000000 ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 6, 0, size, b) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkErasureRead_16_40MB(b *testing.B) {
|
func BenchmarkErasureDecode_16_40MB(b *testing.B) {
|
||||||
const size = 40 * 1024 * 1024
|
const size = 40 * 1024 * 1024
|
||||||
b.Run(" 00000000|00000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 0, 0, size, b) })
|
b.Run(" 00000000|00000000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 0, 0, size, b) })
|
||||||
b.Run(" 00000000|X0000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 0, 1, size, b) })
|
b.Run(" 00000000|X0000000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 0, 1, size, b) })
|
||||||
b.Run(" X0000000|00000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 1, 0, size, b) })
|
b.Run(" X0000000|00000000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 1, 0, size, b) })
|
||||||
b.Run(" X0000000|X0000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 1, 1, size, b) })
|
b.Run(" X0000000|X0000000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 1, 1, size, b) })
|
||||||
b.Run(" 00000000|XXXXXXXX ", func(b *testing.B) { benchmarkErasureRead(8, 8, 0, 8, size, b) })
|
b.Run(" 00000000|XXXXXXXX ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 0, 8, size, b) })
|
||||||
b.Run(" XXXX0000|XXXX0000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 4, 4, size, b) })
|
b.Run(" XXXX0000|XXXX0000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 4, 4, size, b) })
|
||||||
b.Run(" XXXXXXXX|00000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 8, 0, size, b) })
|
b.Run(" XXXXXXXX|00000000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 8, 0, size, b) })
|
||||||
}
|
}
|
||||||
@@ -69,8 +69,8 @@ func (p *parallelWriter) Append(ctx context.Context, blocks [][]byte) error {
|
|||||||
return reduceWriteQuorumErrs(ctx, p.errs, objectOpIgnoredErrs, p.writeQuorum)
|
return reduceWriteQuorumErrs(ctx, p.errs, objectOpIgnoredErrs, p.writeQuorum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateFile reads from the reader, erasure-encodes the data and writes to the writers.
|
// Encode reads from the reader, erasure-encodes the data and writes to the writers.
|
||||||
func (s *ErasureStorage) CreateFile(ctx context.Context, src io.Reader, writers []*bitrotWriter, buf []byte, quorum int) (total int64, err error) {
|
func (e *Erasure) Encode(ctx context.Context, src io.Reader, writers []*bitrotWriter, buf []byte, quorum int) (total int64, err error) {
|
||||||
writer := ¶llelWriter{
|
writer := ¶llelWriter{
|
||||||
writers: writers,
|
writers: writers,
|
||||||
writeQuorum: quorum,
|
writeQuorum: quorum,
|
||||||
@@ -90,7 +90,7 @@ func (s *ErasureStorage) CreateFile(ctx context.Context, src io.Reader, writers
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
// We take care of the situation where if n == 0 and total == 0 by creating empty data and parity files.
|
// We take care of the situation where if n == 0 and total == 0 by creating empty data and parity files.
|
||||||
blocks, err = s.ErasureEncode(ctx, buf[:n])
|
blocks, err = e.EncodeData(ctx, buf[:n])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -28,13 +28,17 @@ import (
|
|||||||
|
|
||||||
type badDisk struct{ StorageAPI }
|
type badDisk struct{ StorageAPI }
|
||||||
|
|
||||||
|
func (a badDisk) String() string {
|
||||||
|
return "bad-disk"
|
||||||
|
}
|
||||||
|
|
||||||
func (a badDisk) AppendFile(volume string, path string, buf []byte) error {
|
func (a badDisk) AppendFile(volume string, path string, buf []byte) error {
|
||||||
return errFaultyDisk
|
return errFaultyDisk
|
||||||
}
|
}
|
||||||
|
|
||||||
const oneMiByte = 1 * humanize.MiByte
|
const oneMiByte = 1 * humanize.MiByte
|
||||||
|
|
||||||
var erasureCreateFileTests = []struct {
|
var erasureEncodeTests = []struct {
|
||||||
dataBlocks int
|
dataBlocks int
|
||||||
onDisks, offDisks int
|
onDisks, offDisks int
|
||||||
blocksize, data int64
|
blocksize, data int64
|
||||||
@@ -64,14 +68,14 @@ var erasureCreateFileTests = []struct {
|
|||||||
{dataBlocks: 10, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 19
|
{dataBlocks: 10, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 19
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErasureCreateFile(t *testing.T) {
|
func TestErasureEncode(t *testing.T) {
|
||||||
for i, test := range erasureCreateFileTests {
|
for i, test := range erasureEncodeTests {
|
||||||
setup, err := newErasureTestSetup(test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
setup, err := newErasureTestSetup(test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
||||||
}
|
}
|
||||||
disks := setup.disks
|
disks := setup.disks
|
||||||
storage, err := NewErasureStorage(context.Background(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
erasure, err := NewErasure(context.Background(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
setup.Remove()
|
setup.Remove()
|
||||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||||
@@ -90,7 +94,7 @@ func TestErasureCreateFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", test.algorithm)
|
writers[i] = newBitrotWriter(disk, "testbucket", "object", test.algorithm)
|
||||||
}
|
}
|
||||||
n, err := storage.CreateFile(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, storage.dataBlocks+1)
|
n, err := erasure.Encode(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, erasure.dataBlocks+1)
|
||||||
if err != nil && !test.shouldFail {
|
if err != nil && !test.shouldFail {
|
||||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -119,7 +123,7 @@ func TestErasureCreateFile(t *testing.T) {
|
|||||||
if test.offDisks > 0 {
|
if test.offDisks > 0 {
|
||||||
writers[0] = nil
|
writers[0] = nil
|
||||||
}
|
}
|
||||||
n, err = storage.CreateFile(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, storage.dataBlocks+1)
|
n, err = erasure.Encode(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, erasure.dataBlocks+1)
|
||||||
if err != nil && !test.shouldFailQuorum {
|
if err != nil && !test.shouldFailQuorum {
|
||||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -138,13 +142,13 @@ func TestErasureCreateFile(t *testing.T) {
|
|||||||
|
|
||||||
// Benchmarks
|
// Benchmarks
|
||||||
|
|
||||||
func benchmarkErasureWrite(data, parity, dataDown, parityDown int, size int64, b *testing.B) {
|
func benchmarkErasureEncode(data, parity, dataDown, parityDown int, size int64, b *testing.B) {
|
||||||
setup, err := newErasureTestSetup(data, parity, blockSizeV1)
|
setup, err := newErasureTestSetup(data, parity, blockSizeV1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("failed to create test setup: %v", err)
|
b.Fatalf("failed to create test setup: %v", err)
|
||||||
}
|
}
|
||||||
defer setup.Remove()
|
defer setup.Remove()
|
||||||
storage, err := NewErasureStorage(context.Background(), data, parity, blockSizeV1)
|
erasure, err := NewErasure(context.Background(), data, parity, blockSizeV1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("failed to create ErasureStorage: %v", err)
|
b.Fatalf("failed to create ErasureStorage: %v", err)
|
||||||
}
|
}
|
||||||
@@ -170,50 +174,50 @@ func benchmarkErasureWrite(data, parity, dataDown, parityDown int, size int64, b
|
|||||||
}
|
}
|
||||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
|
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
|
||||||
}
|
}
|
||||||
_, err := storage.CreateFile(context.Background(), bytes.NewReader(content), writers, buffer, storage.dataBlocks+1)
|
_, err := erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkErasureWriteQuick(b *testing.B) {
|
func BenchmarkErasureEncodeQuick(b *testing.B) {
|
||||||
const size = 12 * 1024 * 1024
|
const size = 12 * 1024 * 1024
|
||||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 0, 0, size, b) })
|
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 0, size, b) })
|
||||||
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 0, 1, size, b) })
|
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 1, size, b) })
|
||||||
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 1, 0, size, b) })
|
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 1, 0, size, b) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkErasureWrite_4_64KB(b *testing.B) {
|
func BenchmarkErasureEncode_4_64KB(b *testing.B) {
|
||||||
const size = 64 * 1024
|
const size = 64 * 1024
|
||||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 0, 0, size, b) })
|
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 0, size, b) })
|
||||||
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 0, 1, size, b) })
|
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 1, size, b) })
|
||||||
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 1, 0, size, b) })
|
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 1, 0, size, b) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkErasureWrite_8_20MB(b *testing.B) {
|
func BenchmarkErasureEncode_8_20MB(b *testing.B) {
|
||||||
const size = 20 * 1024 * 1024
|
const size = 20 * 1024 * 1024
|
||||||
b.Run(" 0000|0000 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 0, 0, size, b) })
|
b.Run(" 0000|0000 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 0, 0, size, b) })
|
||||||
b.Run(" 0000|X000 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 0, 1, size, b) })
|
b.Run(" 0000|X000 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 0, 1, size, b) })
|
||||||
b.Run(" X000|0000 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 1, 0, size, b) })
|
b.Run(" X000|0000 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 1, 0, size, b) })
|
||||||
b.Run(" 0000|XXX0 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 0, 3, size, b) })
|
b.Run(" 0000|XXX0 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 0, 3, size, b) })
|
||||||
b.Run(" XXX0|0000 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 3, 0, size, b) })
|
b.Run(" XXX0|0000 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 3, 0, size, b) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkErasureWrite_12_30MB(b *testing.B) {
|
func BenchmarkErasureEncode_12_30MB(b *testing.B) {
|
||||||
const size = 30 * 1024 * 1024
|
const size = 30 * 1024 * 1024
|
||||||
b.Run(" 000000|000000 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 0, 0, size, b) })
|
b.Run(" 000000|000000 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 0, 0, size, b) })
|
||||||
b.Run(" 000000|X00000 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 0, 1, size, b) })
|
b.Run(" 000000|X00000 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 0, 1, size, b) })
|
||||||
b.Run(" X00000|000000 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 1, 0, size, b) })
|
b.Run(" X00000|000000 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 1, 0, size, b) })
|
||||||
b.Run(" 000000|XXXXX0 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 0, 5, size, b) })
|
b.Run(" 000000|XXXXX0 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 0, 5, size, b) })
|
||||||
b.Run(" XXXXX0|000000 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 5, 0, size, b) })
|
b.Run(" XXXXX0|000000 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 5, 0, size, b) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkErasureWrite_16_40MB(b *testing.B) {
|
func BenchmarkErasureEncode_16_40MB(b *testing.B) {
|
||||||
const size = 40 * 1024 * 1024
|
const size = 40 * 1024 * 1024
|
||||||
b.Run(" 00000000|00000000 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 0, 0, size, b) })
|
b.Run(" 00000000|00000000 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 0, 0, size, b) })
|
||||||
b.Run(" 00000000|X0000000 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 0, 1, size, b) })
|
b.Run(" 00000000|X0000000 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 0, 1, size, b) })
|
||||||
b.Run(" X0000000|00000000 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 1, 0, size, b) })
|
b.Run(" X0000000|00000000 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 1, 0, size, b) })
|
||||||
b.Run(" 00000000|XXXXXXX0 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 0, 7, size, b) })
|
b.Run(" 00000000|XXXXXXX0 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 0, 7, size, b) })
|
||||||
b.Run(" XXXXXXX0|00000000 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 7, 0, size, b) })
|
b.Run(" XXXXXXX0|00000000 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 7, 0, size, b) })
|
||||||
}
|
}
|
||||||
@@ -23,20 +23,20 @@ import (
|
|||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HealFile heals the shard files on non-nil writers. Note that the quorum passed is 1
|
// Heal heals the shard files on non-nil writers. Note that the quorum passed is 1
|
||||||
// as healing should continue even if it has been successful healing only one shard file.
|
// as healing should continue even if it has been successful healing only one shard file.
|
||||||
func (s ErasureStorage) HealFile(ctx context.Context, readers []*bitrotReader, writers []*bitrotWriter, size int64) error {
|
func (e Erasure) Heal(ctx context.Context, readers []*bitrotReader, writers []*bitrotWriter, size int64) error {
|
||||||
r, w := io.Pipe()
|
r, w := io.Pipe()
|
||||||
go func() {
|
go func() {
|
||||||
if err := s.ReadFile(ctx, w, readers, 0, size, size); err != nil {
|
if err := e.Decode(ctx, w, readers, 0, size, size); err != nil {
|
||||||
w.CloseWithError(err)
|
w.CloseWithError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Close()
|
w.Close()
|
||||||
}()
|
}()
|
||||||
buf := make([]byte, s.blockSize)
|
buf := make([]byte, e.blockSize)
|
||||||
// quorum is 1 because CreateFile should continue writing as long as we are writing to even 1 disk.
|
// quorum is 1 because CreateFile should continue writing as long as we are writing to even 1 disk.
|
||||||
n, err := s.CreateFile(ctx, r, writers, buf, 1)
|
n, err := e.Encode(ctx, r, writers, buf, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var erasureHealFileTests = []struct {
|
var erasureHealTests = []struct {
|
||||||
dataBlocks, disks int
|
dataBlocks, disks int
|
||||||
|
|
||||||
// number of offline disks is also number of staleDisks for
|
// number of offline disks is also number of staleDisks for
|
||||||
@@ -60,8 +60,8 @@ var erasureHealFileTests = []struct {
|
|||||||
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte * 64, algorithm: SHA256, shouldFail: false}, // 19
|
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte * 64, algorithm: SHA256, shouldFail: false}, // 19
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErasureHealFile(t *testing.T) {
|
func TestErasureHeal(t *testing.T) {
|
||||||
for i, test := range erasureHealFileTests {
|
for i, test := range erasureHealTests {
|
||||||
if test.offDisks < test.badStaleDisks {
|
if test.offDisks < test.badStaleDisks {
|
||||||
// test case sanity check
|
// test case sanity check
|
||||||
t.Fatalf("Test %d: Bad test case - number of stale disks cannot be less than number of badstale disks", i)
|
t.Fatalf("Test %d: Bad test case - number of stale disks cannot be less than number of badstale disks", i)
|
||||||
@@ -73,7 +73,7 @@ func TestErasureHealFile(t *testing.T) {
|
|||||||
t.Fatalf("Test %d: failed to setup XL environment: %v", i, err)
|
t.Fatalf("Test %d: failed to setup XL environment: %v", i, err)
|
||||||
}
|
}
|
||||||
disks := setup.disks
|
disks := setup.disks
|
||||||
storage, err := NewErasureStorage(context.Background(), test.dataBlocks, test.disks-test.dataBlocks, test.blocksize)
|
erasure, err := NewErasure(context.Background(), test.dataBlocks, test.disks-test.dataBlocks, test.blocksize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
setup.Remove()
|
setup.Remove()
|
||||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||||
@@ -88,7 +88,7 @@ func TestErasureHealFile(t *testing.T) {
|
|||||||
for i, disk := range disks {
|
for i, disk := range disks {
|
||||||
writers[i] = newBitrotWriter(disk, "testbucket", "testobject", test.algorithm)
|
writers[i] = newBitrotWriter(disk, "testbucket", "testobject", test.algorithm)
|
||||||
}
|
}
|
||||||
_, err = storage.CreateFile(context.Background(), bytes.NewReader(data), writers, buffer, storage.dataBlocks+1)
|
_, err = erasure.Encode(context.Background(), bytes.NewReader(data), writers, buffer, erasure.dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
setup.Remove()
|
setup.Remove()
|
||||||
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
||||||
@@ -96,7 +96,7 @@ func TestErasureHealFile(t *testing.T) {
|
|||||||
|
|
||||||
readers := make([]*bitrotReader, len(disks))
|
readers := make([]*bitrotReader, len(disks))
|
||||||
for i, disk := range disks {
|
for i, disk := range disks {
|
||||||
shardFilesize := getErasureShardFileSize(test.blocksize, test.size, storage.dataBlocks)
|
shardFilesize := getErasureShardFileSize(test.blocksize, test.size, erasure.dataBlocks)
|
||||||
readers[i] = newBitrotReader(disk, "testbucket", "testobject", test.algorithm, shardFilesize, writers[i].Sum())
|
readers[i] = newBitrotReader(disk, "testbucket", "testobject", test.algorithm, shardFilesize, writers[i].Sum())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ func TestErasureHealFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// test case setup is complete - now call Healfile()
|
// test case setup is complete - now call Healfile()
|
||||||
err = storage.HealFile(context.Background(), readers, staleWriters, test.size)
|
err = erasure.Heal(context.Background(), readers, staleWriters, test.size)
|
||||||
if err != nil && !test.shouldFail {
|
if err != nil && !test.shouldFail {
|
||||||
t.Errorf("Test %d: should pass but it failed with: %v", i, err)
|
t.Errorf("Test %d: should pass but it failed with: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,9 @@ func writeDataBlocks(ctx context.Context, dst io.Writer, enBlocks [][]byte, data
|
|||||||
if write < int64(len(block)) {
|
if write < int64(len(block)) {
|
||||||
n, err := io.Copy(dst, bytes.NewReader(block[:write]))
|
n, err := io.Copy(dst, bytes.NewReader(block[:write]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
if err != io.ErrClosedPipe {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
totalWritten += n
|
totalWritten += n
|
||||||
@@ -90,7 +92,10 @@ func writeDataBlocks(ctx context.Context, dst io.Writer, enBlocks [][]byte, data
|
|||||||
// Copy the block.
|
// Copy the block.
|
||||||
n, err := io.Copy(dst, bytes.NewReader(block))
|
n, err := io.Copy(dst, bytes.NewReader(block))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
// The writer will be closed incase of range queries, which will emit ErrClosedPipe.
|
||||||
|
if err != io.ErrClosedPipe {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-23
@@ -23,26 +23,23 @@ import (
|
|||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OfflineDisk represents an unavailable disk.
|
// Erasure - erasure encoding details.
|
||||||
var OfflineDisk StorageAPI // zero value is nil
|
type Erasure struct {
|
||||||
|
encoder reedsolomon.Encoder
|
||||||
// ErasureStorage - erasure encoding details.
|
|
||||||
type ErasureStorage struct {
|
|
||||||
erasure reedsolomon.Encoder
|
|
||||||
dataBlocks, parityBlocks int
|
dataBlocks, parityBlocks int
|
||||||
blockSize int64
|
blockSize int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewErasureStorage creates a new ErasureStorage.
|
// NewErasure creates a new ErasureStorage.
|
||||||
func NewErasureStorage(ctx context.Context, dataBlocks, parityBlocks int, blockSize int64) (s ErasureStorage, err error) {
|
func NewErasure(ctx context.Context, dataBlocks, parityBlocks int, blockSize int64) (e Erasure, err error) {
|
||||||
shardsize := int(ceilFrac(blockSize, int64(dataBlocks)))
|
shardsize := int(ceilFrac(blockSize, int64(dataBlocks)))
|
||||||
erasure, err := reedsolomon.New(dataBlocks, parityBlocks, reedsolomon.WithAutoGoroutines(shardsize))
|
erasure, err := reedsolomon.New(dataBlocks, parityBlocks, reedsolomon.WithAutoGoroutines(shardsize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return s, err
|
return e, err
|
||||||
}
|
}
|
||||||
s = ErasureStorage{
|
e = Erasure{
|
||||||
erasure: erasure,
|
encoder: erasure,
|
||||||
dataBlocks: dataBlocks,
|
dataBlocks: dataBlocks,
|
||||||
parityBlocks: parityBlocks,
|
parityBlocks: parityBlocks,
|
||||||
blockSize: blockSize,
|
blockSize: blockSize,
|
||||||
@@ -50,30 +47,30 @@ func NewErasureStorage(ctx context.Context, dataBlocks, parityBlocks int, blockS
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErasureEncode encodes the given data and returns the erasure-coded data.
|
// EncodeData encodes the given data and returns the erasure-coded data.
|
||||||
// It returns an error if the erasure coding failed.
|
// It returns an error if the erasure coding failed.
|
||||||
func (s *ErasureStorage) ErasureEncode(ctx context.Context, data []byte) ([][]byte, error) {
|
func (e *Erasure) EncodeData(ctx context.Context, data []byte) ([][]byte, error) {
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return make([][]byte, s.dataBlocks+s.parityBlocks), nil
|
return make([][]byte, e.dataBlocks+e.parityBlocks), nil
|
||||||
}
|
}
|
||||||
encoded, err := s.erasure.Split(data)
|
encoded, err := e.encoder.Split(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err = s.erasure.Encode(encoded); err != nil {
|
if err = e.encoder.Encode(encoded); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return encoded, nil
|
return encoded, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErasureDecodeDataBlocks decodes the given erasure-coded data.
|
// DecodeDataBlocks decodes the given erasure-coded data.
|
||||||
// It only decodes the data blocks but does not verify them.
|
// It only decodes the data blocks but does not verify them.
|
||||||
// It returns an error if the decoding failed.
|
// It returns an error if the decoding failed.
|
||||||
func (s *ErasureStorage) ErasureDecodeDataBlocks(data [][]byte) error {
|
func (e *Erasure) DecodeDataBlocks(data [][]byte) error {
|
||||||
needsReconstruction := false
|
needsReconstruction := false
|
||||||
for _, b := range data[:s.dataBlocks] {
|
for _, b := range data[:e.dataBlocks] {
|
||||||
if b == nil {
|
if b == nil {
|
||||||
needsReconstruction = true
|
needsReconstruction = true
|
||||||
break
|
break
|
||||||
@@ -82,16 +79,16 @@ func (s *ErasureStorage) ErasureDecodeDataBlocks(data [][]byte) error {
|
|||||||
if !needsReconstruction {
|
if !needsReconstruction {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := s.erasure.ReconstructData(data); err != nil {
|
if err := e.encoder.ReconstructData(data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErasureDecodeDataAndParityBlocks decodes the given erasure-coded data and verifies it.
|
// DecodeDataAndParityBlocks decodes the given erasure-coded data and verifies it.
|
||||||
// It returns an error if the decoding failed.
|
// It returns an error if the decoding failed.
|
||||||
func (s *ErasureStorage) ErasureDecodeDataAndParityBlocks(ctx context.Context, data [][]byte) error {
|
func (e *Erasure) DecodeDataAndParityBlocks(ctx context.Context, data [][]byte) error {
|
||||||
if err := s.erasure.Reconstruct(data); err != nil {
|
if err := e.encoder.Reconstruct(data); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -25,7 +25,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var erasureDecodeTests = []struct {
|
var erasureEncodeDecodeTests = []struct {
|
||||||
dataBlocks, parityBlocks int
|
dataBlocks, parityBlocks int
|
||||||
missingData, missingParity int
|
missingData, missingParity int
|
||||||
reconstructParity bool
|
reconstructParity bool
|
||||||
@@ -43,20 +43,20 @@ var erasureDecodeTests = []struct {
|
|||||||
{dataBlocks: 8, parityBlocks: 4, missingData: 2, missingParity: 2, reconstructParity: false, shouldFail: false},
|
{dataBlocks: 8, parityBlocks: 4, missingData: 2, missingParity: 2, reconstructParity: false, shouldFail: false},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErasureDecode(t *testing.T) {
|
func TestErasureEncodeDecode(t *testing.T) {
|
||||||
data := make([]byte, 256)
|
data := make([]byte, 256)
|
||||||
if _, err := io.ReadFull(rand.Reader, data); err != nil {
|
if _, err := io.ReadFull(rand.Reader, data); err != nil {
|
||||||
t.Fatalf("Failed to read random data: %v", err)
|
t.Fatalf("Failed to read random data: %v", err)
|
||||||
}
|
}
|
||||||
for i, test := range erasureDecodeTests {
|
for i, test := range erasureEncodeDecodeTests {
|
||||||
buffer := make([]byte, len(data), 2*len(data))
|
buffer := make([]byte, len(data), 2*len(data))
|
||||||
copy(buffer, data)
|
copy(buffer, data)
|
||||||
|
|
||||||
storage, err := NewErasureStorage(context.Background(), test.dataBlocks, test.parityBlocks, blockSizeV1)
|
erasure, err := NewErasure(context.Background(), test.dataBlocks, test.parityBlocks, blockSizeV1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: failed to create erasure storage: %v", i, err)
|
t.Fatalf("Test %d: failed to create erasure: %v", i, err)
|
||||||
}
|
}
|
||||||
encoded, err := storage.ErasureEncode(context.Background(), buffer)
|
encoded, err := erasure.EncodeData(context.Background(), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: failed to encode data: %v", i, err)
|
t.Fatalf("Test %d: failed to encode data: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -69,9 +69,9 @@ func TestErasureDecode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if test.reconstructParity {
|
if test.reconstructParity {
|
||||||
err = storage.ErasureDecodeDataAndParityBlocks(context.Background(), encoded)
|
err = erasure.DecodeDataAndParityBlocks(context.Background(), encoded)
|
||||||
} else {
|
} else {
|
||||||
err = storage.ErasureDecodeDataBlocks(encoded)
|
err = erasure.DecodeDataBlocks(encoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil && test.shouldFail {
|
if err == nil && test.shouldFail {
|
||||||
|
|||||||
+1
-2
@@ -24,7 +24,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
|
||||||
"github.com/minio/minio/pkg/lock"
|
"github.com/minio/minio/pkg/lock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -273,7 +272,7 @@ func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, er
|
|||||||
rlk.Close()
|
rlk.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
logger.SetDeploymentID(id)
|
globalDeploymentID = id
|
||||||
return rlk, nil
|
return rlk, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-3
@@ -379,6 +379,23 @@ func saveFormatXL(disk StorageAPI, format interface{}) error {
|
|||||||
return disk.RenameFile(minioMetaBucket, formatConfigFileTmp, minioMetaBucket, formatConfigFile)
|
return disk.RenameFile(minioMetaBucket, formatConfigFileTmp, minioMetaBucket, formatConfigFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ignoredHiddenDirectories = []string{
|
||||||
|
minioMetaBucket,
|
||||||
|
".snapshot",
|
||||||
|
"lost+found",
|
||||||
|
"$RECYCLE.BIN",
|
||||||
|
"System Volume Information",
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIgnoreHiddenDirectories(dir string) bool {
|
||||||
|
for _, ignDir := range ignoredHiddenDirectories {
|
||||||
|
if dir == ignDir {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// loadFormatXL - loads format.json from disk.
|
// loadFormatXL - loads format.json from disk.
|
||||||
func loadFormatXL(disk StorageAPI) (format *formatXLV3, err error) {
|
func loadFormatXL(disk StorageAPI) (format *formatXLV3, err error) {
|
||||||
buf, err := disk.ReadAll(minioMetaBucket, formatConfigFile)
|
buf, err := disk.ReadAll(minioMetaBucket, formatConfigFile)
|
||||||
@@ -391,9 +408,7 @@ func loadFormatXL(disk StorageAPI) (format *formatXLV3, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(vols) > 1 || (len(vols) == 1 &&
|
if len(vols) > 1 || (len(vols) == 1 && !isIgnoreHiddenDirectories(vols[0].Name)) {
|
||||||
vols[0].Name != minioMetaBucket &&
|
|
||||||
vols[0].Name != "lost+found") {
|
|
||||||
// 'format.json' not found, but we
|
// 'format.json' not found, but we
|
||||||
// found user data.
|
// found user data.
|
||||||
return nil, errCorruptedFormat
|
return nil, errCorruptedFormat
|
||||||
|
|||||||
+7
-10
@@ -24,7 +24,6 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
pathutil "path"
|
pathutil "path"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/lock"
|
"github.com/minio/minio/pkg/lock"
|
||||||
@@ -137,11 +136,7 @@ func (m fsMetaV1) ToObjectInfo(bucket, object string, fi os.FileInfo) ObjectInfo
|
|||||||
|
|
||||||
// Guess content-type from the extension if possible.
|
// Guess content-type from the extension if possible.
|
||||||
if m.Meta["content-type"] == "" {
|
if m.Meta["content-type"] == "" {
|
||||||
if objectExt := pathutil.Ext(object); objectExt != "" {
|
m.Meta["content-type"] = mimedb.TypeByExtension(pathutil.Ext(object))
|
||||||
if content, ok := mimedb.DB[strings.ToLower(strings.TrimPrefix(objectExt, "."))]; ok {
|
|
||||||
m.Meta["content-type"] = content.ContentType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasSuffix(object, slashSeparator) {
|
if hasSuffix(object, slashSeparator) {
|
||||||
@@ -223,11 +218,13 @@ func parseFSPartsArray(fsMetaBuf []byte) []objectPartInfo {
|
|||||||
name := gjson.Get(partJSON, "name").String()
|
name := gjson.Get(partJSON, "name").String()
|
||||||
etag := gjson.Get(partJSON, "etag").String()
|
etag := gjson.Get(partJSON, "etag").String()
|
||||||
size := gjson.Get(partJSON, "size").Int()
|
size := gjson.Get(partJSON, "size").Int()
|
||||||
|
actualSize := gjson.Get(partJSON, "actualSize").Int()
|
||||||
partsArray = append(partsArray, objectPartInfo{
|
partsArray = append(partsArray, objectPartInfo{
|
||||||
Number: int(number),
|
Number: int(number),
|
||||||
Name: name,
|
Name: name,
|
||||||
ETag: etag,
|
ETag: etag,
|
||||||
Size: size,
|
Size: size,
|
||||||
|
ActualSize: int64(actualSize),
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func TestReadFSMetadata(t *testing.T) {
|
|||||||
if err := obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil {
|
if err := obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil {
|
||||||
t.Fatal("Unexpected err: ", err)
|
t.Fatal("Unexpected err: ", err)
|
||||||
}
|
}
|
||||||
if _, err := obj.PutObject(context.Background(), bucketName, objectName, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil); err != nil {
|
if _, err := obj.PutObject(context.Background(), bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil, ObjectOptions{}); err != nil {
|
||||||
t.Fatal("Unexpected err: ", err)
|
t.Fatal("Unexpected err: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ func TestWriteFSMetadata(t *testing.T) {
|
|||||||
if err := obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil {
|
if err := obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil {
|
||||||
t.Fatal("Unexpected err: ", err)
|
t.Fatal("Unexpected err: ", err)
|
||||||
}
|
}
|
||||||
if _, err := obj.PutObject(context.Background(), bucketName, objectName, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil); err != nil {
|
if _, err := obj.PutObject(context.Background(), bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil, ObjectOptions{}); err != nil {
|
||||||
t.Fatal("Unexpected err: ", err)
|
t.Fatal("Unexpected err: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+106
-56
@@ -18,7 +18,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -31,8 +30,6 @@ import (
|
|||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
mioutil "github.com/minio/minio/pkg/ioutil"
|
mioutil "github.com/minio/minio/pkg/ioutil"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/hash"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns EXPORT/.minio.sys/multipart/SHA256/UPLOADID
|
// Returns EXPORT/.minio.sys/multipart/SHA256/UPLOADID
|
||||||
@@ -46,21 +43,25 @@ func (fs *FSObjects) getMultipartSHADir(bucket, object string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns partNumber.etag
|
// Returns partNumber.etag
|
||||||
func (fs *FSObjects) encodePartFile(partNumber int, etag string) string {
|
func (fs *FSObjects) encodePartFile(partNumber int, etag string, actualSize int64) string {
|
||||||
return fmt.Sprintf("%.5d.%s", partNumber, etag)
|
return fmt.Sprintf("%.5d.%s.%d", partNumber, etag, actualSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns partNumber and etag
|
// Returns partNumber and etag
|
||||||
func (fs *FSObjects) decodePartFile(name string) (partNumber int, etag string, err error) {
|
func (fs *FSObjects) decodePartFile(name string) (partNumber int, etag string, actualSize int64, err error) {
|
||||||
result := strings.Split(name, ".")
|
result := strings.Split(name, ".")
|
||||||
if len(result) != 2 {
|
if len(result) != 3 {
|
||||||
return 0, "", errUnexpected
|
return 0, "", 0, errUnexpected
|
||||||
}
|
}
|
||||||
partNumber, err = strconv.Atoi(result[0])
|
partNumber, err = strconv.Atoi(result[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", errUnexpected
|
return 0, "", 0, errUnexpected
|
||||||
}
|
}
|
||||||
return partNumber, result[1], nil
|
actualSize, err = strconv.ParseInt(result[2], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", 0, errUnexpected
|
||||||
|
}
|
||||||
|
return partNumber, result[1], actualSize, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appends parts to an appendFile sequentially.
|
// Appends parts to an appendFile sequentially.
|
||||||
@@ -95,7 +96,7 @@ func (fs *FSObjects) backgroundAppend(ctx context.Context, bucket, object, uploa
|
|||||||
if entry == fs.metaJSONFile {
|
if entry == fs.metaJSONFile {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
partNumber, etag, err := fs.decodePartFile(entry)
|
partNumber, etag, actualSize, err := fs.decodePartFile(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.GetReqInfo(ctx).AppendTags("entry", entry)
|
logger.GetReqInfo(ctx).AppendTags("entry", entry)
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
@@ -119,7 +120,7 @@ func (fs *FSObjects) backgroundAppend(ctx context.Context, bucket, object, uploa
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file.parts = append(file.parts, PartInfo{PartNumber: nextPartNumber, ETag: etag})
|
file.parts = append(file.parts, PartInfo{PartNumber: partNumber, ETag: etag, ActualSize: actualSize})
|
||||||
nextPartNumber++
|
nextPartNumber++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,7 +210,7 @@ func (fs *FSObjects) ListMultipartUploads(ctx context.Context, bucket, object, k
|
|||||||
// subsequent request each UUID is unique.
|
// subsequent request each UUID is unique.
|
||||||
//
|
//
|
||||||
// Implements S3 compatible initiate multipart API.
|
// Implements S3 compatible initiate multipart API.
|
||||||
func (fs *FSObjects) NewMultipartUpload(ctx context.Context, bucket, object string, meta map[string]string) (string, error) {
|
func (fs *FSObjects) NewMultipartUpload(ctx context.Context, bucket, object string, meta map[string]string, opts ObjectOptions) (string, error) {
|
||||||
if err := checkNewMultipartArgs(ctx, bucket, object, fs); err != nil {
|
if err := checkNewMultipartArgs(ctx, bucket, object, fs); err != nil {
|
||||||
return "", toObjectErr(err, bucket)
|
return "", toObjectErr(err, bucket)
|
||||||
}
|
}
|
||||||
@@ -249,29 +250,13 @@ func (fs *FSObjects) NewMultipartUpload(ctx context.Context, bucket, object stri
|
|||||||
// 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(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject, uploadID string, partID int,
|
func (fs *FSObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject, uploadID string, partID int,
|
||||||
startOffset int64, length int64, srcInfo ObjectInfo) (pi PartInfo, e error) {
|
startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (pi PartInfo, e error) {
|
||||||
|
|
||||||
if err := checkNewMultipartArgs(ctx, srcBucket, srcObject, fs); err != nil {
|
if err := checkNewMultipartArgs(ctx, srcBucket, srcObject, fs); err != nil {
|
||||||
return pi, toObjectErr(err)
|
return pi, toObjectErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize pipe.
|
partInfo, err := fs.PutObjectPart(ctx, dstBucket, dstObject, uploadID, partID, srcInfo.PutObjReader, dstOpts)
|
||||||
go func() {
|
|
||||||
if gerr := fs.GetObject(ctx, srcBucket, srcObject, startOffset, length, srcInfo.Writer, srcInfo.ETag); gerr != nil {
|
|
||||||
if gerr = srcInfo.Writer.Close(); gerr != nil {
|
|
||||||
logger.LogIf(ctx, gerr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Close writer explicitly signaling we wrote all data.
|
|
||||||
if gerr := srcInfo.Writer.Close(); gerr != nil {
|
|
||||||
logger.LogIf(ctx, gerr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
partInfo, err := fs.PutObjectPart(ctx, dstBucket, dstObject, uploadID, partID, srcInfo.Reader)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pi, toObjectErr(err, dstBucket, dstObject)
|
return pi, toObjectErr(err, dstBucket, dstObject)
|
||||||
}
|
}
|
||||||
@@ -283,7 +268,8 @@ func (fs *FSObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, d
|
|||||||
// 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(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader) (pi PartInfo, e error) {
|
func (fs *FSObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, r *PutObjReader, opts ObjectOptions) (pi PartInfo, e error) {
|
||||||
|
data := r.Reader
|
||||||
if err := checkPutObjectPartArgs(ctx, bucket, object, fs); err != nil {
|
if err := checkPutObjectPartArgs(ctx, bucket, object, fs); err != nil {
|
||||||
return pi, toObjectErr(err, bucket)
|
return pi, toObjectErr(err, bucket)
|
||||||
}
|
}
|
||||||
@@ -292,8 +278,8 @@ func (fs *FSObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
|||||||
return pi, toObjectErr(err, bucket)
|
return pi, toObjectErr(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate input data size and it can never be less than zero.
|
// Validate input data size and it can never be less than -1.
|
||||||
if data.Size() < 0 {
|
if data.Size() < -1 {
|
||||||
logger.LogIf(ctx, errInvalidArgument)
|
logger.LogIf(ctx, errInvalidArgument)
|
||||||
return pi, toObjectErr(errInvalidArgument)
|
return pi, toObjectErr(errInvalidArgument)
|
||||||
}
|
}
|
||||||
@@ -333,12 +319,13 @@ func (fs *FSObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
|||||||
// PutObjectPart succeeds then there would be nothing to
|
// PutObjectPart succeeds then there would be nothing to
|
||||||
// delete in which case we just ignore the error.
|
// delete in which case we just ignore the error.
|
||||||
defer fsRemoveFile(ctx, tmpPartPath)
|
defer fsRemoveFile(ctx, tmpPartPath)
|
||||||
|
etag := r.MD5CurrentHexString()
|
||||||
|
|
||||||
etag := hex.EncodeToString(data.MD5Current())
|
|
||||||
if etag == "" {
|
if etag == "" {
|
||||||
etag = GenETag()
|
etag = GenETag()
|
||||||
}
|
}
|
||||||
partPath := pathJoin(uploadIDDir, fs.encodePartFile(partID, etag))
|
|
||||||
|
partPath := pathJoin(uploadIDDir, fs.encodePartFile(partID, etag, data.ActualSize()))
|
||||||
|
|
||||||
if err = fsRenameFile(ctx, tmpPartPath, partPath); err != nil {
|
if err = fsRenameFile(ctx, tmpPartPath, partPath); err != nil {
|
||||||
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
||||||
@@ -355,6 +342,7 @@ func (fs *FSObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
|||||||
LastModified: fi.ModTime(),
|
LastModified: fi.ModTime(),
|
||||||
ETag: etag,
|
ETag: etag,
|
||||||
Size: fi.Size(),
|
Size: fi.Size(),
|
||||||
|
ActualSize: data.ActualSize(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,8 +369,7 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
|
|||||||
}
|
}
|
||||||
|
|
||||||
uploadIDDir := fs.getUploadIDDir(bucket, object, uploadID)
|
uploadIDDir := fs.getUploadIDDir(bucket, object, uploadID)
|
||||||
_, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile))
|
if _, err := fsStatFile(ctx, pathJoin(uploadIDDir, fs.metaJSONFile)); err != nil {
|
||||||
if err != nil {
|
|
||||||
if err == errFileNotFound || err == errFileAccessDenied {
|
if err == errFileNotFound || err == errFileAccessDenied {
|
||||||
return result, InvalidUploadID{UploadID: uploadID}
|
return result, InvalidUploadID{UploadID: uploadID}
|
||||||
}
|
}
|
||||||
@@ -400,7 +387,7 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
|
|||||||
if entry == fs.metaJSONFile {
|
if entry == fs.metaJSONFile {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
partNumber, etag1, derr := fs.decodePartFile(entry)
|
partNumber, etag1, _, derr := fs.decodePartFile(entry)
|
||||||
if derr != nil {
|
if derr != nil {
|
||||||
logger.LogIf(ctx, derr)
|
logger.LogIf(ctx, derr)
|
||||||
return result, toObjectErr(derr)
|
return result, toObjectErr(derr)
|
||||||
@@ -410,11 +397,11 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
|
|||||||
partsMap[partNumber] = etag1
|
partsMap[partNumber] = etag1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
stat1, serr := fsStatFile(ctx, pathJoin(uploadIDDir, fs.encodePartFile(partNumber, etag1)))
|
stat1, serr := fsStatFile(ctx, pathJoin(uploadIDDir, getPartFile(entries, partNumber, etag1)))
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
return result, toObjectErr(serr)
|
return result, toObjectErr(serr)
|
||||||
}
|
}
|
||||||
stat2, serr := fsStatFile(ctx, pathJoin(uploadIDDir, fs.encodePartFile(partNumber, etag2)))
|
stat2, serr := fsStatFile(ctx, pathJoin(uploadIDDir, getPartFile(entries, partNumber, etag2)))
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
return result, toObjectErr(serr)
|
return result, toObjectErr(serr)
|
||||||
}
|
}
|
||||||
@@ -424,8 +411,19 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
|
|||||||
}
|
}
|
||||||
|
|
||||||
var parts []PartInfo
|
var parts []PartInfo
|
||||||
|
var actualSize int64
|
||||||
for partNumber, etag := range partsMap {
|
for partNumber, etag := range partsMap {
|
||||||
parts = append(parts, PartInfo{PartNumber: partNumber, ETag: etag})
|
partFile := getPartFile(entries, partNumber, etag)
|
||||||
|
if partFile == "" {
|
||||||
|
return result, InvalidPart{}
|
||||||
|
}
|
||||||
|
// Read the actualSize from the pathFileName.
|
||||||
|
subParts := strings.Split(partFile, ".")
|
||||||
|
actualSize, err = strconv.ParseInt(subParts[len(subParts)-1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return result, InvalidPart{}
|
||||||
|
}
|
||||||
|
parts = append(parts, PartInfo{PartNumber: partNumber, ETag: etag, ActualSize: actualSize})
|
||||||
}
|
}
|
||||||
sort.Slice(parts, func(i int, j int) bool {
|
sort.Slice(parts, func(i int, j int) bool {
|
||||||
return parts[i].PartNumber < parts[j].PartNumber
|
return parts[i].PartNumber < parts[j].PartNumber
|
||||||
@@ -455,12 +453,12 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
|
|||||||
}
|
}
|
||||||
for i, part := range result.Parts {
|
for i, part := range result.Parts {
|
||||||
var stat os.FileInfo
|
var stat os.FileInfo
|
||||||
stat, err = fsStatFile(ctx, pathJoin(uploadIDDir, fs.encodePartFile(part.PartNumber, part.ETag)))
|
stat, err = fsStatFile(ctx, pathJoin(uploadIDDir, fs.encodePartFile(part.PartNumber, part.ETag, part.ActualSize)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, toObjectErr(err)
|
return result, toObjectErr(err)
|
||||||
}
|
}
|
||||||
result.Parts[i].LastModified = stat.ModTime()
|
result.Parts[i].LastModified = stat.ModTime()
|
||||||
result.Parts[i].Size = stat.Size()
|
result.Parts[i].Size = part.ActualSize
|
||||||
}
|
}
|
||||||
|
|
||||||
fsMetaBytes, err := ioutil.ReadFile(pathJoin(uploadIDDir, fs.metaJSONFile))
|
fsMetaBytes, err := ioutil.ReadFile(pathJoin(uploadIDDir, fs.metaJSONFile))
|
||||||
@@ -479,7 +477,10 @@ func (fs *FSObjects) ListObjectParts(ctx context.Context, bucket, object, upload
|
|||||||
// 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(ctx context.Context, bucket string, object string, uploadID string, parts []CompletePart) (oi ObjectInfo, e error) {
|
func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, parts []CompletePart, opts ObjectOptions) (oi ObjectInfo, e error) {
|
||||||
|
|
||||||
|
var actualSize int64
|
||||||
|
|
||||||
if err := checkCompleteMultipartArgs(ctx, bucket, object, fs); err != nil {
|
if err := checkCompleteMultipartArgs(ctx, bucket, object, fs); err != nil {
|
||||||
return oi, toObjectErr(err)
|
return oi, toObjectErr(err)
|
||||||
}
|
}
|
||||||
@@ -516,9 +517,37 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
|||||||
// Allocate parts similar to incoming slice.
|
// Allocate parts similar to incoming slice.
|
||||||
fsMeta.Parts = make([]objectPartInfo, len(parts))
|
fsMeta.Parts = make([]objectPartInfo, len(parts))
|
||||||
|
|
||||||
|
entries, err := readDir(uploadIDDir)
|
||||||
|
if err != nil {
|
||||||
|
logger.GetReqInfo(ctx).AppendTags("uploadIDDir", uploadIDDir)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return oi, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save consolidated actual size.
|
||||||
|
var objectActualSize int64
|
||||||
// Validate all parts and then commit to disk.
|
// Validate all parts and then commit to disk.
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
partPath := pathJoin(uploadIDDir, fs.encodePartFile(part.PartNumber, part.ETag))
|
partFile := getPartFile(entries, part.PartNumber, part.ETag)
|
||||||
|
if partFile == "" {
|
||||||
|
return oi, InvalidPart{
|
||||||
|
PartNumber: part.PartNumber,
|
||||||
|
GotETag: part.ETag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the actualSize from the pathFileName.
|
||||||
|
subParts := strings.Split(partFile, ".")
|
||||||
|
actualSize, err = strconv.ParseInt(subParts[len(subParts)-1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return oi, InvalidPart{
|
||||||
|
PartNumber: part.PartNumber,
|
||||||
|
GotETag: part.ETag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partPath := pathJoin(uploadIDDir, partFile)
|
||||||
|
|
||||||
var fi os.FileInfo
|
var fi os.FileInfo
|
||||||
fi, err = fsStatFile(ctx, partPath)
|
fi, err = fsStatFile(ctx, partPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -528,24 +557,28 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
|||||||
return oi, err
|
return oi, err
|
||||||
}
|
}
|
||||||
if partSize == -1 {
|
if partSize == -1 {
|
||||||
partSize = fi.Size()
|
partSize = actualSize
|
||||||
}
|
}
|
||||||
|
|
||||||
fsMeta.Parts[i] = objectPartInfo{
|
fsMeta.Parts[i] = objectPartInfo{
|
||||||
Number: part.PartNumber,
|
Number: part.PartNumber,
|
||||||
ETag: part.ETag,
|
ETag: part.ETag,
|
||||||
Size: fi.Size(),
|
Size: fi.Size(),
|
||||||
|
ActualSize: actualSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consolidate the actual size.
|
||||||
|
objectActualSize += actualSize
|
||||||
|
|
||||||
if i == len(parts)-1 {
|
if i == len(parts)-1 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// All parts except the last part has to be atleast 5MB.
|
// All parts except the last part has to be atleast 5MB.
|
||||||
if !isMinAllowedPartSize(fi.Size()) {
|
if !isMinAllowedPartSize(actualSize) {
|
||||||
return oi, PartTooSmall{
|
return oi, PartTooSmall{
|
||||||
PartNumber: part.PartNumber,
|
PartNumber: part.PartNumber,
|
||||||
PartSize: fi.Size(),
|
PartSize: actualSize,
|
||||||
PartETag: part.ETag,
|
PartETag: part.ETag,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -587,11 +620,12 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if appendFallback {
|
if appendFallback {
|
||||||
fsRemoveFile(ctx, file.filePath)
|
if file != nil {
|
||||||
|
fsRemoveFile(ctx, file.filePath)
|
||||||
|
}
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
partPath := pathJoin(uploadIDDir, fs.encodePartFile(part.PartNumber, part.ETag))
|
partPath := getPartFile(entries, part.PartNumber, part.ETag)
|
||||||
err = mioutil.AppendFile(appendFilePath, partPath)
|
if err = mioutil.AppendFile(appendFilePath, pathJoin(uploadIDDir, partPath)); err != nil {
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return oi, toObjectErr(err)
|
return oi, toObjectErr(err)
|
||||||
}
|
}
|
||||||
@@ -628,6 +662,8 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
|||||||
fsMeta.Meta = make(map[string]string)
|
fsMeta.Meta = make(map[string]string)
|
||||||
}
|
}
|
||||||
fsMeta.Meta["etag"] = s3MD5
|
fsMeta.Meta["etag"] = s3MD5
|
||||||
|
// Save consolidated actual size.
|
||||||
|
fsMeta.Meta[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(objectActualSize, 10)
|
||||||
if _, err = fsMeta.WriteTo(metaFile); err != nil {
|
if _, err = fsMeta.WriteTo(metaFile); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return oi, toObjectErr(err, bucket, object)
|
return oi, toObjectErr(err, bucket, object)
|
||||||
@@ -646,6 +682,8 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
|||||||
return oi, toObjectErr(err, bucket, object)
|
return oi, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
fsRemoveAll(ctx, uploadIDDir)
|
fsRemoveAll(ctx, uploadIDDir)
|
||||||
|
// It is safe to ignore any directory not empty error (in case there were multiple uploadIDs on the same object)
|
||||||
|
fsRemoveDir(ctx, fs.getMultipartSHADir(bucket, object))
|
||||||
fi, err := fsStatFile(ctx, pathJoin(fs.fsPath, bucket, object))
|
fi, err := fsStatFile(ctx, pathJoin(fs.fsPath, bucket, object))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return oi, toObjectErr(err, bucket, object)
|
return oi, toObjectErr(err, bucket, object)
|
||||||
@@ -676,6 +714,11 @@ func (fs *FSObjects) AbortMultipartUpload(ctx context.Context, bucket, object, u
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.appendFileMapMu.Lock()
|
fs.appendFileMapMu.Lock()
|
||||||
|
// Remove file in tmp folder
|
||||||
|
file := fs.appendFileMap[uploadID]
|
||||||
|
if file != nil {
|
||||||
|
fsRemoveFile(ctx, file.filePath)
|
||||||
|
}
|
||||||
delete(fs.appendFileMap, uploadID)
|
delete(fs.appendFileMap, uploadID)
|
||||||
fs.appendFileMapMu.Unlock()
|
fs.appendFileMapMu.Unlock()
|
||||||
|
|
||||||
@@ -688,10 +731,14 @@ func (fs *FSObjects) AbortMultipartUpload(ctx context.Context, bucket, object, u
|
|||||||
}
|
}
|
||||||
return toObjectErr(err, bucket, object)
|
return toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore the error returned as Windows fails to remove directory if a file in it
|
// Ignore the error returned as Windows fails to remove directory if a file in it
|
||||||
// is Open()ed by the backgroundAppend()
|
// is Open()ed by the backgroundAppend()
|
||||||
fsRemoveAll(ctx, uploadIDDir)
|
fsRemoveAll(ctx, uploadIDDir)
|
||||||
|
|
||||||
|
// It is safe to ignore any directory not empty error (in case there were multiple uploadIDs on the same object)
|
||||||
|
fsRemoveDir(ctx, fs.getMultipartSHADir(bucket, object))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,6 +771,9 @@ func (fs *FSObjects) cleanupStaleMultipartUploads(ctx context.Context, cleanupIn
|
|||||||
}
|
}
|
||||||
if now.Sub(fi.ModTime()) > expiry {
|
if now.Sub(fi.ModTime()) > expiry {
|
||||||
fsRemoveAll(ctx, pathJoin(fs.fsPath, minioMetaMultipartBucket, entry, uploadID))
|
fsRemoveAll(ctx, pathJoin(fs.fsPath, minioMetaMultipartBucket, entry, uploadID))
|
||||||
|
// It is safe to ignore any directory not empty error (in case there were multiple uploadIDs on the same object)
|
||||||
|
fsRemoveDir(ctx, pathJoin(fs.fsPath, minioMetaMultipartBucket, entry))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -42,7 +42,7 @@ func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
|
|||||||
objectName := "object"
|
objectName := "object"
|
||||||
|
|
||||||
obj.MakeBucketWithLocation(context.Background(), bucketName, "")
|
obj.MakeBucketWithLocation(context.Background(), bucketName, "")
|
||||||
uploadID, err := obj.NewMultipartUpload(context.Background(), bucketName, objectName, nil)
|
uploadID, err := obj.NewMultipartUpload(context.Background(), bucketName, objectName, nil, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Unexpected err: ", err)
|
t.Fatal("Unexpected err: ", err)
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ func TestNewMultipartUploadFaultyDisk(t *testing.T) {
|
|||||||
|
|
||||||
// Test with disk removed.
|
// Test with disk removed.
|
||||||
os.RemoveAll(disk)
|
os.RemoveAll(disk)
|
||||||
if _, err := fs.NewMultipartUpload(context.Background(), bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"}); err != nil {
|
if _, err := fs.NewMultipartUpload(context.Background(), bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"}, ObjectOptions{}); err != nil {
|
||||||
if !isSameType(err, BucketNotFound{}) {
|
if !isSameType(err, BucketNotFound{}) {
|
||||||
t.Fatal("Unexpected error ", err)
|
t.Fatal("Unexpected error ", err)
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ func TestPutObjectPartFaultyDisk(t *testing.T) {
|
|||||||
t.Fatal("Cannot create bucket, err: ", err)
|
t.Fatal("Cannot create bucket, err: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadID, err := fs.NewMultipartUpload(context.Background(), bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"})
|
uploadID, err := fs.NewMultipartUpload(context.Background(), bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"}, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Unexpected error ", err)
|
t.Fatal("Unexpected error ", err)
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ func TestPutObjectPartFaultyDisk(t *testing.T) {
|
|||||||
sha256sum := ""
|
sha256sum := ""
|
||||||
|
|
||||||
fs.fsPath = filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
fs.fsPath = filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||||
_, err = fs.PutObjectPart(context.Background(), bucketName, objectName, uploadID, 1, mustGetHashReader(t, bytes.NewReader(data), dataLen, md5Hex, sha256sum))
|
_, err = fs.PutObjectPart(context.Background(), bucketName, objectName, uploadID, 1, mustGetPutObjReader(t, bytes.NewReader(data), dataLen, md5Hex, sha256sum), ObjectOptions{})
|
||||||
if !isSameType(err, BucketNotFound{}) {
|
if !isSameType(err, BucketNotFound{}) {
|
||||||
t.Fatal("Unexpected error ", err)
|
t.Fatal("Unexpected error ", err)
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ func TestCompleteMultipartUploadFaultyDisk(t *testing.T) {
|
|||||||
t.Fatal("Cannot create bucket, err: ", err)
|
t.Fatal("Cannot create bucket, err: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadID, err := fs.NewMultipartUpload(context.Background(), bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"})
|
uploadID, err := fs.NewMultipartUpload(context.Background(), bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"}, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Unexpected error ", err)
|
t.Fatal("Unexpected error ", err)
|
||||||
}
|
}
|
||||||
@@ -145,7 +145,7 @@ func TestCompleteMultipartUploadFaultyDisk(t *testing.T) {
|
|||||||
|
|
||||||
parts := []CompletePart{{PartNumber: 1, ETag: md5Hex}}
|
parts := []CompletePart{{PartNumber: 1, ETag: md5Hex}}
|
||||||
fs.fsPath = filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
fs.fsPath = filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||||
if _, err := fs.CompleteMultipartUpload(context.Background(), bucketName, objectName, uploadID, parts); err != nil {
|
if _, err := fs.CompleteMultipartUpload(context.Background(), bucketName, objectName, uploadID, parts, ObjectOptions{}); err != nil {
|
||||||
if !isSameType(err, BucketNotFound{}) {
|
if !isSameType(err, BucketNotFound{}) {
|
||||||
t.Fatal("Unexpected error ", err)
|
t.Fatal("Unexpected error ", err)
|
||||||
}
|
}
|
||||||
@@ -168,20 +168,20 @@ func TestCompleteMultipartUpload(t *testing.T) {
|
|||||||
t.Fatal("Cannot create bucket, err: ", err)
|
t.Fatal("Cannot create bucket, err: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadID, err := fs.NewMultipartUpload(context.Background(), bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"})
|
uploadID, err := fs.NewMultipartUpload(context.Background(), bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"}, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Unexpected error ", err)
|
t.Fatal("Unexpected error ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
md5Hex := getMD5Hash(data)
|
md5Hex := getMD5Hash(data)
|
||||||
|
|
||||||
if _, err := fs.PutObjectPart(context.Background(), bucketName, objectName, uploadID, 1, mustGetHashReader(t, bytes.NewReader(data), 5, md5Hex, "")); err != nil {
|
if _, err := fs.PutObjectPart(context.Background(), bucketName, objectName, uploadID, 1, mustGetPutObjReader(t, bytes.NewReader(data), 5, md5Hex, ""), ObjectOptions{}); err != nil {
|
||||||
t.Fatal("Unexpected error ", err)
|
t.Fatal("Unexpected error ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := []CompletePart{{PartNumber: 1, ETag: md5Hex}}
|
parts := []CompletePart{{PartNumber: 1, ETag: md5Hex}}
|
||||||
|
|
||||||
if _, err := fs.CompleteMultipartUpload(context.Background(), bucketName, objectName, uploadID, parts); err != nil {
|
if _, err := fs.CompleteMultipartUpload(context.Background(), bucketName, objectName, uploadID, parts, ObjectOptions{}); err != nil {
|
||||||
t.Fatal("Unexpected error ", err)
|
t.Fatal("Unexpected error ", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,14 +202,14 @@ func TestAbortMultipartUpload(t *testing.T) {
|
|||||||
t.Fatal("Cannot create bucket, err: ", err)
|
t.Fatal("Cannot create bucket, err: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadID, err := fs.NewMultipartUpload(context.Background(), bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"})
|
uploadID, err := fs.NewMultipartUpload(context.Background(), bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"}, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Unexpected error ", err)
|
t.Fatal("Unexpected error ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
md5Hex := getMD5Hash(data)
|
md5Hex := getMD5Hash(data)
|
||||||
|
|
||||||
if _, err := fs.PutObjectPart(context.Background(), bucketName, objectName, uploadID, 1, mustGetHashReader(t, bytes.NewReader(data), 5, md5Hex, "")); err != nil {
|
if _, err := fs.PutObjectPart(context.Background(), bucketName, objectName, uploadID, 1, mustGetPutObjReader(t, bytes.NewReader(data), 5, md5Hex, ""), ObjectOptions{}); err != nil {
|
||||||
t.Fatal("Unexpected error ", err)
|
t.Fatal("Unexpected error ", err)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second) // Without Sleep on windows, the fs.AbortMultipartUpload() fails with "The process cannot access the file because it is being used by another process."
|
time.Sleep(time.Second) // Without Sleep on windows, the fs.AbortMultipartUpload() fails with "The process cannot access the file because it is being used by another process."
|
||||||
@@ -234,7 +234,7 @@ func TestListMultipartUploadsFaultyDisk(t *testing.T) {
|
|||||||
t.Fatal("Cannot create bucket, err: ", err)
|
t.Fatal("Cannot create bucket, err: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := fs.NewMultipartUpload(context.Background(), bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"})
|
_, err := fs.NewMultipartUpload(context.Background(), bucketName, objectName, map[string]string{"X-Amz-Meta-xid": "3f"}, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Unexpected error ", err)
|
t.Fatal("Unexpected error ", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+143
-82
@@ -17,10 +17,11 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -30,7 +31,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/hash"
|
|
||||||
"github.com/minio/minio/pkg/lock"
|
"github.com/minio/minio/pkg/lock"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
"github.com/minio/minio/pkg/mimedb"
|
"github.com/minio/minio/pkg/mimedb"
|
||||||
@@ -187,14 +187,9 @@ func (fs *FSObjects) diskUsage(doneCh chan struct{}) {
|
|||||||
case <-doneCh:
|
case <-doneCh:
|
||||||
return errWalkAbort
|
return errWalkAbort
|
||||||
default:
|
default:
|
||||||
var fi os.FileInfo
|
fi, err := os.Stat(entry)
|
||||||
var err error
|
|
||||||
if hasSuffix(entry, slashSeparator) {
|
|
||||||
fi, err = fsStatDir(ctx, entry)
|
|
||||||
} else {
|
|
||||||
fi, err = fsStatFile(ctx, entry)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err = osErrToFSFileErr(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
atomic.AddUint64(&fs.totalUsed, uint64(fi.Size()))
|
atomic.AddUint64(&fs.totalUsed, uint64(fi.Size()))
|
||||||
@@ -226,14 +221,9 @@ func (fs *FSObjects) diskUsage(doneCh chan struct{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fi os.FileInfo
|
fi, err := os.Stat(entry)
|
||||||
var err error
|
|
||||||
if hasSuffix(entry, slashSeparator) {
|
|
||||||
fi, err = fsStatDir(ctx, entry)
|
|
||||||
} else {
|
|
||||||
fi, err = fsStatFile(ctx, entry)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err = osErrToFSFileErr(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
usage = usage + uint64(fi.Size())
|
usage = usage + uint64(fi.Size())
|
||||||
@@ -261,7 +251,7 @@ func (fs *FSObjects) StorageInfo(ctx context.Context) StorageInfo {
|
|||||||
storageInfo := StorageInfo{
|
storageInfo := StorageInfo{
|
||||||
Used: used,
|
Used: used,
|
||||||
}
|
}
|
||||||
storageInfo.Backend.Type = FS
|
storageInfo.Backend.Type = BackendFS
|
||||||
return storageInfo
|
return storageInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,37 +404,21 @@ func (fs *FSObjects) DeleteBucket(ctx context.Context, 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(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo) (oi ObjectInfo, e error) {
|
func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (oi ObjectInfo, e error) {
|
||||||
cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject))
|
cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject))
|
||||||
// Hold write lock on destination since in both cases
|
|
||||||
// - if source and destination are same
|
|
||||||
// - if source and destination are different
|
|
||||||
// it is the sole mutating state.
|
|
||||||
objectDWLock := fs.nsMutex.NewNSLock(dstBucket, dstObject)
|
|
||||||
if err := objectDWLock.GetLock(globalObjectTimeout); err != nil {
|
|
||||||
return oi, err
|
|
||||||
}
|
|
||||||
defer objectDWLock.Unlock()
|
|
||||||
// if source and destination are different, we have to hold
|
|
||||||
// additional read lock as well to protect against writes on
|
|
||||||
// source.
|
|
||||||
if !cpSrcDstSame {
|
if !cpSrcDstSame {
|
||||||
// Hold read locks on source object only if we are
|
objectDWLock := fs.nsMutex.NewNSLock(dstBucket, dstObject)
|
||||||
// going to read data from source object.
|
if err := objectDWLock.GetLock(globalObjectTimeout); err != nil {
|
||||||
objectSRLock := fs.nsMutex.NewNSLock(srcBucket, srcObject)
|
|
||||||
if err := objectSRLock.GetRLock(globalObjectTimeout); err != nil {
|
|
||||||
return oi, err
|
return oi, err
|
||||||
}
|
}
|
||||||
defer objectSRLock.RUnlock()
|
defer objectDWLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fs.statBucketDir(ctx, srcBucket); err != nil {
|
if _, err := fs.statBucketDir(ctx, srcBucket); err != nil {
|
||||||
return oi, toObjectErr(err, srcBucket)
|
return oi, toObjectErr(err, srcBucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cpSrcDstSame && srcInfo.metadataOnly {
|
if cpSrcDstSame && srcInfo.metadataOnly {
|
||||||
// Close any writer which was initialized.
|
|
||||||
defer srcInfo.Writer.Close()
|
|
||||||
|
|
||||||
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, srcBucket, srcObject, fs.metaJSONFile)
|
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, srcBucket, srcObject, fs.metaJSONFile)
|
||||||
wlk, err := fs.rwPool.Write(fsMetaPath)
|
wlk, err := fs.rwPool.Write(fsMetaPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -456,7 +430,7 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
|
|||||||
|
|
||||||
// Save objects' metadata in `fs.json`.
|
// Save objects' metadata in `fs.json`.
|
||||||
fsMeta := newFSMetaV1()
|
fsMeta := newFSMetaV1()
|
||||||
if _, err = fsMeta.ReadFrom(ctx, wlk); err != nil {
|
if _, err = fsMeta.ReadFrom(ctx, wlk); err != nil && err != io.EOF {
|
||||||
return oi, toObjectErr(err, srcBucket, srcObject)
|
return oi, toObjectErr(err, srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,22 +449,7 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
|
|||||||
// Return the new object info.
|
// Return the new object info.
|
||||||
return fsMeta.ToObjectInfo(srcBucket, srcObject, fi), nil
|
return fsMeta.ToObjectInfo(srcBucket, srcObject, fi), nil
|
||||||
}
|
}
|
||||||
|
objInfo, err := fs.putObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, srcInfo.UserDefined, dstOpts)
|
||||||
go func() {
|
|
||||||
if gerr := fs.getObject(ctx, srcBucket, srcObject, 0, srcInfo.Size, srcInfo.Writer, srcInfo.ETag, !cpSrcDstSame); gerr != nil {
|
|
||||||
if gerr = srcInfo.Writer.Close(); gerr != nil {
|
|
||||||
logger.LogIf(ctx, gerr)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Close writer explicitly signaling we wrote all data.
|
|
||||||
if gerr := srcInfo.Writer.Close(); gerr != nil {
|
|
||||||
logger.LogIf(ctx, gerr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
objInfo, err := fs.putObject(ctx, dstBucket, dstObject, srcInfo.Reader, srcInfo.UserDefined)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return oi, toObjectErr(err, dstBucket, dstObject)
|
return oi, toObjectErr(err, dstBucket, dstObject)
|
||||||
}
|
}
|
||||||
@@ -498,13 +457,107 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
|
|||||||
return objInfo, nil
|
return objInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetObjectNInfo - returns object info and a reader for object
|
||||||
|
// content.
|
||||||
|
func (fs *FSObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||||
|
|
||||||
|
if err = checkGetObjArgs(ctx, bucket, object); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = fs.statBucketDir(ctx, bucket); err != nil {
|
||||||
|
return nil, toObjectErr(err, bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nsUnlocker = func() {}
|
||||||
|
|
||||||
|
if lockType != noLock {
|
||||||
|
// Lock the object before reading.
|
||||||
|
lock := fs.nsMutex.NewNSLock(bucket, object)
|
||||||
|
switch lockType {
|
||||||
|
case writeLock:
|
||||||
|
if err = lock.GetLock(globalObjectTimeout); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nsUnlocker = lock.Unlock
|
||||||
|
case readLock:
|
||||||
|
if err = lock.GetRLock(globalObjectTimeout); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nsUnlocker = lock.RUnlock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For a directory, we need to send an reader that returns no bytes.
|
||||||
|
if hasSuffix(object, slashSeparator) {
|
||||||
|
// The lock taken above is released when
|
||||||
|
// objReader.Close() is called by the caller.
|
||||||
|
return NewGetObjectReaderFromReader(bytes.NewBuffer(nil), ObjectInfo{}, nsUnlocker), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we get the object info
|
||||||
|
var objInfo ObjectInfo
|
||||||
|
if objInfo, err = fs.getObjectInfo(ctx, bucket, object); err != nil {
|
||||||
|
nsUnlocker()
|
||||||
|
return nil, toObjectErr(err, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take a rwPool lock for NFS gateway type deployment
|
||||||
|
rwPoolUnlocker := func() {}
|
||||||
|
if bucket != minioMetaBucket && lockType != noLock {
|
||||||
|
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, bucket, object, fs.metaJSONFile)
|
||||||
|
_, err = fs.rwPool.Open(fsMetaPath)
|
||||||
|
if err != nil && err != errFileNotFound {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
nsUnlocker()
|
||||||
|
return nil, toObjectErr(err, bucket, object)
|
||||||
|
}
|
||||||
|
// Need to clean up lock after getObject is
|
||||||
|
// completed.
|
||||||
|
rwPoolUnlocker = func() { fs.rwPool.Close(fsMetaPath) }
|
||||||
|
}
|
||||||
|
|
||||||
|
objReaderFn, off, length, rErr := NewGetObjectReader(rs, objInfo, nsUnlocker, rwPoolUnlocker)
|
||||||
|
if rErr != nil {
|
||||||
|
return nil, rErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the object, doesn't exist returns an s3 compatible error.
|
||||||
|
fsObjPath := pathJoin(fs.fsPath, bucket, object)
|
||||||
|
readCloser, size, err := fsOpenFile(ctx, fsObjPath, off)
|
||||||
|
if err != nil {
|
||||||
|
rwPoolUnlocker()
|
||||||
|
nsUnlocker()
|
||||||
|
return nil, toObjectErr(err, bucket, object)
|
||||||
|
}
|
||||||
|
var reader io.Reader
|
||||||
|
reader = io.LimitReader(readCloser, length)
|
||||||
|
closeFn := func() {
|
||||||
|
readCloser.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if range is valid
|
||||||
|
if off > size || off+length > size {
|
||||||
|
err = InvalidRange{off, length, size}
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
closeFn()
|
||||||
|
rwPoolUnlocker()
|
||||||
|
nsUnlocker()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return objReaderFn(reader, h, closeFn)
|
||||||
|
}
|
||||||
|
|
||||||
// GetObject - reads an object from the disk.
|
// GetObject - reads an object from the disk.
|
||||||
// Supports additional parameters like offset and length
|
// Supports additional parameters like offset and length
|
||||||
// which are synonymous with HTTP Range requests.
|
// which are synonymous with HTTP Range requests.
|
||||||
//
|
//
|
||||||
// startOffset indicates the starting read location of the object.
|
// startOffset indicates the starting read location of the object.
|
||||||
// length indicates the total length of the object.
|
// length indicates the total length of the object.
|
||||||
func (fs *FSObjects) GetObject(ctx context.Context, bucket, object string, offset int64, length int64, writer io.Writer, etag string) (err error) {
|
func (fs *FSObjects) GetObject(ctx context.Context, bucket, object string, offset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error) {
|
||||||
if err = checkGetObjArgs(ctx, bucket, object); err != nil {
|
if err = checkGetObjArgs(ctx, bucket, object); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -596,7 +649,10 @@ func (fs *FSObjects) getObject(ctx context.Context, bucket, object string, offse
|
|||||||
buf := make([]byte, int(bufSize))
|
buf := make([]byte, int(bufSize))
|
||||||
|
|
||||||
_, err = io.CopyBuffer(writer, io.LimitReader(reader, length), buf)
|
_, err = io.CopyBuffer(writer, io.LimitReader(reader, length), buf)
|
||||||
logger.LogIf(ctx, err)
|
// The writer will be closed incase of range queries, which will emit ErrClosedPipe.
|
||||||
|
if err == io.ErrClosedPipe {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
return toObjectErr(err, bucket, object)
|
return toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,13 +661,8 @@ func (fs *FSObjects) createFsJSON(object, fsMetaPath string) error {
|
|||||||
fsMeta := newFSMetaV1()
|
fsMeta := newFSMetaV1()
|
||||||
fsMeta.Meta = make(map[string]string)
|
fsMeta.Meta = make(map[string]string)
|
||||||
fsMeta.Meta["etag"] = GenETag()
|
fsMeta.Meta["etag"] = GenETag()
|
||||||
if objectExt := path.Ext(object); objectExt != "" {
|
contentType := mimedb.TypeByExtension(path.Ext(object))
|
||||||
if content, ok := mimedb.DB[strings.ToLower(strings.TrimPrefix(objectExt, "."))]; ok {
|
fsMeta.Meta["content-type"] = contentType
|
||||||
fsMeta.Meta["content-type"] = content.ContentType
|
|
||||||
} else {
|
|
||||||
fsMeta.Meta["content-type"] = "application/octet-stream"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wlk, werr := fs.rwPool.Create(fsMetaPath)
|
wlk, werr := fs.rwPool.Create(fsMetaPath)
|
||||||
if werr == nil {
|
if werr == nil {
|
||||||
_, err := fsMeta.WriteTo(wlk)
|
_, err := fsMeta.WriteTo(wlk)
|
||||||
@@ -626,13 +677,8 @@ func (fs *FSObjects) defaultFsJSON(object string) fsMetaV1 {
|
|||||||
fsMeta := newFSMetaV1()
|
fsMeta := newFSMetaV1()
|
||||||
fsMeta.Meta = make(map[string]string)
|
fsMeta.Meta = make(map[string]string)
|
||||||
fsMeta.Meta["etag"] = defaultEtag
|
fsMeta.Meta["etag"] = defaultEtag
|
||||||
if objectExt := path.Ext(object); objectExt != "" {
|
contentType := mimedb.TypeByExtension(path.Ext(object))
|
||||||
if content, ok := mimedb.DB[strings.ToLower(strings.TrimPrefix(objectExt, "."))]; ok {
|
fsMeta.Meta["content-type"] = contentType
|
||||||
fsMeta.Meta["content-type"] = content.ContentType
|
|
||||||
} else {
|
|
||||||
fsMeta.Meta["content-type"] = "application/octet-stream"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fsMeta
|
return fsMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,7 +707,11 @@ func (fs *FSObjects) getObjectInfo(ctx context.Context, bucket, object string) (
|
|||||||
_, rerr := fsMeta.ReadFrom(ctx, rlk.LockedFile)
|
_, rerr := fsMeta.ReadFrom(ctx, rlk.LockedFile)
|
||||||
fs.rwPool.Close(fsMetaPath)
|
fs.rwPool.Close(fsMetaPath)
|
||||||
if rerr != nil {
|
if rerr != nil {
|
||||||
return oi, rerr
|
if rerr != io.EOF {
|
||||||
|
return oi, rerr
|
||||||
|
}
|
||||||
|
// Set Default ETag, if fs.json is empty
|
||||||
|
fsMeta = fs.defaultFsJSON(object)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,7 +760,7 @@ func (fs *FSObjects) getObjectInfoWithLock(ctx context.Context, bucket, object s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetObjectInfo - reads object metadata and replies back ObjectInfo.
|
// GetObjectInfo - reads object metadata and replies back ObjectInfo.
|
||||||
func (fs *FSObjects) GetObjectInfo(ctx context.Context, bucket, object string) (oi ObjectInfo, e error) {
|
func (fs *FSObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (oi ObjectInfo, e error) {
|
||||||
oi, err := fs.getObjectInfoWithLock(ctx, bucket, object)
|
oi, err := fs.getObjectInfoWithLock(ctx, bucket, object)
|
||||||
if err == errCorruptedFormat || err == io.EOF {
|
if err == errCorruptedFormat || err == io.EOF {
|
||||||
objectLock := fs.nsMutex.NewNSLock(bucket, object)
|
objectLock := fs.nsMutex.NewNSLock(bucket, object)
|
||||||
@@ -754,8 +804,8 @@ func (fs *FSObjects) parentDirIsObject(ctx context.Context, bucket, parent strin
|
|||||||
// until EOF, writes data directly to configured filesystem path.
|
// until EOF, writes data directly to configured filesystem path.
|
||||||
// Additionally writes `fs.json` which carries the necessary metadata
|
// Additionally writes `fs.json` which carries the necessary metadata
|
||||||
// for future object operations.
|
// for future object operations.
|
||||||
func (fs *FSObjects) PutObject(ctx context.Context, bucket string, object string, data *hash.Reader, metadata map[string]string) (objInfo ObjectInfo, retErr error) {
|
func (fs *FSObjects) PutObject(ctx context.Context, bucket string, object string, r *PutObjReader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, retErr error) {
|
||||||
if err := checkPutObjectArgs(ctx, bucket, object, fs, data.Size()); err != nil {
|
if err := checkPutObjectArgs(ctx, bucket, object, fs, r.Size()); err != nil {
|
||||||
return ObjectInfo{}, err
|
return ObjectInfo{}, err
|
||||||
}
|
}
|
||||||
// Lock the object.
|
// Lock the object.
|
||||||
@@ -765,11 +815,13 @@ func (fs *FSObjects) PutObject(ctx context.Context, bucket string, object string
|
|||||||
return objInfo, err
|
return objInfo, err
|
||||||
}
|
}
|
||||||
defer objectLock.Unlock()
|
defer objectLock.Unlock()
|
||||||
return fs.putObject(ctx, bucket, object, data, metadata)
|
return fs.putObject(ctx, bucket, object, r, metadata, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// putObject - wrapper for PutObject
|
// putObject - wrapper for PutObject
|
||||||
func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string, data *hash.Reader, metadata map[string]string) (objInfo ObjectInfo, retErr error) {
|
func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string, r *PutObjReader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, retErr error) {
|
||||||
|
data := r.Reader
|
||||||
|
|
||||||
// No metadata is set, allocate a new one.
|
// No metadata is set, allocate a new one.
|
||||||
meta := make(map[string]string)
|
meta := make(map[string]string)
|
||||||
for k, v := range metadata {
|
for k, v := range metadata {
|
||||||
@@ -816,7 +868,7 @@ func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate input data size and it can never be less than zero.
|
// Validate input data size and it can never be less than zero.
|
||||||
if data.Size() < 0 {
|
if data.Size() < -1 {
|
||||||
logger.LogIf(ctx, errInvalidArgument)
|
logger.LogIf(ctx, errInvalidArgument)
|
||||||
return ObjectInfo{}, errInvalidArgument
|
return ObjectInfo{}, errInvalidArgument
|
||||||
}
|
}
|
||||||
@@ -860,8 +912,7 @@ func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string
|
|||||||
fsRemoveFile(ctx, fsTmpObjPath)
|
fsRemoveFile(ctx, fsTmpObjPath)
|
||||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
fsMeta.Meta["etag"] = r.MD5CurrentHexString()
|
||||||
fsMeta.Meta["etag"] = hex.EncodeToString(data.MD5Current())
|
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -959,7 +1010,7 @@ func (fs *FSObjects) listDirFactory(isLeaf isLeafFunc) listDirFunc {
|
|||||||
listDir := func(bucket, prefixDir, prefixEntry string) (entries []string, delayIsLeaf bool) {
|
listDir := func(bucket, prefixDir, prefixEntry string) (entries []string, delayIsLeaf bool) {
|
||||||
var err error
|
var err error
|
||||||
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
||||||
if err != nil {
|
if err != nil && err != errFileNotFound {
|
||||||
logger.LogIf(context.Background(), err)
|
logger.LogIf(context.Background(), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1215,7 +1266,7 @@ func (fs *FSObjects) ListBucketsHeal(ctx context.Context) ([]BucketInfo, error)
|
|||||||
|
|
||||||
// SetBucketPolicy sets policy on bucket
|
// SetBucketPolicy sets policy on bucket
|
||||||
func (fs *FSObjects) SetBucketPolicy(ctx context.Context, bucket string, policy *policy.Policy) error {
|
func (fs *FSObjects) SetBucketPolicy(ctx context.Context, bucket string, policy *policy.Policy) error {
|
||||||
return savePolicyConfig(fs, bucket, policy)
|
return savePolicyConfig(ctx, fs, bucket, policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketPolicy will get policy on bucket
|
// GetBucketPolicy will get policy on bucket
|
||||||
@@ -1255,7 +1306,17 @@ func (fs *FSObjects) IsNotificationSupported() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsListenBucketSupported returns whether listen bucket notification is applicable for this layer.
|
||||||
|
func (fs *FSObjects) IsListenBucketSupported() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// IsEncryptionSupported returns whether server side encryption is applicable for this layer.
|
// IsEncryptionSupported returns whether server side encryption is applicable for this layer.
|
||||||
func (fs *FSObjects) IsEncryptionSupported() bool {
|
func (fs *FSObjects) IsEncryptionSupported() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCompressionSupported returns whether compression is applicable for this layer.
|
||||||
|
func (fs *FSObjects) IsCompressionSupported() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
+8
-8
@@ -40,7 +40,7 @@ func TestFSParentDirIsObject(t *testing.T) {
|
|||||||
}
|
}
|
||||||
objectContent := "12345"
|
objectContent := "12345"
|
||||||
objInfo, err := obj.PutObject(context.Background(), bucketName, objectName,
|
objInfo, err := obj.PutObject(context.Background(), bucketName, objectName,
|
||||||
mustGetHashReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), nil)
|
mustGetPutObjReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), nil, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ func TestFSShutdown(t *testing.T) {
|
|||||||
|
|
||||||
objectContent := "12345"
|
objectContent := "12345"
|
||||||
obj.MakeBucketWithLocation(context.Background(), bucketName, "")
|
obj.MakeBucketWithLocation(context.Background(), bucketName, "")
|
||||||
obj.PutObject(context.Background(), bucketName, objectName, mustGetHashReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), nil)
|
obj.PutObject(context.Background(), bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), nil, ObjectOptions{})
|
||||||
return fs, disk
|
return fs, disk
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ func TestFSPutObject(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// With a regular object.
|
// With a regular object.
|
||||||
_, err := obj.PutObject(context.Background(), bucketName+"non-existent", objectName, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil)
|
_, err := obj.PutObject(context.Background(), bucketName+"non-existent", objectName, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil, ObjectOptions{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Unexpected should fail here, bucket doesn't exist")
|
t.Fatal("Unexpected should fail here, bucket doesn't exist")
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,7 @@ func TestFSPutObject(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// With a directory object.
|
// With a directory object.
|
||||||
_, err = obj.PutObject(context.Background(), bucketName+"non-existent", objectName+"/", mustGetHashReader(t, bytes.NewReader([]byte("abcd")), 0, "", ""), nil)
|
_, err = obj.PutObject(context.Background(), bucketName+"non-existent", objectName+"/", mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), 0, "", ""), nil, ObjectOptions{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Unexpected should fail here, bucket doesn't exist")
|
t.Fatal("Unexpected should fail here, bucket doesn't exist")
|
||||||
}
|
}
|
||||||
@@ -220,11 +220,11 @@ func TestFSPutObject(t *testing.T) {
|
|||||||
t.Fatalf("Expected error type BucketNotFound, got %#v", err)
|
t.Fatalf("Expected error type BucketNotFound, got %#v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = obj.PutObject(context.Background(), bucketName, objectName, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil)
|
_, err = obj.PutObject(context.Background(), bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil, ObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_, err = obj.PutObject(context.Background(), bucketName, objectName+"/1", mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil)
|
_, err = obj.PutObject(context.Background(), bucketName, objectName+"/1", mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil, ObjectOptions{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Unexpected should fail here, backend corruption occurred")
|
t.Fatal("Unexpected should fail here, backend corruption occurred")
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ func TestFSPutObject(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = obj.PutObject(context.Background(), bucketName, objectName+"/1/", mustGetHashReader(t, bytes.NewReader([]byte("abcd")), 0, "", ""), nil)
|
_, err = obj.PutObject(context.Background(), bucketName, objectName+"/1/", mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), 0, "", ""), nil, ObjectOptions{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Unexpected should fail here, backned corruption occurred")
|
t.Fatal("Unexpected should fail here, backned corruption occurred")
|
||||||
}
|
}
|
||||||
@@ -267,7 +267,7 @@ func TestFSDeleteObject(t *testing.T) {
|
|||||||
objectName := "object"
|
objectName := "object"
|
||||||
|
|
||||||
obj.MakeBucketWithLocation(context.Background(), bucketName, "")
|
obj.MakeBucketWithLocation(context.Background(), bucketName, "")
|
||||||
obj.PutObject(context.Background(), bucketName, objectName, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil)
|
obj.PutObject(context.Background(), bucketName, objectName, mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil, ObjectOptions{})
|
||||||
|
|
||||||
// Test with invalid bucket name
|
// Test with invalid bucket name
|
||||||
if err := fs.DeleteObject(context.Background(), "fo", objectName); !isSameType(err, BucketNameInvalid{}) {
|
if err := fs.DeleteObject(context.Background(), "fo", objectName); !isSameType(err, BucketNameInvalid{}) {
|
||||||
|
|||||||
+12
-1
@@ -224,7 +224,16 @@ func FromMinioClientListBucketResultToV2Info(bucket string, result minio.ListBuc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMinioClientMetadata converts metadata to map[string][]string
|
// ToMinioClientObjectInfoMetadata convertes metadata to map[string][]string
|
||||||
|
func ToMinioClientObjectInfoMetadata(metadata map[string]string) map[string][]string {
|
||||||
|
mm := make(map[string][]string, len(metadata))
|
||||||
|
for k, v := range metadata {
|
||||||
|
mm[http.CanonicalHeaderKey(k)] = []string{v}
|
||||||
|
}
|
||||||
|
return mm
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMinioClientMetadata converts metadata to map[string]string
|
||||||
func ToMinioClientMetadata(metadata map[string]string) map[string]string {
|
func ToMinioClientMetadata(metadata map[string]string) map[string]string {
|
||||||
mm := make(map[string]string)
|
mm := make(map[string]string)
|
||||||
for k, v := range metadata {
|
for k, v := range metadata {
|
||||||
@@ -285,6 +294,8 @@ func ErrorRespToObjectError(err error, params ...string) error {
|
|||||||
err = BucketPolicyNotFound{}
|
err = BucketPolicyNotFound{}
|
||||||
case "InvalidBucketName":
|
case "InvalidBucketName":
|
||||||
err = BucketNameInvalid{Bucket: bucket}
|
err = BucketNameInvalid{Bucket: bucket}
|
||||||
|
case "InvalidPart":
|
||||||
|
err = InvalidPart{}
|
||||||
case "NoSuchBucket":
|
case "NoSuchBucket":
|
||||||
err = BucketNotFound{Bucket: bucket}
|
err = BucketNotFound{Bucket: bucket}
|
||||||
case "NoSuchKey":
|
case "NoSuchKey":
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user