mirror of
https://github.com/pgsty/minio.git
synced 2026-10-04 00:35:59 +03:00
Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 9f14433cbd | |||
| 50a817e3d3 | |||
| 5a4a57700b | |||
| 3de5a3157f | |||
| 50dec08002 | |||
| e71ef905f9 | |||
| 3d197c1449 | |||
| 65de2d68c0 | |||
| 1103ad2d08 | |||
| eab947cf42 | |||
| 0fe9e95250 | |||
| c7946ab9ab | |||
| f5df3b4795 | |||
| f26325c988 | |||
| 7c14cdb60e | |||
| 0e02328c98 | |||
| 380524ae27 | |||
| 0286e61aee | |||
| ff29aed05d | |||
| 64f2c61813 | |||
| 525c04fd07 | |||
| efac90461a | |||
| 71979376b5 | |||
| 5a1ae862a7 | |||
| 6df20734f9 | |||
| 98bce72295 | |||
| 2f1756489e | |||
| 9719640e34 | |||
| ce02ab613d | |||
| 37de2dbd3b | |||
| a82500f162 | |||
| 2dede2fdc2 | |||
| 13fbb96736 | |||
| eabfcea34e | |||
| a078703214 | |||
| bd2b22572f | |||
| 5f69f04909 | |||
| a1a426e523 | |||
| a091b1a3ee | |||
| 556a51120c | |||
| eb391a53c1 | |||
| e17e09ea3c | |||
| 76c423392a | |||
| 8e6d756e3a | |||
| a7c9058375 | |||
| b16e33bcf5 | |||
| 197af49c99 | |||
| 264cc4020f | |||
| df88421087 |
@@ -24,6 +24,10 @@
|
||||
<!--- 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 -->
|
||||
|
||||
## 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
|
||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||
* Version used (`minio version`):
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
<!--- Why is this change required? What problem does it solve? -->
|
||||
<!--- 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?
|
||||
<!--- Please describe in detail how you tested your changes. -->
|
||||
<!--- Include details of your testing environment, and the tests you ran to -->
|
||||
|
||||
@@ -22,3 +22,4 @@ parts/
|
||||
prime/
|
||||
stage/
|
||||
.sia_temp/
|
||||
config.json
|
||||
+9
-2
@@ -8,19 +8,26 @@ dist: trusty
|
||||
|
||||
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:
|
||||
include:
|
||||
- os: linux
|
||||
env:
|
||||
- ARCH=x86_64
|
||||
go: 1.10.1
|
||||
go: 1.10.3
|
||||
script:
|
||||
- make
|
||||
- diff -au <(gofmt -s -d cmd) <(printf "")
|
||||
- diff -au <(gofmt -s -d pkg) <(printf "")
|
||||
- make test GOFLAGS="-timeout 15m -race -v"
|
||||
- make verify
|
||||
- make coverage
|
||||
- node --version
|
||||
- cd browser && yarn && yarn test && cd ..
|
||||
|
||||
before_install:
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ WORKDIR /go/src/github.com/minio/
|
||||
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates curl && \
|
||||
apk add --no-cache ca-certificates 'curl>7.61.0' && \
|
||||
apk add --no-cache --virtual .build-deps git && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
go get -v -d github.com/minio/minio && \
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ 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 ca-certificates 'curl>7.61.0' && \
|
||||
apk add --no-cache --virtual .build-deps git && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
cd /go/src/github.com/minio/minio && \
|
||||
|
||||
+1
-2
@@ -9,8 +9,7 @@ ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
MINIO_SECRET_KEY_FILE=secret_key
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates && \
|
||||
apk add --no-cache --virtual .build-deps curl && \
|
||||
apk add --no-cache ca-certificates 'curl>7.61.0' && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
curl https://dl.minio.io/server/minio/release/linux-amd64/minio > /usr/bin/minio && \
|
||||
chmod +x /usr/bin/minio && \
|
||||
|
||||
@@ -42,8 +42,8 @@ ineffassign:
|
||||
|
||||
cyclo:
|
||||
@echo "Running $@"
|
||||
@${GOPATH}/bin/gocyclo -over 100 cmd
|
||||
@${GOPATH}/bin/gocyclo -over 100 pkg
|
||||
@${GOPATH}/bin/gocyclo -over 200 cmd
|
||||
@${GOPATH}/bin/gocyclo -over 200 pkg
|
||||
|
||||
deadcode:
|
||||
@echo "Running $@"
|
||||
@@ -61,6 +61,8 @@ check: test
|
||||
test: verifiers build
|
||||
@echo "Running unit tests"
|
||||
@go test $(GOFLAGS) -tags kqueue ./...
|
||||
|
||||
verify: build
|
||||
@echo "Verifying build"
|
||||
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ clone_folder: c:\gopath\src\github.com\minio\minio
|
||||
# Environment variables
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
GOROOT: c:\go
|
||||
GOROOT: c:\go110
|
||||
|
||||
# scripts that run after cloning repository
|
||||
install:
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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 { connect } from "react-redux"
|
||||
@@ -42,37 +42,40 @@ export class Policy extends React.Component {
|
||||
render() {
|
||||
const {policy, prefix} = this.props
|
||||
let newPrefix = prefix
|
||||
|
||||
if (newPrefix === '')
|
||||
newPrefix = '*'
|
||||
|
||||
return (
|
||||
<div className="pmb-list">
|
||||
<div className="pmbl-item">
|
||||
{ newPrefix }
|
||||
if (policy === NONE) {
|
||||
return <noscript />
|
||||
} else {
|
||||
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 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 { shallow, mount } from "enzyme"
|
||||
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"
|
||||
|
||||
jest.mock("../../web", () => ({
|
||||
@@ -31,6 +31,11 @@ describe("Policy", () => {
|
||||
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", () => {
|
||||
const fetchPolicies = jest.fn()
|
||||
const wrapper = shallow(
|
||||
|
||||
@@ -23,6 +23,7 @@ export const minioBrowserPrefix = p.slice(0, p.indexOf("/", 1))
|
||||
export const READ_ONLY = "readonly"
|
||||
export const WRITE_ONLY = "writeonly"
|
||||
export const READ_WRITE = "readwrite"
|
||||
export const NONE = "none"
|
||||
|
||||
export const SHARE_OBJECT_EXPIRY_DAYS = 5
|
||||
export const SHARE_OBJECT_EXPIRY_HOURS = 0
|
||||
|
||||
@@ -57,7 +57,7 @@ export class ObjectActions extends React.Component {
|
||||
})
|
||||
}
|
||||
render() {
|
||||
const { object, showShareObjectModal } = this.props
|
||||
const { object, showShareObjectModal, shareObjectName } = this.props
|
||||
return (
|
||||
<Dropdown id={`obj-actions-${object.name}`}>
|
||||
<Dropdown.Toggle noCaret className="fia-toggle" />
|
||||
@@ -77,7 +77,8 @@ export class ObjectActions extends React.Component {
|
||||
<i className="fa fa-trash" />
|
||||
</a>
|
||||
</Dropdown.Menu>
|
||||
{showShareObjectModal && <ShareObjectModal object={object} />}
|
||||
{(showShareObjectModal && shareObjectName === object.name) &&
|
||||
<ShareObjectModal object={object} />}
|
||||
{this.state.showDeleteConfirmation && (
|
||||
<DeleteObjectConfirmModal
|
||||
deleteObject={this.deleteObject.bind(this)}
|
||||
@@ -92,7 +93,8 @@ export class ObjectActions extends React.Component {
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
object: ownProps.object,
|
||||
showShareObjectModal: state.objects.shareObject.show
|
||||
showShareObjectModal: state.objects.shareObject.show,
|
||||
shareObjectName: state.objects.shareObject.object
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,8 +88,21 @@ describe("ObjectActions", () => {
|
||||
object={{ name: "obj1" }}
|
||||
currentPrefix={"pre1/"}
|
||||
showShareObjectModal={true}
|
||||
shareObjectName={"obj1"}
|
||||
/>
|
||||
)
|
||||
expect(wrapper.find("Connect(ShareObjectModal)").length).toBe(1)
|
||||
})
|
||||
|
||||
it("shouldn't render ShareObjectModal when the names of the objects don't match", () => {
|
||||
const wrapper = shallow(
|
||||
<ObjectActions
|
||||
object={{ name: "obj1" }}
|
||||
currentPrefix={"pre1/"}
|
||||
showShareObjectModal={true}
|
||||
shareObjectName={"obj2"}
|
||||
/>
|
||||
)
|
||||
expect(wrapper.find("Connect(ShareObjectModal)").length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,16 +19,27 @@ import thunk from "redux-thunk"
|
||||
import * as actionsObjects from "../actions"
|
||||
import * as alertActions from "../../alert/actions"
|
||||
import { minioBrowserPrefix } from "../../constants"
|
||||
import history from "../../history"
|
||||
|
||||
jest.mock("../../web", () => ({
|
||||
LoggedIn: jest.fn(() => true).mockReturnValueOnce(false),
|
||||
ListObjects: jest.fn(() => {
|
||||
return Promise.resolve({
|
||||
objects: [{ name: "test1" }, { name: "test2" }],
|
||||
istruncated: false,
|
||||
nextmarker: "test2",
|
||||
writable: false
|
||||
})
|
||||
LoggedIn: jest
|
||||
.fn(() => true)
|
||||
.mockReturnValueOnce(true)
|
||||
.mockReturnValueOnce(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 }) => {
|
||||
if (!bucketName) {
|
||||
@@ -112,6 +123,9 @@ describe("Objects actions", () => {
|
||||
objects: { currentPrefix: "" }
|
||||
})
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "alert/CLEAR"
|
||||
},
|
||||
{
|
||||
type: "objects/SET_LIST",
|
||||
objects: [{ name: "test1" }, { name: "test2" }],
|
||||
@@ -143,6 +157,9 @@ describe("Objects actions", () => {
|
||||
objects: { currentPrefix: "" }
|
||||
})
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "alert/CLEAR"
|
||||
},
|
||||
{
|
||||
type: "objects/APPEND_LIST",
|
||||
objects: [{ name: "test1" }, { name: "test2" }],
|
||||
@@ -160,6 +177,43 @@ 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/CLEAR"
|
||||
},
|
||||
{
|
||||
type: "alert/SET",
|
||||
alert: {
|
||||
type: "danger",
|
||||
message: "listobjects is denied",
|
||||
id: alertActions.alertId
|
||||
}
|
||||
},
|
||||
{
|
||||
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", () => {
|
||||
const store = mockStore({
|
||||
objects: {
|
||||
@@ -198,6 +252,7 @@ describe("Objects actions", () => {
|
||||
})
|
||||
const expectedActions = [
|
||||
{ type: "objects/SET_CURRENT_PREFIX", prefix: "abc/" },
|
||||
{ type: "alert/CLEAR" },
|
||||
{ type: "objects/CHECKED_LIST_RESET" }
|
||||
]
|
||||
store.dispatch(actionsObjects.selectPrefix("abc/"))
|
||||
@@ -244,7 +299,11 @@ describe("Objects actions", () => {
|
||||
const expectedActions = [
|
||||
{
|
||||
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(() => {
|
||||
|
||||
@@ -24,9 +24,11 @@ import {
|
||||
import { getCurrentBucket } from "../buckets/selectors"
|
||||
import { getCurrentPrefix, getCheckedList } from "./selectors"
|
||||
import * as alertActions from "../alert/actions"
|
||||
import * as bucketActions from "../buckets/actions"
|
||||
import { minioBrowserPrefix } from "../constants"
|
||||
|
||||
export const SET_LIST = "objects/SET_LIST"
|
||||
export const RESET_LIST = "object/RESET_LIST"
|
||||
export const APPEND_LIST = "objects/APPEND_LIST"
|
||||
export const REMOVE = "objects/REMOVE"
|
||||
export const SET_SORT_BY = "objects/SET_SORT_BY"
|
||||
@@ -45,6 +47,10 @@ export const setList = (objects, marker, isTruncated) => ({
|
||||
isTruncated
|
||||
})
|
||||
|
||||
export const resetList = () => ({
|
||||
type: RESET_LIST
|
||||
})
|
||||
|
||||
export const appendList = (objects, marker, isTruncated) => ({
|
||||
type: APPEND_LIST,
|
||||
objects,
|
||||
@@ -58,6 +64,7 @@ export const fetchObjects = append => {
|
||||
buckets: { currentBucket },
|
||||
objects: { currentPrefix, marker }
|
||||
} = getState()
|
||||
dispatch(alertActions.clear())
|
||||
if (currentBucket) {
|
||||
return web
|
||||
.ListObjects({
|
||||
@@ -85,8 +92,13 @@ export const fetchObjects = append => {
|
||||
dispatch(setPrefixWritable(res.writable))
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(alertActions.set({ type: "danger", message: err.message }))
|
||||
history.push("/login")
|
||||
if (web.LoggedIn()) {
|
||||
dispatch(alertActions.set({ type: "danger", message: err.message }))
|
||||
dispatch(resetList())
|
||||
}
|
||||
else {
|
||||
history.push("/login")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,13 @@ export default (
|
||||
marker: action.marker,
|
||||
isTruncated: action.isTruncated
|
||||
}
|
||||
case actionsObjects.RESET_LIST:
|
||||
return {
|
||||
...state,
|
||||
list: [],
|
||||
marker: "",
|
||||
isTruncated: false
|
||||
}
|
||||
case actionsObjects.APPEND_LIST:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -109,7 +109,15 @@ div.fesl-row {
|
||||
/*--------------------------
|
||||
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=zip] { .list-type(#427089, '\f1c6'); }
|
||||
&[data-type=audio] { .list-type(#009688, '\f1c7'); }
|
||||
|
||||
+2
-2
@@ -70,9 +70,9 @@ async.waterfall([
|
||||
commitId = stdout.replace('\n', '')
|
||||
if (commitId.length !== 40) throw new Error('commitId invalid : ' + commitId)
|
||||
assetsFileName = 'ui-assets.go';
|
||||
var cmd = 'go-bindata-assetfs -pkg browser -nocompress=true production/...'
|
||||
var cmd = 'go-bindata-assetfs -o bindata_assetfs.go -pkg browser -nocompress=true production/...'
|
||||
if (!isProduction) {
|
||||
cmd = 'go-bindata-assetfs -pkg browser -nocompress=true dev/...'
|
||||
cmd = 'go-bindata-assetfs -o bindata_assetfs.go -pkg browser -nocompress=true dev/...'
|
||||
}
|
||||
console.log('Running', cmd)
|
||||
exec(cmd, cb)
|
||||
|
||||
+26
-37
File diff suppressed because one or more lines are too long
@@ -89,11 +89,11 @@ check_minimum_version() {
|
||||
|
||||
assert_is_supported_arch() {
|
||||
case "${ARCH}" in
|
||||
x86_64 | amd64 | aarch64 | ppc64le | arm* )
|
||||
x86_64 | amd64 | aarch64 | ppc64le | arm* | s390x )
|
||||
return
|
||||
;;
|
||||
*)
|
||||
echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, aarch64, ppc64le, arm*]"
|
||||
echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, aarch64, ppc64le, arm*, s390x]"
|
||||
exit 1
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ function start_minio_erasure_sets()
|
||||
function start_minio_dist_erasure_sets()
|
||||
{
|
||||
declare -a minio_pids
|
||||
export MINIO_ACCESS_KEY=$ACCESS_KEY
|
||||
export MINIO_SECRET_KEY=$SECRET_KEY
|
||||
"${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9000.log" 2>&1 &
|
||||
minio_pids[0]=$!
|
||||
"${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9001.log" 2>&1 &
|
||||
@@ -99,6 +101,8 @@ function start_minio_dist_erasure_sets()
|
||||
function start_minio_dist_erasure()
|
||||
{
|
||||
declare -a minio_pids
|
||||
export MINIO_ACCESS_KEY=$ACCESS_KEY
|
||||
export MINIO_SECRET_KEY=$SECRET_KEY
|
||||
"${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9000.log" 2>&1 &
|
||||
minio_pids[0]=$!
|
||||
"${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9001.log" 2>&1 &
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
// Before proceeding validate if object exists.
|
||||
_, err := objAPI.GetObjectInfo(ctx, bucket, object)
|
||||
_, err := objAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
|
||||
+322
-271
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016, 2017 Minio, Inc.
|
||||
* 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.
|
||||
@@ -19,12 +19,13 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -34,11 +35,11 @@ import (
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
const (
|
||||
minioConfigTmpFormat = "config-%s.json"
|
||||
|
||||
maxConfigJSONSize = 256 * 1024 // 256KiB
|
||||
)
|
||||
|
||||
@@ -47,11 +48,10 @@ type mgmtQueryKey string
|
||||
|
||||
// Only valid query params for mgmt admin APIs.
|
||||
const (
|
||||
mgmtBucket mgmtQueryKey = "bucket"
|
||||
mgmtPrefix mgmtQueryKey = "prefix"
|
||||
mgmtLockOlderThan mgmtQueryKey = "older-than"
|
||||
mgmtClientToken mgmtQueryKey = "clientToken"
|
||||
mgmtForceStart mgmtQueryKey = "forceStart"
|
||||
mgmtBucket mgmtQueryKey = "bucket"
|
||||
mgmtPrefix mgmtQueryKey = "prefix"
|
||||
mgmtClientToken mgmtQueryKey = "clientToken"
|
||||
mgmtForceStart mgmtQueryKey = "forceStart"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -275,133 +275,6 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
}
|
||||
|
||||
// validateLockQueryParams - Validates query params for list/clear
|
||||
// locks management APIs.
|
||||
func validateLockQueryParams(vars url.Values) (string, string, time.Duration,
|
||||
APIErrorCode) {
|
||||
|
||||
bucket := vars.Get(string(mgmtBucket))
|
||||
prefix := vars.Get(string(mgmtPrefix))
|
||||
olderThanStr := vars.Get(string(mgmtLockOlderThan))
|
||||
|
||||
// N B empty bucket name is invalid
|
||||
if !IsValidBucketName(bucket) {
|
||||
return "", "", time.Duration(0), ErrInvalidBucketName
|
||||
}
|
||||
// empty prefix is valid.
|
||||
if !IsValidObjectPrefix(prefix) {
|
||||
return "", "", time.Duration(0), ErrInvalidObjectName
|
||||
}
|
||||
|
||||
// If older-than parameter was empty then set it to 0s to list
|
||||
// all locks older than now.
|
||||
if olderThanStr == "" {
|
||||
olderThanStr = "0s"
|
||||
}
|
||||
duration, err := time.ParseDuration(olderThanStr)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return "", "", time.Duration(0), ErrInvalidDuration
|
||||
}
|
||||
|
||||
return bucket, prefix, duration, ErrNone
|
||||
}
|
||||
|
||||
// ListLocksHandler - GET /minio/admin/v1/locks?bucket=mybucket&prefix=myprefix&older-than=10s
|
||||
// - bucket is a mandatory query parameter
|
||||
// - prefix and older-than are optional query parameters
|
||||
// ---------
|
||||
// Lists locks held on a given bucket, prefix and duration it was held for.
|
||||
func (a adminAPIHandlers) ListLocksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := r.URL.Query()
|
||||
bucket, prefix, duration, adminAPIErr := validateLockQueryParams(vars)
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch lock information of locks matching bucket/prefix that
|
||||
// are available for longer than duration.
|
||||
volLocks, err := listPeerLocksInfo(globalAdminPeers, bucket, prefix,
|
||||
duration)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
||||
logger.LogIf(context.Background(), err)
|
||||
return
|
||||
}
|
||||
|
||||
// Marshal list of locks as json.
|
||||
jsonBytes, err := json.Marshal(volLocks)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
||||
logger.LogIf(context.Background(), err)
|
||||
return
|
||||
}
|
||||
|
||||
// Reply with list of locks held on bucket, matching prefix
|
||||
// held longer than duration supplied, as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
}
|
||||
|
||||
// ClearLocksHandler - DELETE /minio/admin/v1/locks?bucket=mybucket&prefix=myprefix&duration=duration
|
||||
// - bucket is a mandatory query parameter
|
||||
// - prefix and older-than are optional query parameters
|
||||
// ---------
|
||||
// Clear locks held on a given bucket, prefix and duration it was held for.
|
||||
func (a adminAPIHandlers) ClearLocksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ClearLocks")
|
||||
|
||||
// Get object layer instance.
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := r.URL.Query()
|
||||
bucket, prefix, duration, adminAPIErr := validateLockQueryParams(vars)
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch lock information of locks matching bucket/prefix that
|
||||
// are held for longer than duration.
|
||||
volLocks, err := listPeerLocksInfo(globalAdminPeers, bucket, prefix,
|
||||
duration)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Marshal list of locks as json.
|
||||
jsonBytes, err := json.Marshal(volLocks)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
objLayer.ClearLocks(ctx, volLocks)
|
||||
|
||||
// Reply with list of locks cleared, as json.
|
||||
writeSuccessResponseJSON(w, jsonBytes)
|
||||
}
|
||||
|
||||
// extractHealInitParams - Validates params for heal init API.
|
||||
func extractHealInitParams(r *http.Request) (bucket, objPrefix string,
|
||||
hs madmin.HealOpts, clientToken string, forceStart bool,
|
||||
@@ -570,6 +443,15 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// GetConfigHandler - GET /minio/admin/v1/config
|
||||
// Get config.json of this minio setup.
|
||||
func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetConfigHandler")
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request signature.
|
||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
@@ -577,26 +459,112 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
// Take a read lock on minio/config.json. NB minio is a
|
||||
// reserved bucket name and wouldn't conflict with normal
|
||||
// object operations.
|
||||
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
|
||||
if configLock.GetRLock(globalObjectTimeout) != nil {
|
||||
writeErrorResponseJSON(w, ErrOperationTimedOut, r.URL)
|
||||
return
|
||||
}
|
||||
defer configLock.RUnlock()
|
||||
|
||||
// Get config.json - in distributed mode, the configuration
|
||||
// occurring on a quorum of the servers is returned.
|
||||
configBytes, err := getPeerConfig(globalAdminPeers)
|
||||
config, err := readServerConfig(ctx, objectAPI)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, configBytes)
|
||||
configData, err := json.MarshalIndent(config, "", "\t")
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
password := config.GetCredential().SecretKey
|
||||
econfigData, err := madmin.EncryptServerConfigData(password, configData)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// Disable tidwall json array notation in JSON key path so
|
||||
// users can set json with a key as a number.
|
||||
// In tidwall json, notify.webhook.0 = val means { "notify" : { "webhook" : [val] }}
|
||||
// In Minio, notify.webhook.0 = val means { "notify" : { "webhook" : {"0" : val}}}
|
||||
func normalizeJSONKey(input string) (key string) {
|
||||
subKeys := strings.Split(input, ".")
|
||||
for i, k := range subKeys {
|
||||
if i > 0 {
|
||||
key += "."
|
||||
}
|
||||
if _, err := strconv.Atoi(k); err == nil {
|
||||
key += ":" + k
|
||||
} else {
|
||||
key += k
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetConfigHandler - GET /minio/admin/v1/config-keys
|
||||
// Get some keys in config.json of this minio setup.
|
||||
func (a adminAPIHandlers) GetConfigKeysHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetConfigKeysHandler")
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request signature.
|
||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var keys []string
|
||||
queries := r.URL.Query()
|
||||
|
||||
for k := range queries {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
config, err := readServerConfig(ctx, objectAPI)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
configData, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
configStr := string(configData)
|
||||
newConfigStr := `{}`
|
||||
|
||||
for _, key := range keys {
|
||||
// sjson.Set does not return an error if key is empty
|
||||
// we should check by ourselves here
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
val := gjson.Get(configStr, key)
|
||||
if j, err := sjson.Set(newConfigStr, normalizeJSONKey(key), val.Value()); err == nil {
|
||||
newConfigStr = j
|
||||
}
|
||||
}
|
||||
|
||||
password := config.GetCredential().SecretKey
|
||||
econfigData, err := madmin.EncryptServerConfigData(password, []byte(newConfigStr))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, []byte(econfigData))
|
||||
}
|
||||
|
||||
// toAdminAPIErrCode - converts errXLWriteQuorum error to admin API
|
||||
@@ -605,65 +573,15 @@ func toAdminAPIErrCode(err error) APIErrorCode {
|
||||
switch err {
|
||||
case errXLWriteQuorum:
|
||||
return ErrAdminConfigNoQuorum
|
||||
default:
|
||||
return toAPIErrorCode(err)
|
||||
}
|
||||
return toAPIErrorCode(err)
|
||||
}
|
||||
|
||||
// SetConfigResult - represents detailed results of a set-config
|
||||
// operation.
|
||||
type nodeSummary struct {
|
||||
Name string `json:"name"`
|
||||
ErrSet bool `json:"errSet"`
|
||||
ErrMsg string `json:"errMsg"`
|
||||
}
|
||||
|
||||
type setConfigResult struct {
|
||||
NodeResults []nodeSummary `json:"nodeResults"`
|
||||
Status bool `json:"status"`
|
||||
}
|
||||
|
||||
// writeSetConfigResponse - writes setConfigResult value as json
|
||||
// depending on the status.
|
||||
func writeSetConfigResponse(w http.ResponseWriter, peers adminPeers,
|
||||
errs []error, status bool, reqURL *url.URL) {
|
||||
|
||||
var nodeResults []nodeSummary
|
||||
// Build nodeResults based on error values received during
|
||||
// set-config operation.
|
||||
for i := range errs {
|
||||
nodeResults = append(nodeResults, nodeSummary{
|
||||
Name: peers[i].addr,
|
||||
ErrSet: errs[i] != nil,
|
||||
ErrMsg: fmt.Sprintf("%v", errs[i]),
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
result := setConfigResult{
|
||||
Status: status,
|
||||
NodeResults: nodeResults,
|
||||
}
|
||||
|
||||
// The following elaborate json encoding is to avoid escaping
|
||||
// '<', '>' in <nil>. Note: json.Encoder.Encode() adds a
|
||||
// gratuitous "\n".
|
||||
var resultBuf bytes.Buffer
|
||||
enc := json.NewEncoder(&resultBuf)
|
||||
enc.SetEscapeHTML(false)
|
||||
jsonErr := enc.Encode(result)
|
||||
if jsonErr != nil {
|
||||
writeErrorResponseJSON(w, toAPIErrorCode(jsonErr), reqURL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, resultBuf.Bytes())
|
||||
return
|
||||
}
|
||||
|
||||
// SetConfigHandler - PUT /minio/admin/v1/config
|
||||
func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "SetConfigHandler")
|
||||
|
||||
ctx := context.Background()
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
@@ -671,12 +589,6 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request signature.
|
||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
@@ -684,6 +596,12 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Read configuration bytes from request body.
|
||||
configBuf := make([]byte, maxConfigJSONSize+1)
|
||||
n, err := io.ReadFull(r.Body, configBuf)
|
||||
@@ -698,7 +616,13 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
configBytes := configBuf[:n]
|
||||
password := globalServerConfig.GetCredential().SecretKey
|
||||
configBytes, err := madmin.DecryptServerConfigData(password, bytes.NewReader(configBuf[:n]))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate JSON provided in the request body: check the
|
||||
// client has not sent JSON objects with duplicate keys.
|
||||
@@ -727,58 +651,178 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
if err = config.Validate(); err != nil {
|
||||
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write config received from request onto a temporary file on
|
||||
// all nodes.
|
||||
tmpFileName := fmt.Sprintf(minioConfigTmpFormat, mustGetUUID())
|
||||
errs := writeTmpConfigPeers(globalAdminPeers, tmpFileName, configBytes)
|
||||
|
||||
// Check if the operation succeeded in quorum or more nodes.
|
||||
rErr := reduceWriteQuorumErrs(ctx, errs, nil, len(globalAdminPeers)/2+1)
|
||||
if rErr != nil {
|
||||
writeSetConfigResponse(w, globalAdminPeers, errs, false, r.URL)
|
||||
if err = saveServerConfig(ctx, objectAPI, &config); err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Take a lock on minio/config.json. NB minio is a reserved
|
||||
// bucket name and wouldn't conflict with normal object
|
||||
// operations.
|
||||
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
|
||||
if configLock.GetLock(globalObjectTimeout) != nil {
|
||||
writeErrorResponseJSON(w, ErrOperationTimedOut, r.URL)
|
||||
return
|
||||
}
|
||||
defer configLock.Unlock()
|
||||
// Reply to the client before restarting minio server.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
|
||||
// Rename the temporary config file to config.json
|
||||
errs = commitConfigPeers(globalAdminPeers, tmpFileName)
|
||||
rErr = reduceWriteQuorumErrs(ctx, errs, nil, len(globalAdminPeers)/2+1)
|
||||
if rErr != nil {
|
||||
writeSetConfigResponse(w, globalAdminPeers, errs, false, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// serverMux (cmd/server-mux.go) implements graceful shutdown,
|
||||
// where all listeners are closed and process restart/shutdown
|
||||
// happens after 5s or completion of all ongoing http
|
||||
// requests, whichever is earlier.
|
||||
writeSetConfigResponse(w, globalAdminPeers, errs, true, r.URL)
|
||||
|
||||
// Restart all node for the modified config to take effect.
|
||||
sendServiceCmd(globalAdminPeers, serviceRestart)
|
||||
}
|
||||
|
||||
// ConfigCredsHandler - POST /minio/admin/v1/config/credential
|
||||
func convertValueType(elem []byte, jsonType gjson.Type) (interface{}, error) {
|
||||
str := string(elem)
|
||||
switch jsonType {
|
||||
case gjson.False, gjson.True:
|
||||
return strconv.ParseBool(str)
|
||||
case gjson.JSON:
|
||||
return gjson.Parse(str).Value(), nil
|
||||
case gjson.String:
|
||||
return str, nil
|
||||
case gjson.Number:
|
||||
return strconv.ParseFloat(str, 64)
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfigKeysHandler - PUT /minio/admin/v1/config-keys
|
||||
func (a adminAPIHandlers) SetConfigKeysHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "SetConfigKeysHandler")
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request signature.
|
||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Load config
|
||||
configStruct, err := readServerConfig(ctx, objectAPI)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert config to json bytes
|
||||
configBytes, err := json.Marshal(configStruct)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
configStr := string(configBytes)
|
||||
|
||||
queries := r.URL.Query()
|
||||
password := globalServerConfig.GetCredential().SecretKey
|
||||
|
||||
// Set key values in the JSON config
|
||||
for k := range queries {
|
||||
// Decode encrypted data associated to the current key
|
||||
encryptedElem, dErr := base64.StdEncoding.DecodeString(queries.Get(k))
|
||||
if dErr != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("key", k)
|
||||
ctx = logger.SetReqInfo(ctx, reqInfo)
|
||||
logger.LogIf(ctx, dErr)
|
||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||
return
|
||||
}
|
||||
elem, dErr := madmin.DecryptServerConfigData(password, bytes.NewBuffer([]byte(encryptedElem)))
|
||||
if dErr != nil {
|
||||
logger.LogIf(ctx, dErr)
|
||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||
return
|
||||
}
|
||||
// Calculate the type of the current key from the
|
||||
// original config json
|
||||
jsonFieldType := gjson.Get(configStr, k).Type
|
||||
// Convert passed value to json filed type
|
||||
val, cErr := convertValueType(elem, jsonFieldType)
|
||||
if cErr != nil {
|
||||
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, cErr.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
// Set the key/value in the new json document
|
||||
if s, sErr := sjson.Set(configStr, normalizeJSONKey(k), val); sErr == nil {
|
||||
configStr = s
|
||||
}
|
||||
}
|
||||
|
||||
configBytes = []byte(configStr)
|
||||
|
||||
// Validate config
|
||||
var config serverConfig
|
||||
if err = json.Unmarshal(configBytes, &config); err != nil {
|
||||
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = config.Validate(); err != nil {
|
||||
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = config.TestNotificationTargets(); err != nil {
|
||||
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// If credentials for the server are provided via environment,
|
||||
// then credentials in the provided configuration must match.
|
||||
if globalIsEnvCreds {
|
||||
creds := globalServerConfig.GetCredential()
|
||||
if config.Credential.AccessKey != creds.AccessKey ||
|
||||
config.Credential.SecretKey != creds.SecretKey {
|
||||
writeErrorResponseJSON(w, ErrAdminCredentialsMismatch, r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = saveServerConfig(ctx, objectAPI, &config); err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Send success response
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
|
||||
sendServiceCmd(globalAdminPeers, serviceRestart)
|
||||
}
|
||||
|
||||
// UpdateCredsHandler - POST /minio/admin/v1/config/credential
|
||||
// ----------
|
||||
// Update credentials in a minio server. In a distributed setup,
|
||||
// update all the servers in the cluster.
|
||||
func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
||||
r *http.Request) {
|
||||
|
||||
ctx := newContext(r, w, "UpdateCredentialsHandler")
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Avoid setting new credentials when they are already passed
|
||||
// by the environment. Deny if WORM is enabled.
|
||||
if globalIsEnvCreds {
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate request
|
||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
@@ -786,18 +830,32 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
// Avoid setting new credentials when they are already passed
|
||||
// by the environment. Deny if WORM is enabled.
|
||||
if globalIsEnvCreds || globalWORMEnabled {
|
||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||
// Read configuration bytes from request body.
|
||||
configBuf := make([]byte, maxConfigJSONSize+1)
|
||||
n, err := io.ReadFull(r.Body, configBuf)
|
||||
if err == nil {
|
||||
// More than maxConfigSize bytes were available
|
||||
writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL)
|
||||
return
|
||||
}
|
||||
if err != io.ErrUnexpectedEOF {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
password := globalServerConfig.GetCredential().SecretKey
|
||||
configBytes, err := madmin.DecryptServerConfigData(password, bytes.NewReader(configBuf[:n]))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Decode request body
|
||||
var req madmin.SetCredsReq
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
if err = json.Unmarshal(configBytes, &req); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, ErrRequestBodyParse, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -808,33 +866,26 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
// Take a lock on minio/config.json. Prevents concurrent
|
||||
// config file updates.
|
||||
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
|
||||
if configLock.GetLock(globalObjectTimeout) != nil {
|
||||
writeErrorResponseJSON(w, ErrOperationTimedOut, r.URL)
|
||||
return
|
||||
}
|
||||
defer configLock.Unlock()
|
||||
|
||||
// Acquire lock before updating global configuration.
|
||||
globalServerConfigMu.Lock()
|
||||
defer globalServerConfigMu.Unlock()
|
||||
|
||||
// Update local credentials in memory.
|
||||
globalServerConfig.SetCredential(creds)
|
||||
if err = globalServerConfig.Save(getConfigFile()); err != nil {
|
||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
||||
|
||||
if err = saveServerConfig(ctx, objectAPI, globalServerConfig); err != nil {
|
||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to update credentials
|
||||
for host, err := range globalNotificationSys.SetCredentials(creds) {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", host.String())
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
for host, err := range globalNotificationSys.LoadCredentials() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// At this stage, the operation is successful, return 200 OK
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// Reply to the client before restarting minio server.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
+54
-291
@@ -26,7 +26,6 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -38,7 +37,7 @@ import (
|
||||
|
||||
var (
|
||||
configJSON = []byte(`{
|
||||
"version": "27",
|
||||
"version": "28",
|
||||
"credential": {
|
||||
"accessKey": "minio",
|
||||
"secretKey": "minio123"
|
||||
@@ -57,6 +56,22 @@ var (
|
||||
"maxuse": 80,
|
||||
"exclude": []
|
||||
},
|
||||
"kms": {
|
||||
"vault": {
|
||||
"endpoint": "",
|
||||
"auth": {
|
||||
"type": "",
|
||||
"approle": {
|
||||
"id": "",
|
||||
"secret": ""
|
||||
}
|
||||
},
|
||||
"key-id": {
|
||||
"name": "",
|
||||
"version": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"amqp": {
|
||||
"1": {
|
||||
@@ -86,7 +101,17 @@ var (
|
||||
"1": {
|
||||
"enable": false,
|
||||
"brokers": null,
|
||||
"topic": ""
|
||||
"topic": "",
|
||||
"tls" : {
|
||||
"enable" : false,
|
||||
"skipVerify" : false,
|
||||
"clientAuth" : 0
|
||||
},
|
||||
"sasl" : {
|
||||
"enable" : false,
|
||||
"username" : "",
|
||||
"password" : ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"mqtt": {
|
||||
@@ -193,17 +218,17 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
||||
// reset global variables to start afresh.
|
||||
resetTestGlobals()
|
||||
|
||||
// Initialize minio server config.
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Initializing objectLayer for HealFormatHandler.
|
||||
objLayer, xlDirs, xlErr := initTestXLObjLayer()
|
||||
if xlErr != nil {
|
||||
return nil, xlErr
|
||||
}
|
||||
|
||||
// Initialize minio server config.
|
||||
if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize boot time
|
||||
globalBootTime = UTCNow()
|
||||
|
||||
@@ -230,17 +255,15 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
||||
registerAdminRouter(adminRouter)
|
||||
|
||||
return &adminXLTestBed{
|
||||
configPath: rootPath,
|
||||
xlDirs: xlDirs,
|
||||
objLayer: objLayer,
|
||||
router: adminRouter,
|
||||
xlDirs: xlDirs,
|
||||
objLayer: objLayer,
|
||||
router: adminRouter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TearDown - method that resets the test bed for subsequent unit
|
||||
// tests to start afresh.
|
||||
func (atb *adminXLTestBed) TearDown() {
|
||||
os.RemoveAll(atb.configPath)
|
||||
removeRoots(atb.xlDirs)
|
||||
resetTestGlobals()
|
||||
}
|
||||
@@ -261,7 +284,7 @@ func (atb *adminXLTestBed) GenerateHealTestData(t *testing.T) {
|
||||
objectName := fmt.Sprintf("%s-%d", objName, i)
|
||||
_, err = atb.objLayer.PutObject(context.Background(), bucketName, objectName,
|
||||
mustGetHashReader(t, bytes.NewReader([]byte("hello")),
|
||||
int64(len("hello")), "", ""), nil)
|
||||
int64(len("hello")), "", ""), nil, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create %s - %v", objectName,
|
||||
err)
|
||||
@@ -273,14 +296,14 @@ func (atb *adminXLTestBed) GenerateHealTestData(t *testing.T) {
|
||||
{
|
||||
objName := "mpObject"
|
||||
uploadID, err := atb.objLayer.NewMultipartUpload(context.Background(), bucketName,
|
||||
objName, nil)
|
||||
objName, nil, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("mp new error: %v", err)
|
||||
}
|
||||
|
||||
_, err = atb.objLayer.PutObjectPart(context.Background(), bucketName, objName,
|
||||
uploadID, 3, mustGetHashReader(t, bytes.NewReader(
|
||||
[]byte("hello")), int64(len("hello")), "", ""))
|
||||
[]byte("hello")), int64(len("hello")), "", ""), ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("mp put error: %v", err)
|
||||
}
|
||||
@@ -579,8 +602,14 @@ func TestServiceSetCreds(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("JSONify err: %v", err)
|
||||
}
|
||||
|
||||
ebody, err := madmin.EncryptServerConfigData(credentials.SecretKey, body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Construct setCreds request
|
||||
req, err := getServiceCmdRequest(setCreds, credentials, body)
|
||||
req, err := getServiceCmdRequest(setCreds, credentials, ebody)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build service status request %v", err)
|
||||
}
|
||||
@@ -611,193 +640,6 @@ func TestServiceSetCreds(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// mkLockQueryVal - helper function to build lock query param.
|
||||
func mkLockQueryVal(bucket, prefix, durationStr string) url.Values {
|
||||
qVal := url.Values{}
|
||||
qVal.Set(string(mgmtBucket), bucket)
|
||||
qVal.Set(string(mgmtPrefix), prefix)
|
||||
qVal.Set(string(mgmtLockOlderThan), durationStr)
|
||||
return qVal
|
||||
}
|
||||
|
||||
// Test for locks list management REST API.
|
||||
func TestListLocksHandler(t *testing.T) {
|
||||
adminTestBed, err := prepareAdminXLTestBed()
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
||||
}
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
testCases := []struct {
|
||||
bucket string
|
||||
prefix string
|
||||
duration string
|
||||
expectedStatus int
|
||||
}{
|
||||
// Test 1 - valid testcase
|
||||
{
|
||||
bucket: "mybucket",
|
||||
prefix: "myobject",
|
||||
duration: "1s",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
// Test 2 - invalid duration
|
||||
{
|
||||
bucket: "mybucket",
|
||||
prefix: "myprefix",
|
||||
duration: "invalidDuration",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
// Test 3 - invalid bucket name
|
||||
{
|
||||
bucket: `invalid\\Bucket`,
|
||||
prefix: "myprefix",
|
||||
duration: "1h",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
// Test 4 - invalid prefix
|
||||
{
|
||||
bucket: "mybucket",
|
||||
prefix: `invalid\\Prefix`,
|
||||
duration: "1h",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
queryVal := mkLockQueryVal(test.bucket, test.prefix, test.duration)
|
||||
req, err := newTestRequest("GET", "/minio/admin/v1/locks?"+queryVal.Encode(), 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d - Failed to construct list locks request - %v", i+1, err)
|
||||
}
|
||||
|
||||
cred := globalServerConfig.GetCredential()
|
||||
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d - Failed to sign list locks request - %v", i+1, err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
adminTestBed.router.ServeHTTP(rec, req)
|
||||
if test.expectedStatus != rec.Code {
|
||||
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.expectedStatus, rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test for locks clear management REST API.
|
||||
func TestClearLocksHandler(t *testing.T) {
|
||||
adminTestBed, err := prepareAdminXLTestBed()
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
||||
}
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
||||
|
||||
testCases := []struct {
|
||||
bucket string
|
||||
prefix string
|
||||
duration string
|
||||
expectedStatus int
|
||||
}{
|
||||
// Test 1 - valid testcase
|
||||
{
|
||||
bucket: "mybucket",
|
||||
prefix: "myobject",
|
||||
duration: "1s",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
// Test 2 - invalid duration
|
||||
{
|
||||
bucket: "mybucket",
|
||||
prefix: "myprefix",
|
||||
duration: "invalidDuration",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
// Test 3 - invalid bucket name
|
||||
{
|
||||
bucket: `invalid\\Bucket`,
|
||||
prefix: "myprefix",
|
||||
duration: "1h",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
// Test 4 - invalid prefix
|
||||
{
|
||||
bucket: "mybucket",
|
||||
prefix: `invalid\\Prefix`,
|
||||
duration: "1h",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
queryVal := mkLockQueryVal(test.bucket, test.prefix, test.duration)
|
||||
req, err := newTestRequest("DELETE", "/minio/admin/v1/locks?"+queryVal.Encode(), 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d - Failed to construct clear locks request - %v", i+1, err)
|
||||
}
|
||||
|
||||
cred := globalServerConfig.GetCredential()
|
||||
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d - Failed to sign clear locks request - %v", i+1, err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
adminTestBed.router.ServeHTTP(rec, req)
|
||||
if test.expectedStatus != rec.Code {
|
||||
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.expectedStatus, rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test for lock query param validation helper function.
|
||||
func TestValidateLockQueryParams(t *testing.T) {
|
||||
// reset globals.
|
||||
// this is to make sure that the tests are not affected by modified globals.
|
||||
resetTestGlobals()
|
||||
// initialize NSLock.
|
||||
initNSLock(false)
|
||||
// Sample query values for test cases.
|
||||
allValidVal := mkLockQueryVal("bucket", "prefix", "1s")
|
||||
invalidBucketVal := mkLockQueryVal(`invalid\\Bucket`, "prefix", "1s")
|
||||
invalidPrefixVal := mkLockQueryVal("bucket", `invalid\\Prefix`, "1s")
|
||||
invalidOlderThanVal := mkLockQueryVal("bucket", "prefix", "invalidDuration")
|
||||
|
||||
testCases := []struct {
|
||||
qVals url.Values
|
||||
apiErr APIErrorCode
|
||||
}{
|
||||
{
|
||||
qVals: invalidBucketVal,
|
||||
apiErr: ErrInvalidBucketName,
|
||||
},
|
||||
{
|
||||
qVals: invalidPrefixVal,
|
||||
apiErr: ErrInvalidObjectName,
|
||||
},
|
||||
{
|
||||
qVals: invalidOlderThanVal,
|
||||
apiErr: ErrInvalidDuration,
|
||||
},
|
||||
{
|
||||
qVals: allValidVal,
|
||||
apiErr: ErrNone,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
_, _, _, apiErr := validateLockQueryParams(test.qVals)
|
||||
if apiErr != test.apiErr {
|
||||
t.Errorf("Test %d - Expected error %v but received %v", i+1, test.apiErr, apiErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildAdminRequest - helper function to build an admin API request.
|
||||
func buildAdminRequest(queryVal url.Values, method, path string,
|
||||
contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error) {
|
||||
@@ -867,8 +709,14 @@ func TestSetConfigHandler(t *testing.T) {
|
||||
queryVal := url.Values{}
|
||||
queryVal.Set("config", "")
|
||||
|
||||
password := globalServerConfig.GetCredential().SecretKey
|
||||
econfigJSON, err := madmin.EncryptServerConfigData(password, configJSON)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
||||
int64(len(configJSON)), bytes.NewReader(configJSON))
|
||||
int64(len(econfigJSON)), bytes.NewReader(econfigJSON))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct set-config object request - %v", err)
|
||||
}
|
||||
@@ -879,16 +727,6 @@ func TestSetConfigHandler(t *testing.T) {
|
||||
t.Errorf("Expected to succeed but failed with %d", rec.Code)
|
||||
}
|
||||
|
||||
result := setConfigResult{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&result)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decode set config result json %v", err)
|
||||
}
|
||||
|
||||
if !result.Status {
|
||||
t.Error("Expected set-config to succeed, but failed")
|
||||
}
|
||||
|
||||
// Check that a very large config file returns an error.
|
||||
{
|
||||
// Make a large enough config string
|
||||
@@ -911,7 +749,7 @@ func TestSetConfigHandler(t *testing.T) {
|
||||
// Check that a config with duplicate keys in an object return
|
||||
// error.
|
||||
{
|
||||
invalidCfg := append(configJSON[:len(configJSON)-1], []byte(`, "version": "15"}`)...)
|
||||
invalidCfg := append(econfigJSON[:len(econfigJSON)-1], []byte(`, "version": "15"}`)...)
|
||||
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
||||
int64(len(invalidCfg)), bytes.NewReader(invalidCfg))
|
||||
if err != nil {
|
||||
@@ -1009,81 +847,6 @@ func TestToAdminAPIErr(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSetConfigResponse(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
testCases := []struct {
|
||||
status bool
|
||||
errs []error
|
||||
}{
|
||||
// 1. all nodes returned success.
|
||||
{
|
||||
status: true,
|
||||
errs: []error{nil, nil, nil, nil},
|
||||
},
|
||||
// 2. some nodes returned errors.
|
||||
{
|
||||
status: false,
|
||||
errs: []error{errDiskNotFound, nil, errDiskAccessDenied, errFaultyDisk},
|
||||
},
|
||||
}
|
||||
|
||||
testPeers := []adminPeer{
|
||||
{
|
||||
addr: "localhost:9001",
|
||||
},
|
||||
{
|
||||
addr: "localhost:9002",
|
||||
},
|
||||
{
|
||||
addr: "localhost:9003",
|
||||
},
|
||||
{
|
||||
addr: "localhost:9004",
|
||||
},
|
||||
}
|
||||
|
||||
testURL, err := url.Parse("http://dummy.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse a place-holder url")
|
||||
}
|
||||
|
||||
var actualResult setConfigResult
|
||||
for i, test := range testCases {
|
||||
rec := httptest.NewRecorder()
|
||||
writeSetConfigResponse(rec, testPeers, test.errs, test.status, testURL)
|
||||
resp := rec.Result()
|
||||
jsonBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to read response %v", i+1, err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(jsonBytes, &actualResult)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to unmarshal json %v", i+1, err)
|
||||
}
|
||||
if actualResult.Status != test.status {
|
||||
t.Errorf("Test %d: Expected status %v but received %v", i+1, test.status, actualResult.Status)
|
||||
}
|
||||
for p, res := range actualResult.NodeResults {
|
||||
if res.Name != testPeers[p].addr {
|
||||
t.Errorf("Test %d: Expected node name %s but received %s", i+1, testPeers[p].addr, res.Name)
|
||||
}
|
||||
expectedErrMsg := fmt.Sprintf("%v", test.errs[p])
|
||||
if res.ErrMsg != expectedErrMsg {
|
||||
t.Errorf("Test %d: Expected error %s but received %s", i+1, expectedErrMsg, res.ErrMsg)
|
||||
}
|
||||
expectedErrSet := test.errs[p] != nil
|
||||
if res.ErrSet != expectedErrSet {
|
||||
t.Errorf("Test %d: Expected ErrSet %v but received %v", i+1, expectedErrSet, res.ErrSet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mkHealStartReq(t *testing.T, bucket, prefix string,
|
||||
opts madmin.HealOpts) *http.Request {
|
||||
|
||||
|
||||
@@ -610,7 +610,7 @@ func (h *healSequence) healBucket(bucket string) error {
|
||||
if h.objPrefix != "" {
|
||||
// Check if an object named as the objPrefix exists,
|
||||
// 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 {
|
||||
err = h.healObject(bucket, h.objPrefix)
|
||||
if err != nil {
|
||||
|
||||
+8
-10
@@ -53,13 +53,6 @@ func registerAdminRouter(router *mux.Router) {
|
||||
// Info operations
|
||||
adminV1Router.Methods(http.MethodGet).Path("/info").HandlerFunc(httpTraceAll(adminAPI.ServerInfoHandler))
|
||||
|
||||
/// Lock operations
|
||||
|
||||
// List Locks
|
||||
adminV1Router.Methods(http.MethodGet).Path("/locks").HandlerFunc(httpTraceAll(adminAPI.ListLocksHandler))
|
||||
// Clear locks
|
||||
adminV1Router.Methods(http.MethodDelete).Path("/locks").HandlerFunc(httpTraceAll(adminAPI.ClearLocksHandler))
|
||||
|
||||
/// Heal operations
|
||||
|
||||
// Heal processing endpoint.
|
||||
@@ -70,9 +63,14 @@ func registerAdminRouter(router *mux.Router) {
|
||||
/// Config operations
|
||||
|
||||
// Update credentials
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceAll(adminAPI.UpdateCredentialsHandler))
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateCredentialsHandler))
|
||||
// Get config
|
||||
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceAll(adminAPI.GetConfigHandler))
|
||||
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
||||
// Set config
|
||||
adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(httpTraceAll(adminAPI.SetConfigHandler))
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
@@ -54,19 +53,6 @@ func (rpcClient *AdminRPCClient) ReInitFormat(dryRun bool) error {
|
||||
return rpcClient.Call(adminServiceName+".ReInitFormat", &args, &reply)
|
||||
}
|
||||
|
||||
// ListLocks - Sends list locks command to remote server via RPC.
|
||||
func (rpcClient *AdminRPCClient) ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
|
||||
args := ListLocksQuery{
|
||||
Bucket: bucket,
|
||||
Prefix: prefix,
|
||||
Duration: duration,
|
||||
}
|
||||
var reply []VolumeLockInfo
|
||||
|
||||
err := rpcClient.Call(adminServiceName+".ListLocks", &args, &reply)
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// ServerInfo - returns the server info of the server to which the RPC call is made.
|
||||
func (rpcClient *AdminRPCClient) ServerInfo() (sid ServerInfoData, err error) {
|
||||
err = rpcClient.Call(adminServiceName+".ServerInfo", &AuthArgs{}, &sid)
|
||||
@@ -82,29 +68,6 @@ func (rpcClient *AdminRPCClient) GetConfig() ([]byte, error) {
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// WriteTmpConfig - writes config file content to a temporary file on a remote node.
|
||||
func (rpcClient *AdminRPCClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
|
||||
args := WriteConfigArgs{
|
||||
TmpFileName: tmpFileName,
|
||||
Buf: configBytes,
|
||||
}
|
||||
reply := VoidReply{}
|
||||
|
||||
err := rpcClient.Call(adminServiceName+".WriteTmpConfig", &args, &reply)
|
||||
logger.LogIf(context.Background(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
// CommitConfig - Move the new config in tmpFileName onto config.json on a remote node.
|
||||
func (rpcClient *AdminRPCClient) CommitConfig(tmpFileName string) error {
|
||||
args := CommitConfigArgs{FileName: tmpFileName}
|
||||
reply := VoidReply{}
|
||||
|
||||
err := rpcClient.Call(adminServiceName+".CommitConfig", &args, &reply)
|
||||
logger.LogIf(context.Background(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
// NewAdminRPCClient - returns new admin RPC client.
|
||||
func NewAdminRPCClient(host *xnet.Host) (*AdminRPCClient, error) {
|
||||
scheme := "http"
|
||||
@@ -147,11 +110,8 @@ func NewAdminRPCClient(host *xnet.Host) (*AdminRPCClient, error) {
|
||||
type adminCmdRunner interface {
|
||||
SignalService(s serviceSignal) error
|
||||
ReInitFormat(dryRun bool) error
|
||||
ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error)
|
||||
ServerInfo() (ServerInfoData, error)
|
||||
GetConfig() ([]byte, error)
|
||||
WriteTmpConfig(tmpFileName string, configBytes []byte) error
|
||||
CommitConfig(tmpFileName string) error
|
||||
}
|
||||
|
||||
// adminPeer - represents an entity that implements admin API RPCs.
|
||||
@@ -244,56 +204,6 @@ func sendServiceCmd(cps adminPeers, cmd serviceSignal) {
|
||||
errs[0] = invokeServiceCmd(cps[0], cmd)
|
||||
}
|
||||
|
||||
// listPeerLocksInfo - fetch list of locks held on the given bucket,
|
||||
// matching prefix held longer than duration from all peer servers.
|
||||
func listPeerLocksInfo(peers adminPeers, bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
|
||||
// Used to aggregate volume lock information from all nodes.
|
||||
allLocks := make([][]VolumeLockInfo, len(peers))
|
||||
errs := make([]error, len(peers))
|
||||
var wg sync.WaitGroup
|
||||
localPeer := peers[0]
|
||||
remotePeers := peers[1:]
|
||||
for i, remotePeer := range remotePeers {
|
||||
wg.Add(1)
|
||||
go func(idx int, remotePeer adminPeer) {
|
||||
defer wg.Done()
|
||||
// `remotePeers` is right-shifted by one position relative to `peers`
|
||||
allLocks[idx], errs[idx] = remotePeer.cmdRunner.ListLocks(bucket, prefix, duration)
|
||||
}(i+1, remotePeer)
|
||||
}
|
||||
wg.Wait()
|
||||
allLocks[0], errs[0] = localPeer.cmdRunner.ListLocks(bucket, prefix, duration)
|
||||
|
||||
// Summarizing errors received for ListLocks RPC across all
|
||||
// nodes. N B the possible unavailability of quorum in errors
|
||||
// applies only to distributed setup.
|
||||
errCount, err := reduceErrs(errs, []error{})
|
||||
if err != nil {
|
||||
if errCount >= (len(peers)/2 + 1) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, InsufficientReadQuorum{}
|
||||
}
|
||||
|
||||
// Group lock information across nodes by (bucket, object)
|
||||
// pair. For readability only.
|
||||
paramLockMap := make(map[nsParam][]VolumeLockInfo)
|
||||
for _, nodeLocks := range allLocks {
|
||||
for _, lockInfo := range nodeLocks {
|
||||
param := nsParam{
|
||||
volume: lockInfo.Bucket,
|
||||
path: lockInfo.Object,
|
||||
}
|
||||
paramLockMap[param] = append(paramLockMap[param], lockInfo)
|
||||
}
|
||||
}
|
||||
groupedLockInfos := []VolumeLockInfo{}
|
||||
for _, volLocks := range paramLockMap {
|
||||
groupedLockInfos = append(groupedLockInfos, volLocks...)
|
||||
}
|
||||
return groupedLockInfos, nil
|
||||
}
|
||||
|
||||
// uptimeSlice - used to sort uptimes in chronological order.
|
||||
type uptimeSlice []struct {
|
||||
err error
|
||||
@@ -365,187 +275,3 @@ func getPeerUptimes(peers adminPeers) (time.Duration, error) {
|
||||
|
||||
return latestUptime, nil
|
||||
}
|
||||
|
||||
// getPeerConfig - Fetches config.json from all nodes in the setup and
|
||||
// returns the one that occurs in a majority of them.
|
||||
func getPeerConfig(peers adminPeers) ([]byte, error) {
|
||||
if !globalIsDistXL {
|
||||
return peers[0].cmdRunner.GetConfig()
|
||||
}
|
||||
|
||||
errs := make([]error, len(peers))
|
||||
configs := make([][]byte, len(peers))
|
||||
|
||||
// Get config from all servers.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
configs[idx], errs[idx] = peer.cmdRunner.GetConfig()
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Find the maximally occurring config among peers in a
|
||||
// distributed setup.
|
||||
|
||||
serverConfigs := make([]serverConfig, len(peers))
|
||||
for i, configBytes := range configs {
|
||||
if errs[i] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Unmarshal the received config files.
|
||||
err := json.Unmarshal(configBytes, &serverConfigs[i])
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", peers[i].addr)
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
configJSON, err := getValidServerConfig(serverConfigs, errs)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return the config.json that was present quorum or more
|
||||
// number of disks.
|
||||
return json.Marshal(configJSON)
|
||||
}
|
||||
|
||||
// getValidServerConfig - finds the server config that is present in
|
||||
// quorum or more number of servers.
|
||||
func getValidServerConfig(serverConfigs []serverConfig, errs []error) (scv serverConfig, e error) {
|
||||
// majority-based quorum
|
||||
quorum := len(serverConfigs)/2 + 1
|
||||
|
||||
// Count the number of disks a config.json was found in.
|
||||
configCounter := make([]int, len(serverConfigs))
|
||||
|
||||
// We group equal serverConfigs by the lowest index of the
|
||||
// same value; e.g, let us take the following serverConfigs
|
||||
// in a 4-node setup,
|
||||
// serverConfigs == [c1, c2, c1, c1]
|
||||
// configCounter == [3, 1, 0, 0]
|
||||
// c1, c2 are the only distinct values that appear. c1 is
|
||||
// identified by 0, the lowest index it appears in and c2 is
|
||||
// identified by 1. So, we need to find the number of times
|
||||
// each of these distinct values occur.
|
||||
|
||||
// Invariants:
|
||||
|
||||
// 1. At the beginning of the i-th iteration, the number of
|
||||
// unique configurations seen so far is equal to the number of
|
||||
// non-zero counter values in config[:i].
|
||||
|
||||
// 2. At the beginning of the i-th iteration, the sum of
|
||||
// elements of configCounter[:i] is equal to the number of
|
||||
// non-error configurations seen so far.
|
||||
|
||||
// For each of the serverConfig ...
|
||||
for i := range serverConfigs {
|
||||
// Skip nodes where getConfig failed.
|
||||
if errs[i] != nil {
|
||||
continue
|
||||
}
|
||||
// Check if it is equal to any of the configurations
|
||||
// seen so far. If j == i is reached then we have an
|
||||
// unseen configuration.
|
||||
for j := 0; j <= i; j++ {
|
||||
if j < i && configCounter[j] == 0 {
|
||||
// serverConfigs[j] is known to be
|
||||
// equal to a value that was already
|
||||
// seen. See example above for
|
||||
// clarity.
|
||||
continue
|
||||
} else if j < i && serverConfigs[i].ConfigDiff(&serverConfigs[j]) == "" {
|
||||
// serverConfigs[i] is equal to
|
||||
// serverConfigs[j], update
|
||||
// serverConfigs[j]'s counter since it
|
||||
// is the lower index.
|
||||
configCounter[j]++
|
||||
break
|
||||
} else if j == i {
|
||||
// serverConfigs[i] is equal to no
|
||||
// other value seen before. It is
|
||||
// unique so far.
|
||||
configCounter[i] = 1
|
||||
break
|
||||
} // else invariants specified above are violated.
|
||||
}
|
||||
}
|
||||
|
||||
// We find the maximally occurring server config and check if
|
||||
// there is quorum.
|
||||
var configJSON serverConfig
|
||||
maxOccurrence := 0
|
||||
for i, count := range configCounter {
|
||||
if maxOccurrence < count {
|
||||
maxOccurrence = count
|
||||
configJSON = serverConfigs[i]
|
||||
}
|
||||
}
|
||||
|
||||
// If quorum nodes don't agree.
|
||||
if maxOccurrence < quorum {
|
||||
return scv, errXLWriteQuorum
|
||||
}
|
||||
|
||||
return configJSON, nil
|
||||
}
|
||||
|
||||
// Write config contents into a temporary file on all nodes.
|
||||
func writeTmpConfigPeers(peers adminPeers, tmpFileName string, configBytes []byte) []error {
|
||||
// For a single-node minio server setup.
|
||||
if !globalIsDistXL {
|
||||
err := peers[0].cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
errs := make([]error, len(peers))
|
||||
|
||||
// Write config into temporary file on all nodes.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
errs[idx] = peer.cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Return bytes written and errors (if any) during writing
|
||||
// temporary config file.
|
||||
return errs
|
||||
}
|
||||
|
||||
// Move config contents from the given temporary file onto config.json
|
||||
// on all nodes.
|
||||
func commitConfigPeers(peers adminPeers, tmpFileName string) []error {
|
||||
// For a single-node minio server setup.
|
||||
if !globalIsDistXL {
|
||||
return []error{peers[0].cmdRunner.CommitConfig(tmpFileName)}
|
||||
}
|
||||
|
||||
errs := make([]error, len(peers))
|
||||
|
||||
// Rename temporary config file into configDir/config.json on
|
||||
// all nodes.
|
||||
wg := sync.WaitGroup{}
|
||||
for i, peer := range peers {
|
||||
wg.Add(1)
|
||||
go func(idx int, peer adminPeer) {
|
||||
defer wg.Done()
|
||||
errs[idx] = peer.cmdRunner.CommitConfig(tmpFileName)
|
||||
}(i, peer)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Return errors (if any) received during rename.
|
||||
return errs
|
||||
}
|
||||
|
||||
+1
-41
@@ -19,7 +19,6 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
@@ -47,20 +46,6 @@ func (receiver *adminRPCReceiver) SignalService(args *SignalServiceArgs, reply *
|
||||
return receiver.local.SignalService(args.Sig)
|
||||
}
|
||||
|
||||
// ListLocksQuery - wraps ListLocks API's query values to send over RPC.
|
||||
type ListLocksQuery struct {
|
||||
AuthArgs
|
||||
Bucket string
|
||||
Prefix string
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// ListLocks - lists locks held by requests handled by this server instance.
|
||||
func (receiver *adminRPCReceiver) ListLocks(args *ListLocksQuery, reply *[]VolumeLockInfo) (err error) {
|
||||
*reply, err = receiver.local.ListLocks(args.Bucket, args.Prefix, args.Duration)
|
||||
return err
|
||||
}
|
||||
|
||||
// ServerInfo - returns the server info when object layer was initialized on this server.
|
||||
func (receiver *adminRPCReceiver) ServerInfo(args *AuthArgs, reply *ServerInfoData) (err error) {
|
||||
*reply, err = receiver.local.ServerInfo()
|
||||
@@ -84,31 +69,6 @@ func (receiver *adminRPCReceiver) ReInitFormat(args *ReInitFormatArgs, reply *Vo
|
||||
return receiver.local.ReInitFormat(args.DryRun)
|
||||
}
|
||||
|
||||
// WriteConfigArgs - wraps the bytes to be written and temporary file name.
|
||||
type WriteConfigArgs struct {
|
||||
AuthArgs
|
||||
TmpFileName string
|
||||
Buf []byte
|
||||
}
|
||||
|
||||
// WriteTmpConfig - writes the supplied config contents onto the
|
||||
// supplied temporary file.
|
||||
func (receiver *adminRPCReceiver) WriteTmpConfig(args *WriteConfigArgs, reply *VoidReply) error {
|
||||
return receiver.local.WriteTmpConfig(args.TmpFileName, args.Buf)
|
||||
}
|
||||
|
||||
// CommitConfigArgs - wraps the config file name that needs to be
|
||||
// committed into config.json on this node.
|
||||
type CommitConfigArgs struct {
|
||||
AuthArgs
|
||||
FileName string
|
||||
}
|
||||
|
||||
// CommitConfig - Renames the temporary file into config.json on this node.
|
||||
func (receiver *adminRPCReceiver) CommitConfig(args *CommitConfigArgs, reply *VoidReply) error {
|
||||
return receiver.local.CommitConfig(args.FileName)
|
||||
}
|
||||
|
||||
// NewAdminRPCServer - returns new admin RPC server.
|
||||
func NewAdminRPCServer() (*xrpc.Server, error) {
|
||||
rpcServer := xrpc.NewServer()
|
||||
@@ -123,5 +83,5 @@ func registerAdminRPCRouter(router *mux.Router) {
|
||||
rpcServer, err := NewAdminRPCServer()
|
||||
logger.FatalIf(err, "Unable to initialize Lock RPC Server", context.Background())
|
||||
subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter()
|
||||
subrouter.Path(adminServiceSubPath).Handler(rpcServer)
|
||||
subrouter.Path(adminServiceSubPath).HandlerFunc(httpTraceHdrs(rpcServer.ServeHTTP))
|
||||
}
|
||||
|
||||
@@ -17,13 +17,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -99,31 +94,6 @@ func testAdminCmdRunnerReInitFormat(t *testing.T, client adminCmdRunner) {
|
||||
}
|
||||
}
|
||||
|
||||
func testAdminCmdRunnerListLocks(t *testing.T, client adminCmdRunner) {
|
||||
tmpGlobalObjectAPI := globalObjectAPI
|
||||
defer func() {
|
||||
globalObjectAPI = tmpGlobalObjectAPI
|
||||
}()
|
||||
|
||||
testCases := []struct {
|
||||
objectAPI ObjectLayer
|
||||
expectErr bool
|
||||
}{
|
||||
{&DummyObjectLayer{}, false},
|
||||
{nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
globalObjectAPI = testCase.objectAPI
|
||||
_, err := client.ListLocks("", "", time.Duration(0))
|
||||
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) {
|
||||
tmpGlobalBootTime := globalBootTime
|
||||
tmpGlobalObjectAPI := globalObjectAPI
|
||||
@@ -197,81 +167,6 @@ func testAdminCmdRunnerGetConfig(t *testing.T, client adminCmdRunner) {
|
||||
}
|
||||
}
|
||||
|
||||
func testAdminCmdRunnerWriteTmpConfig(t *testing.T, client adminCmdRunner) {
|
||||
tmpConfigDir := configDir
|
||||
defer func() {
|
||||
configDir = tmpConfigDir
|
||||
}()
|
||||
|
||||
tempDir, err := ioutil.TempDir("", ".AdminCmdRunnerWriteTmpConfig.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
configDir = &ConfigDir{dir: tempDir}
|
||||
|
||||
testCases := []struct {
|
||||
tmpFilename string
|
||||
configBytes []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{"config1.json", []byte(`{"version":"23","region":"us-west-1a"}`), false},
|
||||
// Overwrite test.
|
||||
{"config1.json", []byte(`{"version":"23","region":"us-west-1a","browser":"on"}`), false},
|
||||
{"config2.json", []byte{}, false},
|
||||
{"config3.json", nil, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := client.WriteTmpConfig(testCase.tmpFilename, testCase.configBytes)
|
||||
expectErr := (err != nil)
|
||||
|
||||
if expectErr != testCase.expectErr {
|
||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testAdminCmdRunnerCommitConfig(t *testing.T, client adminCmdRunner) {
|
||||
tmpConfigDir := configDir
|
||||
defer func() {
|
||||
configDir = tmpConfigDir
|
||||
}()
|
||||
|
||||
tempDir, err := ioutil.TempDir("", ".AdminCmdRunnerCommitConfig.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
configDir = &ConfigDir{dir: tempDir}
|
||||
err = ioutil.WriteFile(filepath.Join(tempDir, "config.json"), []byte{}, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
err = client.WriteTmpConfig("config1.json", []byte(`{"version":"23","region":"us-west-1a"}`))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
tmpFilename string
|
||||
expectErr bool
|
||||
}{
|
||||
{"config1.json", false},
|
||||
{"config2.json", true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
err := client.CommitConfig(testCase.tmpFilename)
|
||||
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) {
|
||||
rpcServer, err := NewAdminRPCServer()
|
||||
if err != nil {
|
||||
@@ -323,16 +218,6 @@ func TestAdminRPCClientReInitFormat(t *testing.T) {
|
||||
testAdminCmdRunnerReInitFormat(t, rpcClient)
|
||||
}
|
||||
|
||||
func TestAdminRPCClientListLocks(t *testing.T) {
|
||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
||||
defer httpServer.Close()
|
||||
defer func() {
|
||||
globalServerConfig = prevGlobalServerConfig
|
||||
}()
|
||||
|
||||
testAdminCmdRunnerListLocks(t, rpcClient)
|
||||
}
|
||||
|
||||
func TestAdminRPCClientServerInfo(t *testing.T) {
|
||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
||||
defer httpServer.Close()
|
||||
@@ -352,260 +237,3 @@ func TestAdminRPCClientGetConfig(t *testing.T) {
|
||||
|
||||
testAdminCmdRunnerGetConfig(t, rpcClient)
|
||||
}
|
||||
|
||||
func TestAdminRPCClientWriteTmpConfig(t *testing.T) {
|
||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
||||
defer httpServer.Close()
|
||||
defer func() {
|
||||
globalServerConfig = prevGlobalServerConfig
|
||||
}()
|
||||
|
||||
testAdminCmdRunnerWriteTmpConfig(t, rpcClient)
|
||||
}
|
||||
|
||||
func TestAdminRPCClientCommitConfig(t *testing.T) {
|
||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
||||
defer httpServer.Close()
|
||||
defer func() {
|
||||
globalServerConfig = prevGlobalServerConfig
|
||||
}()
|
||||
|
||||
testAdminCmdRunnerCommitConfig(t, rpcClient)
|
||||
}
|
||||
|
||||
var (
|
||||
config1 = []byte(`{
|
||||
"version": "13",
|
||||
"credential": {
|
||||
"accessKey": "minio",
|
||||
"secretKey": "minio123"
|
||||
},
|
||||
"region": "us-east-1",
|
||||
"logger": {
|
||||
"console": {
|
||||
"enable": true,
|
||||
"level": "debug"
|
||||
},
|
||||
"file": {
|
||||
"enable": false,
|
||||
"fileName": "",
|
||||
"level": ""
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"amqp": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"exchange": "",
|
||||
"routingKey": "",
|
||||
"exchangeType": "",
|
||||
"mandatory": false,
|
||||
"immediate": false,
|
||||
"durable": false,
|
||||
"internal": false,
|
||||
"noWait": false,
|
||||
"autoDeleted": false
|
||||
}
|
||||
},
|
||||
"nats": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"subject": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"token": "",
|
||||
"secure": false,
|
||||
"pingInterval": 0,
|
||||
"streaming": {
|
||||
"enable": false,
|
||||
"clusterID": "",
|
||||
"clientID": "",
|
||||
"async": false,
|
||||
"maxPubAcksInflight": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"elasticsearch": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"index": ""
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"password": "",
|
||||
"key": ""
|
||||
}
|
||||
},
|
||||
"postgresql": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"connectionString": "",
|
||||
"table": "",
|
||||
"host": "",
|
||||
"port": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"database": ""
|
||||
}
|
||||
},
|
||||
"kafka": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"brokers": null,
|
||||
"topic": ""
|
||||
}
|
||||
},
|
||||
"webhook": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"endpoint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
// diff from config1 - amqp.Enable is True
|
||||
config2 = []byte(`{
|
||||
"version": "13",
|
||||
"credential": {
|
||||
"accessKey": "minio",
|
||||
"secretKey": "minio123"
|
||||
},
|
||||
"region": "us-east-1",
|
||||
"logger": {
|
||||
"console": {
|
||||
"enable": true,
|
||||
"level": "debug"
|
||||
},
|
||||
"file": {
|
||||
"enable": false,
|
||||
"fileName": "",
|
||||
"level": ""
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"amqp": {
|
||||
"1": {
|
||||
"enable": true,
|
||||
"url": "",
|
||||
"exchange": "",
|
||||
"routingKey": "",
|
||||
"exchangeType": "",
|
||||
"mandatory": false,
|
||||
"immediate": false,
|
||||
"durable": false,
|
||||
"internal": false,
|
||||
"noWait": false,
|
||||
"autoDeleted": false
|
||||
}
|
||||
},
|
||||
"nats": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"subject": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"token": "",
|
||||
"secure": false,
|
||||
"pingInterval": 0,
|
||||
"streaming": {
|
||||
"enable": false,
|
||||
"clusterID": "",
|
||||
"clientID": "",
|
||||
"async": false,
|
||||
"maxPubAcksInflight": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"elasticsearch": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"index": ""
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"password": "",
|
||||
"key": ""
|
||||
}
|
||||
},
|
||||
"postgresql": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"connectionString": "",
|
||||
"table": "",
|
||||
"host": "",
|
||||
"port": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"database": ""
|
||||
}
|
||||
},
|
||||
"kafka": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"brokers": null,
|
||||
"topic": ""
|
||||
}
|
||||
},
|
||||
"webhook": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"endpoint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
)
|
||||
|
||||
// TestGetValidServerConfig - test for getValidServerConfig.
|
||||
func TestGetValidServerConfig(t *testing.T) {
|
||||
var c1, c2 serverConfig
|
||||
err := json.Unmarshal(config1, &c1)
|
||||
if err != nil {
|
||||
t.Fatalf("json unmarshal of %s failed: %v", string(config1), err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(config2, &c2)
|
||||
if err != nil {
|
||||
t.Fatalf("json unmarshal of %s failed: %v", string(config2), err)
|
||||
}
|
||||
|
||||
// Valid config.
|
||||
noErrs := []error{nil, nil, nil, nil}
|
||||
serverConfigs := []serverConfig{c1, c2, c1, c1}
|
||||
validConfig, err := getValidServerConfig(serverConfigs, noErrs)
|
||||
if err != nil {
|
||||
t.Errorf("Expected a valid config but received %v instead", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(validConfig, c1) {
|
||||
t.Errorf("Expected valid config to be %v but received %v", config1, validConfig)
|
||||
}
|
||||
|
||||
// Invalid config - no quorum.
|
||||
serverConfigs = []serverConfig{c1, c2, c2, c1}
|
||||
_, err = getValidServerConfig(serverConfigs, noErrs)
|
||||
if err != errXLWriteQuorum {
|
||||
t.Errorf("Expected to fail due to lack of quorum but received %v", err)
|
||||
}
|
||||
|
||||
// All errors
|
||||
allErrs := []error{errDiskNotFound, errDiskNotFound, errDiskNotFound, errDiskNotFound}
|
||||
serverConfigs = []serverConfig{{}, {}, {}, {}}
|
||||
_, err = getValidServerConfig(serverConfigs, allErrs)
|
||||
if err != errXLWriteQuorum {
|
||||
t.Errorf("Expected to fail due to lack of quorum but received %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,87 @@ const (
|
||||
responseRequestIDKey = "x-amz-request-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.
|
||||
type ObjectIdentifier struct {
|
||||
ObjectName string `xml:"Key"`
|
||||
|
||||
+735
-7
@@ -22,10 +22,12 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/s3select"
|
||||
)
|
||||
|
||||
// APIError structure
|
||||
@@ -127,8 +129,12 @@ const (
|
||||
ErrMaximumExpires
|
||||
ErrSlowDown
|
||||
ErrInvalidPrefixMarker
|
||||
ErrBadRequest
|
||||
// Add new error codes here.
|
||||
|
||||
// SSE-S3 related API errors
|
||||
ErrInvalidEncryptionMethod
|
||||
|
||||
// Server-Side-Encryption (with Customer provided key) related API errors.
|
||||
ErrInsecureSSECustomerRequest
|
||||
ErrSSEMultipartEncrypted
|
||||
@@ -140,6 +146,9 @@ const (
|
||||
ErrMissingSSECustomerKeyMD5
|
||||
ErrSSECustomerKeyMD5Mismatch
|
||||
ErrInvalidSSECustomerParameters
|
||||
ErrIncompatibleEncryptionMethod
|
||||
ErrKMSNotConfigured
|
||||
ErrKMSAuthFailure
|
||||
|
||||
// Bucket notification related errors.
|
||||
ErrEventNotification
|
||||
@@ -186,6 +195,7 @@ const (
|
||||
ErrAdminCredentialsMismatch
|
||||
ErrInsecureClientRequest
|
||||
ErrObjectTampered
|
||||
|
||||
ErrHealNotImplemented
|
||||
ErrHealNoSuchProcess
|
||||
ErrHealInvalidClientToken
|
||||
@@ -193,6 +203,94 @@ const (
|
||||
ErrHealAlreadyRunning
|
||||
ErrHealOverlappingPaths
|
||||
ErrIncorrectContinuationToken
|
||||
|
||||
//S3 Select Errors
|
||||
ErrEmptyRequestBody
|
||||
ErrUnsupportedFunction
|
||||
ErrInvalidExpressionType
|
||||
ErrBusy
|
||||
ErrUnauthorizedAccess
|
||||
ErrExpressionTooLong
|
||||
ErrIllegalSQLFunctionArgument
|
||||
ErrInvalidKeyPath
|
||||
ErrInvalidCompressionFormat
|
||||
ErrInvalidFileHeaderInfo
|
||||
ErrInvalidJSONType
|
||||
ErrInvalidQuoteFields
|
||||
ErrInvalidRequestParameter
|
||||
ErrInvalidDataType
|
||||
ErrInvalidTextEncoding
|
||||
ErrInvalidDataSource
|
||||
ErrInvalidTableAlias
|
||||
ErrMissingRequiredParameter
|
||||
ErrObjectSerializationConflict
|
||||
ErrUnsupportedSQLOperation
|
||||
ErrUnsupportedSQLStructure
|
||||
ErrUnsupportedSyntax
|
||||
ErrUnsupportedRangeHeader
|
||||
ErrLexerInvalidChar
|
||||
ErrLexerInvalidOperator
|
||||
ErrLexerInvalidLiteral
|
||||
ErrLexerInvalidIONLiteral
|
||||
ErrParseExpectedDatePart
|
||||
ErrParseExpectedKeyword
|
||||
ErrParseExpectedTokenType
|
||||
ErrParseExpected2TokenTypes
|
||||
ErrParseExpectedNumber
|
||||
ErrParseExpectedRightParenBuiltinFunctionCall
|
||||
ErrParseExpectedTypeName
|
||||
ErrParseExpectedWhenClause
|
||||
ErrParseUnsupportedToken
|
||||
ErrParseUnsupportedLiteralsGroupBy
|
||||
ErrParseExpectedMember
|
||||
ErrParseUnsupportedSelect
|
||||
ErrParseUnsupportedCase
|
||||
ErrParseUnsupportedCaseClause
|
||||
ErrParseUnsupportedAlias
|
||||
ErrParseUnsupportedSyntax
|
||||
ErrParseUnknownOperator
|
||||
ErrParseInvalidPathComponent
|
||||
ErrParseMissingIdentAfterAt
|
||||
ErrParseUnexpectedOperator
|
||||
ErrParseUnexpectedTerm
|
||||
ErrParseUnexpectedToken
|
||||
ErrParseUnexpectedKeyword
|
||||
ErrParseExpectedExpression
|
||||
ErrParseExpectedLeftParenAfterCast
|
||||
ErrParseExpectedLeftParenValueConstructor
|
||||
ErrParseExpectedLeftParenBuiltinFunctionCall
|
||||
ErrParseExpectedArgumentDelimiter
|
||||
ErrParseCastArity
|
||||
ErrParseInvalidTypeParam
|
||||
ErrParseEmptySelect
|
||||
ErrParseSelectMissingFrom
|
||||
ErrParseExpectedIdentForGroupName
|
||||
ErrParseExpectedIdentForAlias
|
||||
ErrParseUnsupportedCallWithStar
|
||||
ErrParseNonUnaryAgregateFunctionCall
|
||||
ErrParseMalformedJoin
|
||||
ErrParseExpectedIdentForAt
|
||||
ErrParseAsteriskIsNotAloneInSelectList
|
||||
ErrParseCannotMixSqbAndWildcardInSelectList
|
||||
ErrParseInvalidContextForWildcardInSelectList
|
||||
ErrIncorrectSQLFunctionArgumentType
|
||||
ErrValueParseFailure
|
||||
ErrEvaluatorInvalidArguments
|
||||
ErrIntegerOverflow
|
||||
ErrLikeInvalidInputs
|
||||
ErrCastFailed
|
||||
ErrInvalidCast
|
||||
ErrEvaluatorInvalidTimestampFormatPattern
|
||||
ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing
|
||||
ErrEvaluatorTimestampFormatPatternDuplicateFields
|
||||
ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch
|
||||
ErrEvaluatorUnterminatedTimestampFormatPatternToken
|
||||
ErrEvaluatorInvalidTimestampFormatPatternToken
|
||||
ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
||||
ErrEvaluatorBindingDoesNotExist
|
||||
ErrInvalidColumnIndex
|
||||
ErrMissingHeaders
|
||||
ErrAdminConfigNotificationTargetsFailed
|
||||
)
|
||||
|
||||
// error code to APIError structure, these fields carry respective
|
||||
@@ -540,6 +638,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Invalid marker prefix combination",
|
||||
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.
|
||||
ErrUnsignedHeaders: {
|
||||
@@ -629,6 +732,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Your metadata headers exceed the maximum allowed metadata size.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidEncryptionMethod: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "The encryption method specified is not supported",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInsecureSSECustomerRequest: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "Requests specifying Server Side Encryption with Customer provided keys must be made over a secure connection.",
|
||||
@@ -679,6 +787,21 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "The provided encryption parameters did not match the ones used originally.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrIncompatibleEncryptionMethod: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Server side encryption specified with both SSE-C and SSE-S3 headers",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrKMSNotConfigured: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Server side encryption specified but KMS is not configured",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrKMSAuthFailure: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Server side encryption specified but KMS authorization failed",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
/// S3 extensions.
|
||||
ErrContentSHA256Mismatch: {
|
||||
@@ -764,6 +887,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "JSON configuration provided has objects with duplicate keys",
|
||||
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,
|
||||
},
|
||||
ErrAdminCredentialsMismatch: {
|
||||
Code: "XMinioAdminCredentialsMismatch",
|
||||
Description: "Credentials in config mismatch with server environment variables",
|
||||
@@ -794,6 +922,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
// Generic Invalid-Request error. Should be used for response errors only for unlikely
|
||||
// corner case errors for which introducing new APIErrorCode is not worth it. LogIf()
|
||||
// should be used to log the error at the source of the error for debugging purposes.
|
||||
@@ -842,6 +971,432 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "The continuation token provided is incorrect",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
//S3 Select API Errors
|
||||
ErrEmptyRequestBody: {
|
||||
Code: "EmptyRequestBody",
|
||||
Description: "Request body cannot be empty.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrUnsupportedFunction: {
|
||||
Code: "UnsupportedFunction",
|
||||
Description: "Encountered an unsupported SQL function.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidDataSource: {
|
||||
Code: "InvalidDataSource",
|
||||
Description: "Invalid data source type. Only CSV and JSON are supported at this time.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidExpressionType: {
|
||||
Code: "InvalidExpressionType",
|
||||
Description: "The ExpressionType is invalid. Only SQL expressions are supported at this time.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrBusy: {
|
||||
Code: "Busy",
|
||||
Description: "The service is unavailable. Please retry.",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrUnauthorizedAccess: {
|
||||
Code: "UnauthorizedAccess",
|
||||
Description: "You are not authorized to perform this operation",
|
||||
HTTPStatusCode: http.StatusUnauthorized,
|
||||
},
|
||||
ErrExpressionTooLong: {
|
||||
Code: "ExpressionTooLong",
|
||||
Description: "The SQL expression is too long: The maximum byte-length for the SQL expression is 256 KB.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrIllegalSQLFunctionArgument: {
|
||||
Code: "IllegalSqlFunctionArgument",
|
||||
Description: "Illegal argument was used in the SQL function.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidKeyPath: {
|
||||
Code: "InvalidKeyPath",
|
||||
Description: "Key path in the SQL expression is invalid.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidCompressionFormat: {
|
||||
Code: "InvalidCompressionFormat",
|
||||
Description: "The file is not in a supported compression format. Only GZIP is supported at this time.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidFileHeaderInfo: {
|
||||
Code: "InvalidFileHeaderInfo",
|
||||
Description: "The FileHeaderInfo is invalid. Only NONE, USE, and IGNORE are supported.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidJSONType: {
|
||||
Code: "InvalidJsonType",
|
||||
Description: "The JsonType is invalid. Only DOCUMENT and LINES are supported at this time.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidQuoteFields: {
|
||||
Code: "InvalidQuoteFields",
|
||||
Description: "The QuoteFields is invalid. Only ALWAYS and ASNEEDED are supported.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidRequestParameter: {
|
||||
Code: "InvalidRequestParameter",
|
||||
Description: "The value of a parameter in SelectRequest element is invalid. Check the service API documentation and try again.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidDataType: {
|
||||
Code: "InvalidDataType",
|
||||
Description: "The SQL expression contains an invalid data type.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidTextEncoding: {
|
||||
Code: "InvalidTextEncoding",
|
||||
Description: "Invalid encoding type. Only UTF-8 encoding is supported at this time.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidTableAlias: {
|
||||
Code: "InvalidTableAlias",
|
||||
Description: "The SQL expression contains an invalid table alias.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingRequiredParameter: {
|
||||
Code: "MissingRequiredParameter",
|
||||
Description: "The SelectRequest entity is missing a required parameter. Check the service documentation and try again.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrObjectSerializationConflict: {
|
||||
Code: "ObjectSerializationConflict",
|
||||
Description: "The SelectRequest entity can only contain one of CSV or JSON. Check the service documentation and try again.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrUnsupportedSQLOperation: {
|
||||
Code: "UnsupportedSqlOperation",
|
||||
Description: "Encountered an unsupported SQL operation.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrUnsupportedSQLStructure: {
|
||||
Code: "UnsupportedSqlStructure",
|
||||
Description: "Encountered an unsupported SQL structure. Check the SQL Reference.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrUnsupportedSyntax: {
|
||||
Code: "UnsupportedSyntax",
|
||||
Description: "Encountered invalid syntax.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrUnsupportedRangeHeader: {
|
||||
Code: "UnsupportedRangeHeader",
|
||||
Description: "Range header is not supported for this operation.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrLexerInvalidChar: {
|
||||
Code: "LexerInvalidChar",
|
||||
Description: "The SQL expression contains an invalid character.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrLexerInvalidOperator: {
|
||||
Code: "LexerInvalidOperator",
|
||||
Description: "The SQL expression contains an invalid literal.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrLexerInvalidLiteral: {
|
||||
Code: "LexerInvalidLiteral",
|
||||
Description: "The SQL expression contains an invalid operator.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrLexerInvalidIONLiteral: {
|
||||
Code: "LexerInvalidIONLiteral",
|
||||
Description: "The SQL expression contains an invalid operator.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedDatePart: {
|
||||
Code: "ParseExpectedDatePart",
|
||||
Description: "Did not find the expected date part in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedKeyword: {
|
||||
Code: "ParseExpectedKeyword",
|
||||
Description: "Did not find the expected keyword in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedTokenType: {
|
||||
Code: "ParseExpectedTokenType",
|
||||
Description: "Did not find the expected token in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpected2TokenTypes: {
|
||||
Code: "ParseExpected2TokenTypes",
|
||||
Description: "Did not find the expected token in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedNumber: {
|
||||
Code: "ParseExpectedNumber",
|
||||
Description: "Did not find the expected number in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedRightParenBuiltinFunctionCall: {
|
||||
Code: "ParseExpectedRightParenBuiltinFunctionCall",
|
||||
Description: "Did not find the expected right parenthesis character in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedTypeName: {
|
||||
Code: "ParseExpectedTypeName",
|
||||
Description: "Did not find the expected type name in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedWhenClause: {
|
||||
Code: "ParseExpectedWhenClause",
|
||||
Description: "Did not find the expected WHEN clause in the SQL expression. CASE is not supported.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseUnsupportedToken: {
|
||||
Code: "ParseUnsupportedToken",
|
||||
Description: "The SQL expression contains an unsupported token.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseUnsupportedLiteralsGroupBy: {
|
||||
Code: "ParseUnsupportedLiteralsGroupBy",
|
||||
Description: "The SQL expression contains an unsupported use of GROUP BY.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedMember: {
|
||||
Code: "ParseExpectedMember",
|
||||
Description: "The SQL expression contains an unsupported use of MEMBER.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseUnsupportedSelect: {
|
||||
Code: "ParseUnsupportedSelect",
|
||||
Description: "The SQL expression contains an unsupported use of SELECT.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseUnsupportedCase: {
|
||||
Code: "ParseUnsupportedCase",
|
||||
Description: "The SQL expression contains an unsupported use of CASE.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseUnsupportedCaseClause: {
|
||||
Code: "ParseUnsupportedCaseClause",
|
||||
Description: "The SQL expression contains an unsupported use of CASE.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseUnsupportedAlias: {
|
||||
Code: "ParseUnsupportedAlias",
|
||||
Description: "The SQL expression contains an unsupported use of ALIAS.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseUnsupportedSyntax: {
|
||||
Code: "ParseUnsupportedSyntax",
|
||||
Description: "The SQL expression contains unsupported syntax.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseUnknownOperator: {
|
||||
Code: "ParseUnknownOperator",
|
||||
Description: "The SQL expression contains an invalid operator.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseInvalidPathComponent: {
|
||||
Code: "ParseInvalidPathComponent",
|
||||
Description: "The SQL expression contains an invalid path component.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseMissingIdentAfterAt: {
|
||||
Code: "ParseMissingIdentAfterAt",
|
||||
Description: "Did not find the expected identifier after the @ symbol in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseUnexpectedOperator: {
|
||||
Code: "ParseUnexpectedOperator",
|
||||
Description: "The SQL expression contains an unexpected operator.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseUnexpectedTerm: {
|
||||
Code: "ParseUnexpectedTerm",
|
||||
Description: "The SQL expression contains an unexpected term.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseUnexpectedToken: {
|
||||
Code: "ParseUnexpectedToken",
|
||||
Description: "The SQL expression contains an unexpected token.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseUnexpectedKeyword: {
|
||||
Code: "ParseUnexpectedKeyword",
|
||||
Description: "The SQL expression contains an unexpected keyword.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedExpression: {
|
||||
Code: "ParseExpectedExpression",
|
||||
Description: "Did not find the expected SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedLeftParenAfterCast: {
|
||||
Code: "ParseExpectedLeftParenAfterCast",
|
||||
Description: "Did not find expected the left parenthesis in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedLeftParenValueConstructor: {
|
||||
Code: "ParseExpectedLeftParenValueConstructor",
|
||||
Description: "Did not find expected the left parenthesis in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedLeftParenBuiltinFunctionCall: {
|
||||
Code: "ParseExpectedLeftParenBuiltinFunctionCall",
|
||||
Description: "Did not find the expected left parenthesis in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedArgumentDelimiter: {
|
||||
Code: "ParseExpectedArgumentDelimiter",
|
||||
Description: "Did not find the expected argument delimiter in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseCastArity: {
|
||||
Code: "ParseCastArity",
|
||||
Description: "The SQL expression CAST has incorrect arity.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseInvalidTypeParam: {
|
||||
Code: "ParseInvalidTypeParam",
|
||||
Description: "The SQL expression contains an invalid parameter value.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseEmptySelect: {
|
||||
Code: "ParseEmptySelect",
|
||||
Description: "The SQL expression contains an empty SELECT.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseSelectMissingFrom: {
|
||||
Code: "ParseSelectMissingFrom",
|
||||
Description: "GROUP is not supported in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedIdentForGroupName: {
|
||||
Code: "ParseExpectedIdentForGroupName",
|
||||
Description: "GROUP is not supported in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedIdentForAlias: {
|
||||
Code: "ParseExpectedIdentForAlias",
|
||||
Description: "Did not find the expected identifier for the alias in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseUnsupportedCallWithStar: {
|
||||
Code: "ParseUnsupportedCallWithStar",
|
||||
Description: "Only COUNT with (*) as a parameter is supported in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseNonUnaryAgregateFunctionCall: {
|
||||
Code: "ParseNonUnaryAgregateFunctionCall",
|
||||
Description: "Only one argument is supported for aggregate functions in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseMalformedJoin: {
|
||||
Code: "ParseMalformedJoin",
|
||||
Description: "JOIN is not supported in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseExpectedIdentForAt: {
|
||||
Code: "ParseExpectedIdentForAt",
|
||||
Description: "Did not find the expected identifier for AT name in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseAsteriskIsNotAloneInSelectList: {
|
||||
Code: "ParseAsteriskIsNotAloneInSelectList",
|
||||
Description: "Other expressions are not allowed in the SELECT list when '*' is used without dot notation in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseCannotMixSqbAndWildcardInSelectList: {
|
||||
Code: "ParseCannotMixSqbAndWildcardInSelectList",
|
||||
Description: "Cannot mix [] and * in the same expression in a SELECT list in SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrParseInvalidContextForWildcardInSelectList: {
|
||||
Code: "ParseInvalidContextForWildcardInSelectList",
|
||||
Description: "Invalid use of * in SELECT list in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrIncorrectSQLFunctionArgumentType: {
|
||||
Code: "IncorrectSqlFunctionArgumentType",
|
||||
Description: "Incorrect type of arguments in function call in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrValueParseFailure: {
|
||||
Code: "ValueParseFailure",
|
||||
Description: "Time stamp parse failure in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrEvaluatorInvalidArguments: {
|
||||
Code: "EvaluatorInvalidArguments",
|
||||
Description: "Incorrect number of arguments in the function call in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrIntegerOverflow: {
|
||||
Code: "IntegerOverflow",
|
||||
Description: "Int overflow or underflow in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrLikeInvalidInputs: {
|
||||
Code: "LikeInvalidInputs",
|
||||
Description: "Invalid argument given to the LIKE clause in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrCastFailed: {
|
||||
Code: "CastFailed",
|
||||
Description: "Attempt to convert from one data type to another using CAST failed in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidCast: {
|
||||
Code: "InvalidCast",
|
||||
Description: "Attempt to convert from one data type to another using CAST failed in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrEvaluatorInvalidTimestampFormatPattern: {
|
||||
Code: "EvaluatorInvalidTimestampFormatPattern",
|
||||
Description: "Time stamp format pattern requires additional fields in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing: {
|
||||
Code: "EvaluatorInvalidTimestampFormatPatternSymbolForParsing",
|
||||
Description: "Time stamp format pattern contains a valid format symbol that cannot be applied to time stamp parsing in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrEvaluatorTimestampFormatPatternDuplicateFields: {
|
||||
Code: "EvaluatorTimestampFormatPatternDuplicateFields",
|
||||
Description: "Time stamp format pattern contains multiple format specifiers representing the time stamp field in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch: {
|
||||
Code: "EvaluatorUnterminatedTimestampFormatPatternToken",
|
||||
Description: "Time stamp format pattern contains unterminated token in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrEvaluatorUnterminatedTimestampFormatPatternToken: {
|
||||
Code: "EvaluatorInvalidTimestampFormatPatternToken",
|
||||
Description: "Time stamp format pattern contains an invalid token in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrEvaluatorInvalidTimestampFormatPatternToken: {
|
||||
Code: "EvaluatorInvalidTimestampFormatPatternToken",
|
||||
Description: "Time stamp format pattern contains an invalid token in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrEvaluatorInvalidTimestampFormatPatternSymbol: {
|
||||
Code: "EvaluatorInvalidTimestampFormatPatternSymbol",
|
||||
Description: "Time stamp format pattern contains an invalid symbol in the SQL expression.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidColumnIndex: {
|
||||
Code: "InvalidColumnIndex",
|
||||
Description: "Column index in the SQL expression is invalid.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrEvaluatorBindingDoesNotExist: {
|
||||
Code: "ErrEvaluatorBindingDoesNotExist",
|
||||
Description: "A column name or a path provided does not exist in the SQL expression",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingHeaders: {
|
||||
Code: "MissingHeaders",
|
||||
Description: "Some headers in the query are missing from the file. Check the file and try again.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
// Add your error structure here.
|
||||
}
|
||||
|
||||
@@ -866,17 +1421,19 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
case auth.ErrInvalidSecretKeyLength:
|
||||
apiErr = ErrAdminInvalidSecretKey
|
||||
// SSE errors
|
||||
case crypto.ErrInvalidEncryptionMethod:
|
||||
apiErr = ErrInvalidEncryptionMethod
|
||||
case errInsecureSSERequest:
|
||||
apiErr = ErrInsecureSSECustomerRequest
|
||||
case errInvalidSSEAlgorithm:
|
||||
case crypto.ErrInvalidCustomerAlgorithm:
|
||||
apiErr = ErrInvalidSSECustomerAlgorithm
|
||||
case errInvalidSSEKey:
|
||||
case crypto.ErrInvalidCustomerKey:
|
||||
apiErr = ErrInvalidSSECustomerKey
|
||||
case errMissingSSEKey:
|
||||
case crypto.ErrMissingCustomerKey:
|
||||
apiErr = ErrMissingSSECustomerKey
|
||||
case errMissingSSEKeyMD5:
|
||||
case crypto.ErrMissingCustomerKeyMD5:
|
||||
apiErr = ErrMissingSSECustomerKeyMD5
|
||||
case errSSEKeyMD5Mismatch:
|
||||
case crypto.ErrCustomerKeyMD5Mismatch:
|
||||
apiErr = ErrSSECustomerKeyMD5Mismatch
|
||||
case errObjectTampered:
|
||||
apiErr = ErrObjectTampered
|
||||
@@ -884,11 +1441,180 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrSSEEncryptedObject
|
||||
case errInvalidSSEParameters:
|
||||
apiErr = ErrInvalidSSECustomerParameters
|
||||
case errSSEKeyMismatch:
|
||||
case crypto.ErrInvalidCustomerKey, crypto.ErrSecretKeyMismatch:
|
||||
apiErr = ErrAccessDenied // no access without correct key
|
||||
case context.Canceled, context.DeadlineExceeded:
|
||||
case crypto.ErrIncompatibleEncryptionMethod:
|
||||
apiErr = ErrIncompatibleEncryptionMethod
|
||||
case errKMSNotConfigured:
|
||||
apiErr = ErrKMSNotConfigured
|
||||
case crypto.ErrKMSAuthLogin:
|
||||
apiErr = ErrKMSAuthFailure
|
||||
case errOperationTimedOut, context.Canceled, context.DeadlineExceeded:
|
||||
apiErr = ErrOperationTimedOut
|
||||
}
|
||||
switch err {
|
||||
case s3select.ErrBusy:
|
||||
apiErr = ErrBusy
|
||||
case s3select.ErrUnauthorizedAccess:
|
||||
apiErr = ErrUnauthorizedAccess
|
||||
case s3select.ErrExpressionTooLong:
|
||||
apiErr = ErrExpressionTooLong
|
||||
case s3select.ErrIllegalSQLFunctionArgument:
|
||||
apiErr = ErrIllegalSQLFunctionArgument
|
||||
case s3select.ErrInvalidKeyPath:
|
||||
apiErr = ErrInvalidKeyPath
|
||||
case s3select.ErrInvalidCompressionFormat:
|
||||
apiErr = ErrInvalidCompressionFormat
|
||||
case s3select.ErrInvalidFileHeaderInfo:
|
||||
apiErr = ErrInvalidFileHeaderInfo
|
||||
case s3select.ErrInvalidJSONType:
|
||||
apiErr = ErrInvalidJSONType
|
||||
case s3select.ErrInvalidQuoteFields:
|
||||
apiErr = ErrInvalidQuoteFields
|
||||
case s3select.ErrInvalidRequestParameter:
|
||||
apiErr = ErrInvalidRequestParameter
|
||||
case s3select.ErrInvalidDataType:
|
||||
apiErr = ErrInvalidDataType
|
||||
case s3select.ErrInvalidTextEncoding:
|
||||
apiErr = ErrInvalidTextEncoding
|
||||
case s3select.ErrInvalidTableAlias:
|
||||
apiErr = ErrInvalidTableAlias
|
||||
case s3select.ErrMissingRequiredParameter:
|
||||
apiErr = ErrMissingRequiredParameter
|
||||
case s3select.ErrObjectSerializationConflict:
|
||||
apiErr = ErrObjectSerializationConflict
|
||||
case s3select.ErrUnsupportedSQLOperation:
|
||||
apiErr = ErrUnsupportedSQLOperation
|
||||
case s3select.ErrUnsupportedSQLStructure:
|
||||
apiErr = ErrUnsupportedSQLStructure
|
||||
case s3select.ErrUnsupportedSyntax:
|
||||
apiErr = ErrUnsupportedSyntax
|
||||
case s3select.ErrUnsupportedRangeHeader:
|
||||
apiErr = ErrUnsupportedRangeHeader
|
||||
case s3select.ErrLexerInvalidChar:
|
||||
apiErr = ErrLexerInvalidChar
|
||||
case s3select.ErrLexerInvalidOperator:
|
||||
apiErr = ErrLexerInvalidOperator
|
||||
case s3select.ErrLexerInvalidLiteral:
|
||||
apiErr = ErrLexerInvalidLiteral
|
||||
case s3select.ErrLexerInvalidIONLiteral:
|
||||
apiErr = ErrLexerInvalidIONLiteral
|
||||
case s3select.ErrParseExpectedDatePart:
|
||||
apiErr = ErrParseExpectedDatePart
|
||||
case s3select.ErrParseExpectedKeyword:
|
||||
apiErr = ErrParseExpectedKeyword
|
||||
case s3select.ErrParseExpectedTokenType:
|
||||
apiErr = ErrParseExpectedTokenType
|
||||
case s3select.ErrParseExpected2TokenTypes:
|
||||
apiErr = ErrParseExpected2TokenTypes
|
||||
case s3select.ErrParseExpectedNumber:
|
||||
apiErr = ErrParseExpectedNumber
|
||||
case s3select.ErrParseExpectedRightParenBuiltinFunctionCall:
|
||||
apiErr = ErrParseExpectedRightParenBuiltinFunctionCall
|
||||
case s3select.ErrParseExpectedTypeName:
|
||||
apiErr = ErrParseExpectedTypeName
|
||||
case s3select.ErrParseExpectedWhenClause:
|
||||
apiErr = ErrParseExpectedWhenClause
|
||||
case s3select.ErrParseUnsupportedToken:
|
||||
apiErr = ErrParseUnsupportedToken
|
||||
case s3select.ErrParseUnsupportedLiteralsGroupBy:
|
||||
apiErr = ErrParseUnsupportedLiteralsGroupBy
|
||||
case s3select.ErrParseExpectedMember:
|
||||
apiErr = ErrParseExpectedMember
|
||||
case s3select.ErrParseUnsupportedSelect:
|
||||
apiErr = ErrParseUnsupportedSelect
|
||||
case s3select.ErrParseUnsupportedCase:
|
||||
apiErr = ErrParseUnsupportedCase
|
||||
case s3select.ErrParseUnsupportedCaseClause:
|
||||
apiErr = ErrParseUnsupportedCaseClause
|
||||
case s3select.ErrParseUnsupportedAlias:
|
||||
apiErr = ErrParseUnsupportedAlias
|
||||
case s3select.ErrParseUnsupportedSyntax:
|
||||
apiErr = ErrParseUnsupportedSyntax
|
||||
case s3select.ErrParseUnknownOperator:
|
||||
apiErr = ErrParseUnknownOperator
|
||||
case s3select.ErrParseInvalidPathComponent:
|
||||
apiErr = ErrParseInvalidPathComponent
|
||||
case s3select.ErrParseMissingIdentAfterAt:
|
||||
apiErr = ErrParseMissingIdentAfterAt
|
||||
case s3select.ErrParseUnexpectedOperator:
|
||||
apiErr = ErrParseUnexpectedOperator
|
||||
case s3select.ErrParseUnexpectedTerm:
|
||||
apiErr = ErrParseUnexpectedTerm
|
||||
case s3select.ErrParseUnexpectedToken:
|
||||
apiErr = ErrParseUnexpectedToken
|
||||
case s3select.ErrParseUnexpectedKeyword:
|
||||
apiErr = ErrParseUnexpectedKeyword
|
||||
case s3select.ErrParseExpectedExpression:
|
||||
apiErr = ErrParseExpectedExpression
|
||||
case s3select.ErrParseExpectedLeftParenAfterCast:
|
||||
apiErr = ErrParseExpectedLeftParenAfterCast
|
||||
case s3select.ErrParseExpectedLeftParenValueConstructor:
|
||||
apiErr = ErrParseExpectedLeftParenValueConstructor
|
||||
case s3select.ErrParseExpectedLeftParenBuiltinFunctionCall:
|
||||
apiErr = ErrParseExpectedLeftParenBuiltinFunctionCall
|
||||
case s3select.ErrParseExpectedArgumentDelimiter:
|
||||
apiErr = ErrParseExpectedArgumentDelimiter
|
||||
case s3select.ErrParseCastArity:
|
||||
apiErr = ErrParseCastArity
|
||||
case s3select.ErrParseInvalidTypeParam:
|
||||
apiErr = ErrParseInvalidTypeParam
|
||||
case s3select.ErrParseEmptySelect:
|
||||
apiErr = ErrParseEmptySelect
|
||||
case s3select.ErrParseSelectMissingFrom:
|
||||
apiErr = ErrParseSelectMissingFrom
|
||||
case s3select.ErrParseExpectedIdentForGroupName:
|
||||
apiErr = ErrParseExpectedIdentForGroupName
|
||||
case s3select.ErrParseExpectedIdentForAlias:
|
||||
apiErr = ErrParseExpectedIdentForAlias
|
||||
case s3select.ErrParseUnsupportedCallWithStar:
|
||||
apiErr = ErrParseUnsupportedCallWithStar
|
||||
case s3select.ErrParseNonUnaryAgregateFunctionCall:
|
||||
apiErr = ErrParseNonUnaryAgregateFunctionCall
|
||||
case s3select.ErrParseMalformedJoin:
|
||||
apiErr = ErrParseMalformedJoin
|
||||
case s3select.ErrParseExpectedIdentForAt:
|
||||
apiErr = ErrParseExpectedIdentForAt
|
||||
case s3select.ErrParseAsteriskIsNotAloneInSelectList:
|
||||
apiErr = ErrParseAsteriskIsNotAloneInSelectList
|
||||
case s3select.ErrParseCannotMixSqbAndWildcardInSelectList:
|
||||
apiErr = ErrParseCannotMixSqbAndWildcardInSelectList
|
||||
case s3select.ErrParseInvalidContextForWildcardInSelectList:
|
||||
apiErr = ErrParseInvalidContextForWildcardInSelectList
|
||||
case s3select.ErrIncorrectSQLFunctionArgumentType:
|
||||
apiErr = ErrIncorrectSQLFunctionArgumentType
|
||||
case s3select.ErrValueParseFailure:
|
||||
apiErr = ErrValueParseFailure
|
||||
case s3select.ErrIntegerOverflow:
|
||||
apiErr = ErrIntegerOverflow
|
||||
case s3select.ErrLikeInvalidInputs:
|
||||
apiErr = ErrLikeInvalidInputs
|
||||
case s3select.ErrCastFailed:
|
||||
apiErr = ErrCastFailed
|
||||
case s3select.ErrInvalidCast:
|
||||
apiErr = ErrInvalidCast
|
||||
case s3select.ErrEvaluatorInvalidTimestampFormatPattern:
|
||||
apiErr = ErrEvaluatorInvalidTimestampFormatPattern
|
||||
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing:
|
||||
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing
|
||||
case s3select.ErrEvaluatorTimestampFormatPatternDuplicateFields:
|
||||
apiErr = ErrEvaluatorTimestampFormatPatternDuplicateFields
|
||||
case s3select.ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch:
|
||||
apiErr = ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch
|
||||
case s3select.ErrEvaluatorUnterminatedTimestampFormatPatternToken:
|
||||
apiErr = ErrEvaluatorUnterminatedTimestampFormatPatternToken
|
||||
case s3select.ErrEvaluatorInvalidTimestampFormatPatternToken:
|
||||
apiErr = ErrEvaluatorInvalidTimestampFormatPatternToken
|
||||
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbol:
|
||||
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
||||
case s3select.ErrInvalidColumnIndex:
|
||||
apiErr = ErrInvalidColumnIndex
|
||||
case s3select.ErrEvaluatorBindingDoesNotExist:
|
||||
apiErr = ErrEvaluatorBindingDoesNotExist
|
||||
case s3select.ErrMissingHeaders:
|
||||
apiErr = ErrMissingHeaders
|
||||
|
||||
}
|
||||
|
||||
if apiErr != ErrNone {
|
||||
// If there was a match in the above switch case.
|
||||
@@ -990,6 +1716,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrUnsupportedNotification
|
||||
case BackendDown:
|
||||
apiErr = ErrBackendDown
|
||||
case crypto.Error:
|
||||
apiErr = ErrObjectTampered
|
||||
default:
|
||||
apiErr = ErrInternalError
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
@@ -52,11 +53,11 @@ var toAPIErrorCodeTests = []struct {
|
||||
|
||||
// SSE-C errors
|
||||
{err: errInsecureSSERequest, errCode: ErrInsecureSSECustomerRequest},
|
||||
{err: errInvalidSSEAlgorithm, errCode: ErrInvalidSSECustomerAlgorithm},
|
||||
{err: errMissingSSEKey, errCode: ErrMissingSSECustomerKey},
|
||||
{err: errInvalidSSEKey, errCode: ErrInvalidSSECustomerKey},
|
||||
{err: errMissingSSEKeyMD5, errCode: ErrMissingSSECustomerKeyMD5},
|
||||
{err: errSSEKeyMD5Mismatch, errCode: ErrSSECustomerKeyMD5Mismatch},
|
||||
{err: crypto.ErrInvalidCustomerAlgorithm, errCode: ErrInvalidSSECustomerAlgorithm},
|
||||
{err: crypto.ErrMissingCustomerKey, errCode: ErrMissingSSECustomerKey},
|
||||
{err: crypto.ErrInvalidCustomerKey, errCode: ErrInvalidSSECustomerKey},
|
||||
{err: crypto.ErrMissingCustomerKeyMD5, errCode: ErrMissingSSECustomerKeyMD5},
|
||||
{err: crypto.ErrCustomerKeyMD5Mismatch, errCode: ErrSSECustomerKeyMD5Mismatch},
|
||||
{err: errObjectTampered, errCode: ErrObjectTampered},
|
||||
|
||||
{err: nil, errCode: ErrNone},
|
||||
|
||||
@@ -100,6 +100,5 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *h
|
||||
// Override content-length
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(contentRange.getLength(), 10))
|
||||
w.Header().Set("Content-Range", contentRange.String())
|
||||
w.WriteHeader(http.StatusPartialContent)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-9
@@ -20,7 +20,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
// objectAPIHandler implements and provides http handlers for S3 API.
|
||||
@@ -31,14 +30,6 @@ type objectAPIHandlers struct {
|
||||
|
||||
// registerAPIRouter - registers S3 compatible APIs.
|
||||
func registerAPIRouter(router *mux.Router) {
|
||||
var err error
|
||||
var cacheConfig = globalServerConfig.GetCacheConfig()
|
||||
if len(cacheConfig.Drives) > 0 {
|
||||
// initialize the new disk cache objects.
|
||||
globalCacheObjectAPI, err = newServerCacheObjects(cacheConfig)
|
||||
logger.FatalIf(err, "Unable to initialize disk caching")
|
||||
}
|
||||
|
||||
// Initialize API.
|
||||
api := objectAPIHandlers{
|
||||
ObjectAPI: newObjectLayerFn,
|
||||
@@ -71,6 +62,8 @@ func registerAPIRouter(router *mux.Router) {
|
||||
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.AbortMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}")
|
||||
// GetObjectACL - this is a dummy call.
|
||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectACLHandler)).Queries("acl", "")
|
||||
// SelectObjectContent
|
||||
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.SelectObjectContentHandler)).Queries("select", "").Queries("select-type", "2")
|
||||
// GetObject
|
||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectHandler))
|
||||
// CopyObject
|
||||
|
||||
@@ -344,11 +344,14 @@ func mustNewSignedBadMD5Request(method string, urlStr string, contentLength int6
|
||||
|
||||
// Tests is requested authenticated function, tests replies for s3 errors.
|
||||
func TestIsReqAuthenticated(t *testing.T) {
|
||||
path, err := newTestConfig(globalMinioDefaultRegion)
|
||||
objLayer, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(fsDir)
|
||||
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
||||
if err != nil {
|
||||
@@ -384,11 +387,15 @@ func TestIsReqAuthenticated(t *testing.T) {
|
||||
}
|
||||
}
|
||||
func TestCheckAdminRequestAuthType(t *testing.T) {
|
||||
path, err := newTestConfig(globalMinioDefaultRegion)
|
||||
objLayer, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(fsDir)
|
||||
|
||||
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||
t.Fatalf("unable initialize config file, %s", err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
||||
if err != nil {
|
||||
|
||||
+17
-60
@@ -22,7 +22,6 @@ import (
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
@@ -62,7 +61,7 @@ func runPutObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
// insert the object.
|
||||
objInfo, err := obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata, ObjectOptions{})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -98,7 +97,8 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
||||
// generate md5sum for the generated data.
|
||||
// md5sum of the data to written is required as input for NewMultipartUpload.
|
||||
metadata := make(map[string]string)
|
||||
uploadID, err = obj.NewMultipartUpload(context.Background(), bucket, object, metadata)
|
||||
opts := ObjectOptions{}
|
||||
uploadID, err = obj.NewMultipartUpload(context.Background(), bucket, object, metadata, opts)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
||||
md5hex = getMD5Hash([]byte(textPartData))
|
||||
var partInfo PartInfo
|
||||
partInfo, err = obj.PutObjectPart(context.Background(), bucket, object, uploadID, j,
|
||||
mustGetHashReader(b, bytes.NewBuffer(textPartData), int64(len(textPartData)), md5hex, sha256hex))
|
||||
mustGetHashReader(b, bytes.NewBuffer(textPartData), int64(len(textPartData)), md5hex, sha256hex), opts)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -138,12 +138,6 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
||||
|
||||
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectPartBenchmark function.
|
||||
func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
if err != nil {
|
||||
@@ -151,18 +145,13 @@ func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
|
||||
}
|
||||
// cleaning up the backend by removing all the directories and files created on function return.
|
||||
defer removeRoots(disks)
|
||||
|
||||
// uses *testing.B and the object Layer to run the benchmark.
|
||||
runPutObjectPartBenchmark(b, objLayer, objSize)
|
||||
}
|
||||
|
||||
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectBenchmark function.
|
||||
func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
if err != nil {
|
||||
@@ -170,18 +159,13 @@ func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
|
||||
}
|
||||
// cleaning up the backend by removing all the directories and files created on function return.
|
||||
defer removeRoots(disks)
|
||||
|
||||
// uses *testing.B and the object Layer to run the benchmark.
|
||||
runPutObjectBenchmark(b, objLayer, objSize)
|
||||
}
|
||||
|
||||
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for put object.
|
||||
func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
if err != nil {
|
||||
@@ -189,6 +173,7 @@ func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int)
|
||||
}
|
||||
// cleaning up the backend by removing all the directories and files created on function return.
|
||||
defer removeRoots(disks)
|
||||
|
||||
// uses *testing.B and the object Layer to run the benchmark.
|
||||
runPutObjectBenchmarkParallel(b, objLayer, objSize)
|
||||
}
|
||||
@@ -196,16 +181,10 @@ func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int)
|
||||
// Benchmark utility functions for ObjectLayer.GetObject().
|
||||
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
|
||||
func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
||||
err := obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -225,7 +204,7 @@ func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
// insert the object.
|
||||
var objInfo ObjectInfo
|
||||
objInfo, err = obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata, ObjectOptions{})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -240,7 +219,7 @@ func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var buffer = new(bytes.Buffer)
|
||||
err = obj.GetObject(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 {
|
||||
b.Error(err)
|
||||
}
|
||||
@@ -269,12 +248,6 @@ func generateBytesData(size int) []byte {
|
||||
|
||||
// creates XL/FS backend setup, obtains the object layer and calls the runGetObjectBenchmark function.
|
||||
func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
if err != nil {
|
||||
@@ -282,18 +255,13 @@ func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
|
||||
}
|
||||
// cleaning up the backend by removing all the directories and files created.
|
||||
defer removeRoots(disks)
|
||||
|
||||
// uses *testing.B and the object Layer to run the benchmark.
|
||||
runGetObjectBenchmark(b, objLayer, objSize)
|
||||
}
|
||||
|
||||
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for ObjectLayer.GetObject() .
|
||||
func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// create a temp XL/FS backend.
|
||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||
if err != nil {
|
||||
@@ -301,6 +269,7 @@ func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int)
|
||||
}
|
||||
// cleaning up the backend by removing all the directories and files created.
|
||||
defer removeRoots(disks)
|
||||
|
||||
// uses *testing.B and the object Layer to run the benchmark.
|
||||
runGetObjectBenchmarkParallel(b, objLayer, objSize)
|
||||
}
|
||||
@@ -308,16 +277,10 @@ func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int)
|
||||
// Parallel benchmark utility functions for ObjectLayer.PutObject().
|
||||
// Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark.
|
||||
func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
||||
err := obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -341,7 +304,7 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
for pb.Next() {
|
||||
// insert the object.
|
||||
objInfo, err := obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata, ObjectOptions{})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -359,16 +322,10 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
// Parallel benchmark utility functions for ObjectLayer.GetObject().
|
||||
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
|
||||
func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
b.Fatalf("Unable to initialize config. %s", err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// obtains random bucket name.
|
||||
bucket := getRandomBucketName()
|
||||
// create bucket.
|
||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
||||
err := obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -387,7 +344,7 @@ func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
// insert the object.
|
||||
var objInfo ObjectInfo
|
||||
objInfo, err = obj.PutObject(context.Background(), bucket, "object"+strconv.Itoa(i),
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata)
|
||||
mustGetHashReader(b, bytes.NewBuffer(textData), int64(len(textData)), md5hex, sha256hex), metadata, ObjectOptions{})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -403,7 +360,7 @@ func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
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 {
|
||||
b.Error(err)
|
||||
}
|
||||
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"hash"
|
||||
|
||||
"github.com/minio/highwayhash"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
sha256 "github.com/minio/sha256-simd"
|
||||
"golang.org/x/crypto/blake2b"
|
||||
)
|
||||
|
||||
// magic HH-256 key as HH-256 hash of the first 100 decimals of π as utf-8 string with a zero key.
|
||||
var magicHighwayHash256Key = []byte("\x4b\xe7\x34\xfa\x8e\x23\x8a\xcd\x26\x3e\x83\xe6\xbb\x96\x85\x52\x04\x0f\x93\x5d\xa3\x9f\x44\x14\x97\xe0\x9d\x13\x22\xde\x36\xa0")
|
||||
|
||||
// BitrotAlgorithm specifies a algorithm used for bitrot protection.
|
||||
type BitrotAlgorithm uint
|
||||
|
||||
const (
|
||||
// SHA256 represents the SHA-256 hash function
|
||||
SHA256 BitrotAlgorithm = 1 + iota
|
||||
// HighwayHash256 represents the HighwayHash-256 hash function
|
||||
HighwayHash256
|
||||
// BLAKE2b512 represents the BLAKE2b-512 hash function
|
||||
BLAKE2b512
|
||||
)
|
||||
|
||||
// DefaultBitrotAlgorithm is the default algorithm used for bitrot protection.
|
||||
const (
|
||||
DefaultBitrotAlgorithm = HighwayHash256
|
||||
)
|
||||
|
||||
var bitrotAlgorithms = map[BitrotAlgorithm]string{
|
||||
SHA256: "sha256",
|
||||
BLAKE2b512: "blake2b",
|
||||
HighwayHash256: "highwayhash256",
|
||||
}
|
||||
|
||||
// New returns a new hash.Hash calculating the given bitrot algorithm.
|
||||
func (a BitrotAlgorithm) New() hash.Hash {
|
||||
switch a {
|
||||
case SHA256:
|
||||
return sha256.New()
|
||||
case BLAKE2b512:
|
||||
b2, _ := blake2b.New512(nil) // New512 never returns an error if the key is nil
|
||||
return b2
|
||||
case HighwayHash256:
|
||||
hh, _ := highwayhash.New(magicHighwayHash256Key) // New will never return error since key is 256 bit
|
||||
return hh
|
||||
default:
|
||||
logger.CriticalIf(context.Background(), errors.New("Unsupported bitrot algorithm"))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Available reports whether the given algorithm is available.
|
||||
func (a BitrotAlgorithm) Available() bool {
|
||||
_, ok := bitrotAlgorithms[a]
|
||||
return ok
|
||||
}
|
||||
|
||||
// String returns the string identifier for a given bitrot algorithm.
|
||||
// If the algorithm is not supported String panics.
|
||||
func (a BitrotAlgorithm) String() string {
|
||||
name, ok := bitrotAlgorithms[a]
|
||||
if !ok {
|
||||
logger.CriticalIf(context.Background(), errors.New("Unsupported bitrot algorithm"))
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// NewBitrotVerifier returns a new BitrotVerifier implementing the given algorithm.
|
||||
func NewBitrotVerifier(algorithm BitrotAlgorithm, checksum []byte) *BitrotVerifier {
|
||||
return &BitrotVerifier{algorithm, checksum}
|
||||
}
|
||||
|
||||
// BitrotVerifier can be used to verify protected data.
|
||||
type BitrotVerifier struct {
|
||||
algorithm BitrotAlgorithm
|
||||
sum []byte
|
||||
}
|
||||
|
||||
// BitrotAlgorithmFromString returns a bitrot algorithm from the given string representation.
|
||||
// It returns 0 if the string representation does not match any supported algorithm.
|
||||
// The zero value of a bitrot algorithm is never supported.
|
||||
func BitrotAlgorithmFromString(s string) (a BitrotAlgorithm) {
|
||||
for alg, name := range bitrotAlgorithms {
|
||||
if name == s {
|
||||
return alg
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// To read bit-rot verified data.
|
||||
type bitrotReader struct {
|
||||
disk StorageAPI
|
||||
volume string
|
||||
filePath string
|
||||
verifier *BitrotVerifier // Holds the bit-rot info
|
||||
endOffset int64 // Affects the length of data requested in disk.ReadFile depending on Read()'s offset
|
||||
buf []byte // Holds bit-rot verified data
|
||||
}
|
||||
|
||||
// newBitrotReader returns bitrotReader.
|
||||
// Note that the buffer is allocated later in Read(). This is because we will know the buffer length only
|
||||
// during the bitrotReader.Read(). Depending on when parallelReader fails-over, the buffer length can be different.
|
||||
func newBitrotReader(disk StorageAPI, volume, filePath string, algo BitrotAlgorithm, endOffset int64, sum []byte) *bitrotReader {
|
||||
return &bitrotReader{
|
||||
disk: disk,
|
||||
volume: volume,
|
||||
filePath: filePath,
|
||||
verifier: &BitrotVerifier{algo, sum},
|
||||
endOffset: endOffset,
|
||||
buf: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// ReadChunk returns requested data.
|
||||
func (b *bitrotReader) ReadChunk(offset int64, length int64) ([]byte, error) {
|
||||
if b.buf == nil {
|
||||
b.buf = make([]byte, b.endOffset-offset)
|
||||
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if int64(len(b.buf)) < length {
|
||||
logger.LogIf(context.Background(), errLessData)
|
||||
return nil, errLessData
|
||||
}
|
||||
retBuf := b.buf[:length]
|
||||
b.buf = b.buf[length:]
|
||||
return retBuf, nil
|
||||
}
|
||||
|
||||
// To calculate the bit-rot of the written data.
|
||||
type bitrotWriter struct {
|
||||
disk StorageAPI
|
||||
volume string
|
||||
filePath string
|
||||
h hash.Hash
|
||||
}
|
||||
|
||||
// newBitrotWriter returns bitrotWriter.
|
||||
func newBitrotWriter(disk StorageAPI, volume, filePath string, algo BitrotAlgorithm) *bitrotWriter {
|
||||
return &bitrotWriter{
|
||||
disk: disk,
|
||||
volume: volume,
|
||||
filePath: filePath,
|
||||
h: algo.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// Append appends the data and while calculating the hash.
|
||||
func (b *bitrotWriter) Append(buf []byte) error {
|
||||
n, err := b.h.Write(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != len(buf) {
|
||||
logger.LogIf(context.Background(), errUnexpected)
|
||||
return errUnexpected
|
||||
}
|
||||
if err = b.disk.AppendFile(b.volume, b.filePath, buf); err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sum returns bit-rot sum.
|
||||
func (b *bitrotWriter) Sum() []byte {
|
||||
return b.h.Sum(nil)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBitrotReaderWriter(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
volume := "testvol"
|
||||
filePath := "testfile"
|
||||
|
||||
disk, err := newPosix(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
disk.MakeVol(volume)
|
||||
|
||||
writer := newBitrotWriter(disk, volume, filePath, HighwayHash256)
|
||||
|
||||
err = writer.Append([]byte("aaaaaaaaa"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = writer.Append([]byte("a"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = writer.Append([]byte("aaaaaaaaaa"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = writer.Append([]byte("aaaaa"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = writer.Append([]byte("aaaaaaaaaa"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
reader := newBitrotReader(disk, volume, filePath, HighwayHash256, 35, writer.Sum())
|
||||
|
||||
if _, err = reader.ReadChunk(0, 35); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -56,11 +56,12 @@ func (bf *BoolFlag) UnmarshalJSON(data []byte) (err error) {
|
||||
|
||||
// ParseBoolFlag - parses string into BoolFlag.
|
||||
func ParseBoolFlag(s string) (bf BoolFlag, err error) {
|
||||
if s == "on" {
|
||||
switch s {
|
||||
case "on":
|
||||
bf = true
|
||||
} else if s == "off" {
|
||||
case "off":
|
||||
bf = false
|
||||
} else {
|
||||
default:
|
||||
err = fmt.Errorf("invalid value ‘%s’ for BoolFlag", s)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
|
||||
@@ -100,7 +102,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
for i := range listObjectsV2Info.Objects {
|
||||
if listObjectsV2Info.Objects[i].IsEncrypted() {
|
||||
if crypto.IsEncrypted(listObjectsV2Info.Objects[i].UserDefined) {
|
||||
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].DecryptedSize()
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
@@ -166,7 +168,7 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
for i := range listObjectsInfo.Objects {
|
||||
if listObjectsInfo.Objects[i].IsEncrypted() {
|
||||
if crypto.IsEncrypted(listObjectsInfo.Objects[i].UserDefined) {
|
||||
listObjectsInfo.Objects[i].Size, err = listObjectsInfo.Objects[i].DecryptedSize()
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
|
||||
+10
-7
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
@@ -597,15 +598,17 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
}
|
||||
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if hasSSECustomerHeader(formValues) && !hasSuffix(object, slashSeparator) { // handle SSE-C requests
|
||||
if hasServerSideEncryptionHeader(formValues) && !hasSuffix(object, slashSeparator) { // handle SSE-C and SSE-S3 requests
|
||||
var reader io.Reader
|
||||
var key []byte
|
||||
key, err = ParseSSECustomerHeader(formValues)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
if crypto.SSEC.IsRequested(formValues) {
|
||||
key, err = ParseSSECustomerHeader(formValues)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
reader, err = newEncryptReader(hashReader, key, bucket, object, metadata)
|
||||
reader, err = newEncryptReader(hashReader, key, bucket, object, metadata, crypto.S3.IsRequested(formValues))
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -619,7 +622,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
}
|
||||
}
|
||||
|
||||
objInfo, err := objectAPI.PutObject(ctx, bucket, object, hashReader, metadata)
|
||||
objInfo, err := objectAPI.PutObject(ctx, bucket, object, hashReader, metadata, ObjectOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
|
||||
@@ -625,7 +625,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
for i := 0; i < 10; i++ {
|
||||
objectName := "test-object-" + strconv.Itoa(i)
|
||||
// uploading the object.
|
||||
_, err = obj.PutObject(context.Background(), bucketName, objectName, mustGetHashReader(t, bytes.NewBuffer(contentBytes), int64(len(contentBytes)), "", sha256sum), nil)
|
||||
_, err = obj.PutObject(context.Background(), bucketName, objectName, mustGetHashReader(t, bytes.NewBuffer(contentBytes), int64(len(contentBytes)), "", sha256sum), nil, ObjectOptions{})
|
||||
// if object upload fails stop the test.
|
||||
if err != nil {
|
||||
t.Fatalf("Put Object %d: Error uploading object: <ERROR> %v", i, err)
|
||||
|
||||
+19
-38
@@ -17,7 +17,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
@@ -27,8 +26,8 @@ import (
|
||||
"time"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
@@ -48,39 +47,6 @@ func checkUpdate(mode string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and load config from remote etcd or local config directory
|
||||
func initConfig() {
|
||||
if globalEtcdClient != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
resp, err := globalEtcdClient.Get(ctx, getConfigFile())
|
||||
cancel()
|
||||
// This means there are no entries in etcd with config file
|
||||
// So create a new config
|
||||
if err == nil && resp.Count == 0 {
|
||||
logger.FatalIf(newConfig(), "Unable to initialize minio config for the first time.")
|
||||
logger.Info("Created minio configuration file successfully at %v", globalEtcdClient.Endpoints())
|
||||
} else {
|
||||
// This means there is an entry in etcd, update it if required and proceed
|
||||
if err == nil && resp.Count > 0 {
|
||||
logger.FatalIf(migrateConfig(), "Config migration failed.")
|
||||
logger.FatalIf(loadConfig(), "Unable to load config version: '%s'.", serverConfigVersion)
|
||||
} else {
|
||||
logger.FatalIf(err, "Unable to load config version: '%s'.", serverConfigVersion)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if isFile(getConfigFile()) {
|
||||
logger.FatalIf(migrateConfig(), "Config migration failed")
|
||||
logger.FatalIf(loadConfig(), "Unable to load the configuration file")
|
||||
} else {
|
||||
// Config file does not exist, we create it fresh and return upon success.
|
||||
logger.FatalIf(newConfig(), "Unable to initialize minio config for the first time")
|
||||
logger.Info("Created minio configuration file successfully at " + getConfigDir())
|
||||
}
|
||||
}
|
||||
|
||||
// Load logger targets based on user's configuration
|
||||
func loadLoggers() {
|
||||
if globalServerConfig.Logger.Console.Enabled {
|
||||
@@ -99,9 +65,10 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
||||
|
||||
var configDir string
|
||||
|
||||
if ctx.IsSet("config-dir") {
|
||||
switch {
|
||||
case ctx.IsSet("config-dir"):
|
||||
configDir = ctx.String("config-dir")
|
||||
} else if ctx.GlobalIsSet("config-dir") {
|
||||
case ctx.GlobalIsSet("config-dir"):
|
||||
configDir = ctx.GlobalString("config-dir")
|
||||
// cli package does not expose parent's "config-dir" option. Below code is workaround.
|
||||
if configDir == "" || configDir == getConfigDir() {
|
||||
@@ -109,7 +76,7 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
||||
configDir = ctx.Parent().GlobalString("config-dir")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
// Neither local nor global config-dir option is provided. In this case, try to use
|
||||
// default config directory.
|
||||
configDir = getConfigDir()
|
||||
@@ -279,4 +246,18 @@ func handleCommonEnvVars() {
|
||||
globalIsEnvWORM = true
|
||||
globalWORMEnabled = bool(wormFlag)
|
||||
}
|
||||
|
||||
kmsConf, err := crypto.NewVaultConfig()
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to initialize hashicorp vault")
|
||||
}
|
||||
if kmsConf.Vault.Endpoint != "" {
|
||||
kms, err := crypto.NewVault(kmsConf)
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to initialize KMS")
|
||||
}
|
||||
globalKMS = kms
|
||||
globalKMSKeyID = kmsConf.Vault.Key.Name
|
||||
globalKMSConfig = kmsConf
|
||||
}
|
||||
}
|
||||
|
||||
+245
-143
@@ -24,12 +24,12 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/event/target"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
// Steps to move from version N to version N+1
|
||||
@@ -41,9 +41,9 @@ import (
|
||||
// 6. Make changes in config-current_test.go for any test change
|
||||
|
||||
// Config version
|
||||
const serverConfigVersion = "27"
|
||||
const serverConfigVersion = "28"
|
||||
|
||||
type serverConfig = serverConfigV27
|
||||
type serverConfig = serverConfigV28
|
||||
|
||||
var (
|
||||
// globalServerConfig server config.
|
||||
@@ -64,11 +64,21 @@ func (s *serverConfig) SetRegion(region string) {
|
||||
|
||||
// GetRegion get current region.
|
||||
func (s *serverConfig) GetRegion() string {
|
||||
if globalIsEnvRegion {
|
||||
return globalServerRegion
|
||||
}
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return s.Region
|
||||
}
|
||||
|
||||
// SetCredential sets new credential and returns the previous credential.
|
||||
func (s *serverConfig) SetCredential(creds auth.Credentials) (prevCred auth.Credentials) {
|
||||
if creds.IsValid() && globalActiveCred.IsValid() {
|
||||
globalActiveCred = creds
|
||||
}
|
||||
|
||||
// Save previous credential.
|
||||
prevCred = s.Credential
|
||||
|
||||
@@ -81,6 +91,9 @@ func (s *serverConfig) SetCredential(creds auth.Credentials) (prevCred auth.Cred
|
||||
|
||||
// GetCredentials get current credentials.
|
||||
func (s *serverConfig) GetCredential() auth.Credentials {
|
||||
if globalActiveCred.IsValid() {
|
||||
return globalActiveCred
|
||||
}
|
||||
return s.Credential
|
||||
}
|
||||
|
||||
@@ -104,16 +117,34 @@ func (s *serverConfig) SetStorageClass(standardClass, rrsClass storageClass) {
|
||||
// GetStorageClass reads storage class fields from current config.
|
||||
// It returns the standard and reduced redundancy storage class struct
|
||||
func (s *serverConfig) GetStorageClass() (storageClass, storageClass) {
|
||||
if globalIsStorageClass {
|
||||
return globalStandardStorageClass, globalRRStorageClass
|
||||
}
|
||||
if s == nil {
|
||||
return storageClass{}, storageClass{}
|
||||
}
|
||||
return s.StorageClass.Standard, s.StorageClass.RRS
|
||||
}
|
||||
|
||||
// GetBrowser get current credentials.
|
||||
func (s *serverConfig) GetBrowser() bool {
|
||||
if globalIsEnvBrowser {
|
||||
return globalIsBrowserEnabled
|
||||
}
|
||||
if s == nil {
|
||||
return true
|
||||
}
|
||||
return bool(s.Browser)
|
||||
}
|
||||
|
||||
// GetWorm get current credentials.
|
||||
func (s *serverConfig) GetWorm() bool {
|
||||
if globalIsEnvWORM {
|
||||
return globalWORMEnabled
|
||||
}
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
return bool(s.Worm)
|
||||
}
|
||||
|
||||
@@ -127,10 +158,24 @@ func (s *serverConfig) SetCacheConfig(drives, exclude []string, expiry int, maxu
|
||||
|
||||
// GetCacheConfig gets the current cache config
|
||||
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
|
||||
}
|
||||
|
||||
func (s *serverConfig) Validate() error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
if s.Version != serverConfigVersion {
|
||||
return fmt.Errorf("configuration version mismatch. Expected: ‘%s’, Got: ‘%s’", serverConfigVersion, s.Version)
|
||||
}
|
||||
@@ -208,19 +253,135 @@ func (s *serverConfig) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save config file to corresponding backend
|
||||
func Save(configFile string, data interface{}) error {
|
||||
return quick.SaveConfig(data, configFile, globalEtcdClient)
|
||||
func (s *serverConfig) loadFromEnvs() {
|
||||
// If env is set override the credentials from config file.
|
||||
if globalIsEnvCreds {
|
||||
s.SetCredential(globalActiveCred)
|
||||
}
|
||||
|
||||
if globalIsEnvBrowser {
|
||||
s.SetBrowser(globalIsBrowserEnabled)
|
||||
}
|
||||
|
||||
if globalIsEnvWORM {
|
||||
s.SetWorm(globalWORMEnabled)
|
||||
}
|
||||
|
||||
if globalIsEnvRegion {
|
||||
s.SetRegion(globalServerRegion)
|
||||
}
|
||||
|
||||
if globalIsEnvDomainName {
|
||||
s.Domain = globalDomainName
|
||||
}
|
||||
|
||||
if globalIsStorageClass {
|
||||
s.SetStorageClass(globalStandardStorageClass, globalRRStorageClass)
|
||||
}
|
||||
|
||||
if globalIsDiskCacheEnabled {
|
||||
s.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
|
||||
}
|
||||
|
||||
if globalKMS != nil {
|
||||
s.KMS = globalKMSConfig
|
||||
}
|
||||
}
|
||||
|
||||
// Load config from backend
|
||||
func Load(configFile string, data interface{}) (quick.Config, error) {
|
||||
return quick.LoadConfig(configFile, globalEtcdClient, data)
|
||||
}
|
||||
// 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()
|
||||
}
|
||||
|
||||
// GetVersion gets config version from backend
|
||||
func GetVersion(configFile string) (string, error) {
|
||||
return quick.GetVersion(configFile, globalEtcdClient)
|
||||
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.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
|
||||
@@ -262,6 +423,8 @@ func (s *serverConfig) ConfigDiff(t *serverConfig) string {
|
||||
return "MQTT Notification configuration differs"
|
||||
case !reflect.DeepEqual(s.Logger, t.Logger):
|
||||
return "Logger configuration differs"
|
||||
case !reflect.DeepEqual(s.KMS, t.KMS):
|
||||
return "KMS configuration differs"
|
||||
case reflect.DeepEqual(s, t):
|
||||
return ""
|
||||
default:
|
||||
@@ -290,6 +453,7 @@ func newServerConfig() *serverConfig {
|
||||
Expiry: globalCacheExpiry,
|
||||
MaxUse: globalCacheMaxUse,
|
||||
},
|
||||
KMS: crypto.KMSConfig{},
|
||||
Notify: notifier{},
|
||||
}
|
||||
|
||||
@@ -327,153 +491,89 @@ func newServerConfig() *serverConfig {
|
||||
return srvCfg
|
||||
}
|
||||
|
||||
// newConfig - initialize a new server config, saves env parameters if
|
||||
// found, otherwise use default parameters
|
||||
func newConfig() error {
|
||||
// Initialize server config.
|
||||
srvCfg, err := newQuickConfig(newServerConfig())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If env is set override the credentials from config file.
|
||||
if globalIsEnvCreds {
|
||||
srvCfg.SetCredential(globalActiveCred)
|
||||
}
|
||||
|
||||
if globalIsEnvBrowser {
|
||||
srvCfg.SetBrowser(globalIsBrowserEnabled)
|
||||
}
|
||||
|
||||
if globalIsEnvWORM {
|
||||
srvCfg.SetWorm(globalWORMEnabled)
|
||||
}
|
||||
|
||||
if globalIsEnvRegion {
|
||||
srvCfg.SetRegion(globalServerRegion)
|
||||
}
|
||||
|
||||
if globalIsEnvDomainName {
|
||||
srvCfg.Domain = globalDomainName
|
||||
}
|
||||
|
||||
if globalIsStorageClass {
|
||||
srvCfg.SetStorageClass(globalStandardStorageClass, globalRRStorageClass)
|
||||
}
|
||||
|
||||
if globalIsDiskCacheEnabled {
|
||||
srvCfg.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
|
||||
}
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
// Save the new config globally.
|
||||
// unlock the mutex.
|
||||
globalServerConfigMu.Lock()
|
||||
globalServerConfig = srvCfg
|
||||
globalServerConfigMu.Unlock()
|
||||
|
||||
// Save config into file.
|
||||
return Save(getConfigFile(), globalServerConfig)
|
||||
}
|
||||
|
||||
// newQuickConfig - initialize a new server config, with an allocated
|
||||
// quick.Config interface.
|
||||
func newQuickConfig(srvCfg *serverConfig) (*serverConfig, error) {
|
||||
qcfg, err := quick.NewConfig(srvCfg, globalEtcdClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
srvCfg.Config = qcfg
|
||||
return srvCfg, nil
|
||||
}
|
||||
|
||||
// getValidConfig - returns valid server configuration
|
||||
func getValidConfig() (*serverConfig, error) {
|
||||
srvCfg := &serverConfig{
|
||||
Region: globalMinioDefaultRegion,
|
||||
Browser: true,
|
||||
}
|
||||
|
||||
var err error
|
||||
srvCfg, err = newQuickConfig(srvCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configFile := getConfigFile()
|
||||
if err = srvCfg.Load(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = srvCfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return srvCfg, nil
|
||||
}
|
||||
|
||||
// loadConfig - loads a new config from disk, overrides params from env
|
||||
// if found and valid
|
||||
func loadConfig() error {
|
||||
srvCfg, err := getValidConfig()
|
||||
if err != nil {
|
||||
return uiErrInvalidConfig(nil).Msg(err.Error())
|
||||
}
|
||||
|
||||
// If env is set override the credentials from config file.
|
||||
if globalIsEnvCreds {
|
||||
srvCfg.SetCredential(globalActiveCred)
|
||||
}
|
||||
|
||||
if globalIsEnvBrowser {
|
||||
srvCfg.SetBrowser(globalIsBrowserEnabled)
|
||||
}
|
||||
|
||||
if globalIsEnvRegion {
|
||||
srvCfg.SetRegion(globalServerRegion)
|
||||
}
|
||||
|
||||
if globalIsEnvDomainName {
|
||||
srvCfg.Domain = globalDomainName
|
||||
}
|
||||
|
||||
if globalIsStorageClass {
|
||||
srvCfg.SetStorageClass(globalStandardStorageClass, globalRRStorageClass)
|
||||
}
|
||||
|
||||
if globalIsDiskCacheEnabled {
|
||||
srvCfg.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
|
||||
}
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
globalServerConfigMu.Lock()
|
||||
globalServerConfig = srvCfg
|
||||
func (s *serverConfig) loadToCachedConfigs() {
|
||||
if !globalIsEnvCreds {
|
||||
globalActiveCred = globalServerConfig.GetCredential()
|
||||
globalActiveCred = s.GetCredential()
|
||||
}
|
||||
if !globalIsEnvBrowser {
|
||||
globalIsBrowserEnabled = globalServerConfig.GetBrowser()
|
||||
globalIsBrowserEnabled = s.GetBrowser()
|
||||
}
|
||||
if !globalIsEnvWORM {
|
||||
globalWORMEnabled = globalServerConfig.GetWorm()
|
||||
globalWORMEnabled = s.GetWorm()
|
||||
}
|
||||
if !globalIsEnvRegion {
|
||||
globalServerRegion = globalServerConfig.GetRegion()
|
||||
globalServerRegion = s.GetRegion()
|
||||
}
|
||||
if !globalIsEnvDomainName {
|
||||
globalDomainName = globalServerConfig.Domain
|
||||
globalDomainName = s.Domain
|
||||
}
|
||||
if !globalIsStorageClass {
|
||||
globalStandardStorageClass, globalRRStorageClass = globalServerConfig.GetStorageClass()
|
||||
globalStandardStorageClass, globalRRStorageClass = s.GetStorageClass()
|
||||
}
|
||||
if !globalIsDiskCacheEnabled {
|
||||
cacheConf := globalServerConfig.GetCacheConfig()
|
||||
cacheConf := s.GetCacheConfig()
|
||||
globalCacheDrives = cacheConf.Drives
|
||||
globalCacheExcludes = cacheConf.Exclude
|
||||
globalCacheExpiry = cacheConf.Expiry
|
||||
globalCacheMaxUse = cacheConf.MaxUse
|
||||
}
|
||||
if globalKMS == nil {
|
||||
globalKMSConfig = s.KMS
|
||||
if kms, err := crypto.NewVault(globalKMSConfig); err == nil {
|
||||
globalKMS = kms
|
||||
globalKMSKeyID = globalKMSConfig.Vault.Key.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newConfig - initialize a new server config, saves env parameters if
|
||||
// found, otherwise use default parameters
|
||||
func newConfig(objAPI ObjectLayer) error {
|
||||
// Initialize server config.
|
||||
srvCfg := newServerConfig()
|
||||
|
||||
// Override any values from ENVs.
|
||||
srvCfg.loadFromEnvs()
|
||||
|
||||
// Load values to cached global values.
|
||||
srvCfg.loadToCachedConfigs()
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
globalServerConfigMu.Lock()
|
||||
globalServerConfig = srvCfg
|
||||
globalServerConfigMu.Unlock()
|
||||
|
||||
// Save config into file.
|
||||
return saveServerConfig(context.Background(), objAPI, globalServerConfig)
|
||||
}
|
||||
|
||||
// getValidConfig - returns valid server configuration
|
||||
func getValidConfig(objAPI ObjectLayer) (*serverConfig, error) {
|
||||
srvCfg, err := readServerConfig(context.Background(), objAPI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return srvCfg, srvCfg.Validate()
|
||||
}
|
||||
|
||||
// loadConfig - loads a new config from disk, overrides params from env
|
||||
// if found and valid
|
||||
func loadConfig(objAPI ObjectLayer) error {
|
||||
srvCfg, err := getValidConfig(objAPI)
|
||||
if err != nil {
|
||||
return uiErrInvalidConfig(nil).Msg(err.Error())
|
||||
}
|
||||
|
||||
// Override any values from ENVs.
|
||||
srvCfg.loadFromEnvs()
|
||||
|
||||
// Load values to cached global values.
|
||||
srvCfg.loadToCachedConfigs()
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
globalServerConfigMu.Lock()
|
||||
globalServerConfig = srvCfg
|
||||
globalServerConfigMu.Unlock()
|
||||
|
||||
return nil
|
||||
@@ -486,7 +586,9 @@ func loadConfig() error {
|
||||
// * Handle the configuration in this function to create/add into TargetList.
|
||||
func getNotificationTargets(config *serverConfig) *event.TargetList {
|
||||
targetList := event.NewTargetList()
|
||||
|
||||
if config == nil {
|
||||
return targetList
|
||||
}
|
||||
for id, args := range config.Notify.AMQP {
|
||||
if args.Enable {
|
||||
newTarget, err := target.NewAMQPTarget(id, args)
|
||||
|
||||
+52
-41
@@ -17,9 +17,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
@@ -27,12 +27,15 @@ import (
|
||||
)
|
||||
|
||||
func TestServerConfig(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
objLayer, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(fsDir)
|
||||
|
||||
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
if globalServerConfig.GetRegion() != globalMinioDefaultRegion {
|
||||
t.Errorf("Expecting region `us-east-1` found %s", globalServerConfig.GetRegion())
|
||||
@@ -49,16 +52,12 @@ func TestServerConfig(t *testing.T) {
|
||||
t.Errorf("Expecting version %s found %s", globalServerConfig.GetVersion(), serverConfigVersion)
|
||||
}
|
||||
|
||||
// Attempt to save.
|
||||
if err := globalServerConfig.Save(getConfigFile()); err != nil {
|
||||
if err := saveServerConfig(context.Background(), objLayer, globalServerConfig); err != nil {
|
||||
t.Fatalf("Unable to save updated config file %s", err)
|
||||
}
|
||||
|
||||
// Do this only once here.
|
||||
setConfigDir(rootPath)
|
||||
|
||||
// Initialize server config.
|
||||
if err := loadConfig(); err != nil {
|
||||
if err := loadConfig(objLayer); err != nil {
|
||||
t.Fatalf("Unable to initialize from updated config file %s", err)
|
||||
}
|
||||
}
|
||||
@@ -68,6 +67,9 @@ func TestServerConfigWithEnvs(t *testing.T) {
|
||||
os.Setenv("MINIO_BROWSER", "off")
|
||||
defer os.Unsetenv("MINIO_BROWSER")
|
||||
|
||||
os.Setenv("MINIO_WORM", "on")
|
||||
defer os.Unsetenv("MINIO_WORM")
|
||||
|
||||
os.Setenv("MINIO_ACCESS_KEY", "minio")
|
||||
defer os.Unsetenv("MINIO_ACCESS_KEY")
|
||||
|
||||
@@ -82,61 +84,70 @@ func TestServerConfigWithEnvs(t *testing.T) {
|
||||
|
||||
defer resetGlobalIsEnvs()
|
||||
|
||||
// Get test root.
|
||||
rootPath, err := getTestRoot()
|
||||
objLayer, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(fsDir)
|
||||
|
||||
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
|
||||
globalObjLayerMutex.Lock()
|
||||
globalObjectAPI = objLayer
|
||||
globalObjLayerMutex.Unlock()
|
||||
|
||||
serverHandleEnvVars()
|
||||
|
||||
// Do this only once here.
|
||||
setConfigDir(rootPath)
|
||||
|
||||
// Init config
|
||||
initConfig()
|
||||
initConfig(objLayer)
|
||||
|
||||
// remove the root directory after the test ends.
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
// Check if serverConfig has
|
||||
// Check if serverConfig has browser disabled
|
||||
if globalServerConfig.GetBrowser() {
|
||||
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" {
|
||||
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()
|
||||
|
||||
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" {
|
||||
t.Errorf("Expecting access key to be `minio123` found %s", cred.SecretKey)
|
||||
t.Errorf("Expected access key to be `minio123`, found %s", cred.SecretKey)
|
||||
}
|
||||
|
||||
// Check if serverConfig has the correct domain
|
||||
if globalServerConfig.Domain != "domain.com" {
|
||||
t.Errorf("Expecting Domain to be `domain.com` found " + globalServerConfig.Domain)
|
||||
t.Errorf("Expected Domain to be `domain.com`, found " + globalServerConfig.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests config validator..
|
||||
func TestValidateConfig(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
objLayer, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(fsDir)
|
||||
|
||||
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
|
||||
// remove the root directory after the test ends.
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
configPath := filepath.Join(rootPath, minioConfigFile)
|
||||
|
||||
configPath := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
v := serverConfigVersion
|
||||
|
||||
testCases := []struct {
|
||||
@@ -226,14 +237,14 @@ func TestValidateConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
if werr := ioutil.WriteFile(configPath, []byte(testCase.configData), 0700); werr != nil {
|
||||
t.Fatal(werr)
|
||||
if err = saveConfig(objLayer, configPath, []byte(testCase.configData)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, verr := getValidConfig()
|
||||
if testCase.shouldPass && verr != nil {
|
||||
t.Errorf("Test %d, should pass but it failed with err = %v", i+1, verr)
|
||||
_, err = getValidConfig(objLayer)
|
||||
if testCase.shouldPass && err != nil {
|
||||
t.Errorf("Test %d, should pass but it failed with err = %v", i+1, err)
|
||||
}
|
||||
if !testCase.shouldPass && verr == nil {
|
||||
if !testCase.shouldPass && err == nil {
|
||||
t.Errorf("Test %d, should fail but it succeeded.", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,6 @@ const (
|
||||
// Default minio configuration directory where below configuration files/directories are stored.
|
||||
defaultMinioConfigDir = ".minio"
|
||||
|
||||
// Minio configuration file.
|
||||
minioConfigFile = "config.json"
|
||||
|
||||
// Directory contains below files/directories for HTTPS configuration.
|
||||
certsDir = "certs"
|
||||
|
||||
|
||||
+81
-1
@@ -17,10 +17,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
@@ -33,7 +36,22 @@ import (
|
||||
// DO NOT EDIT following message template, please open a github issue to discuss instead.
|
||||
var configMigrateMSGTemplate = "Configuration file %s migrated from version '%s' to '%s' successfully."
|
||||
|
||||
// Migrates all config versions from "1" to "18".
|
||||
// Save config file to corresponding backend
|
||||
func Save(configFile string, data interface{}) error {
|
||||
return quick.SaveConfig(data, configFile, globalEtcdClient)
|
||||
}
|
||||
|
||||
// Load config from backend
|
||||
func Load(configFile string, data interface{}) (quick.Config, error) {
|
||||
return quick.LoadConfig(configFile, globalEtcdClient, data)
|
||||
}
|
||||
|
||||
// GetVersion gets config version from backend
|
||||
func GetVersion(configFile string) (string, error) {
|
||||
return quick.GetVersion(configFile, globalEtcdClient)
|
||||
}
|
||||
|
||||
// Migrates all config versions from "1" to "28".
|
||||
func migrateConfig() error {
|
||||
// Purge all configs with version '1',
|
||||
// this is a special case since version '1' used
|
||||
@@ -45,6 +63,9 @@ func migrateConfig() error {
|
||||
// Load only config version information.
|
||||
version, err := GetVersion(getConfigFile())
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -193,6 +214,11 @@ func migrateConfig() error {
|
||||
return err
|
||||
}
|
||||
fallthrough
|
||||
case "27":
|
||||
if err = migrateV27ToV28(); err != nil {
|
||||
return err
|
||||
}
|
||||
fallthrough
|
||||
case serverConfigVersion:
|
||||
// No migration needed. this always points to current version.
|
||||
err = nil
|
||||
@@ -2355,3 +2381,57 @@ func migrateV26ToV27() error {
|
||||
logger.Info(configMigrateMSGTemplate, configFile, "26", "27")
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateV27ToV28() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
// config V28 is backward compatible with V27, load the old
|
||||
// config file in serverConfigV28 struct and initialize KMSConfig
|
||||
srvConfig := &serverConfigV28{}
|
||||
_, err := quick.LoadConfig(configFile, globalEtcdClient, srvConfig)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("Unable to load config file. %v", err)
|
||||
}
|
||||
|
||||
if srvConfig.Version != "27" {
|
||||
return nil
|
||||
}
|
||||
|
||||
srvConfig.Version = "28"
|
||||
srvConfig.KMS = crypto.KMSConfig{}
|
||||
if err = quick.SaveConfig(srvConfig, configFile, globalEtcdClient); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %v", err)
|
||||
}
|
||||
|
||||
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Migrates '.minio.sys/config.json' v27 to v28.
|
||||
func migrateMinioSysConfig(objAPI ObjectLayer) error {
|
||||
return migrateV27ToV28MinioSys(objAPI)
|
||||
}
|
||||
|
||||
func migrateV27ToV28MinioSys(objAPI ObjectLayer) error {
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
srvConfig, err := readServerConfig(context.Background(), objAPI)
|
||||
if err == errConfigNotFound {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("Unable to load config file. %v", err)
|
||||
}
|
||||
if srvConfig.Version != "27" {
|
||||
return nil
|
||||
}
|
||||
|
||||
srvConfig.Version = "28"
|
||||
srvConfig.KMS = crypto.KMSConfig{}
|
||||
if err = saveServerConfig(context.Background(), objAPI, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %v", err)
|
||||
}
|
||||
|
||||
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
|
||||
return nil
|
||||
}
|
||||
|
||||
+45
-28
@@ -25,15 +25,26 @@ import (
|
||||
|
||||
// Test if config v1 is purged
|
||||
func TestServerConfigMigrateV1(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
objLayer, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(fsDir)
|
||||
err = newTestConfig(globalMinioDefaultRegion, objLayer)
|
||||
if err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
setConfigDir(rootPath)
|
||||
|
||||
globalObjLayerMutex.Lock()
|
||||
globalObjectAPI = objLayer
|
||||
globalObjLayerMutex.Unlock()
|
||||
|
||||
// Create a V1 config json file and store it
|
||||
configJSON := "{ \"version\":\"1\", \"accessKeyId\":\"abcde\", \"secretAccessKey\":\"abcdefgh\"}"
|
||||
configPath := rootPath + "/fsUsers.json"
|
||||
@@ -45,13 +56,14 @@ func TestServerConfigMigrateV1(t *testing.T) {
|
||||
if err := migrateConfig(); err != nil {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
// Check if config v1 is removed from filesystem
|
||||
if _, err := os.Stat(configPath); err == nil || !os.IsNotExist(err) {
|
||||
t.Fatal("Config V1 file is not purged")
|
||||
}
|
||||
|
||||
// Initialize server config and check again if everything is fine
|
||||
if err := loadConfig(); err != nil {
|
||||
if err := loadConfig(objLayer); err != nil {
|
||||
t.Fatalf("Unable to initialize from updated config file %s", err)
|
||||
}
|
||||
}
|
||||
@@ -59,20 +71,13 @@ func TestServerConfigMigrateV1(t *testing.T) {
|
||||
// Test if all migrate code returns nil when config file does not
|
||||
// exist
|
||||
func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
t.Fatal(err)
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
setConfigDir(rootPath)
|
||||
configPath := rootPath + "/" + minioConfigFile
|
||||
|
||||
// Remove config file
|
||||
if err := os.Remove(configPath); err != nil {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
if err := migrateV2ToV3(); err != nil {
|
||||
t.Fatal("migrate v2 to v3 should succeed when no config file is found")
|
||||
@@ -149,18 +154,26 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
||||
if err := migrateV26ToV27(); err != nil {
|
||||
t.Fatal("migrate v26 to v27 should succeed when no config file is found")
|
||||
}
|
||||
if err := migrateV27ToV28(); err != nil {
|
||||
t.Fatal("migrate v27 to v28 should succeed when no config file is found")
|
||||
}
|
||||
}
|
||||
|
||||
// Test if a config migration from v2 to v27 is successfully done
|
||||
func TestServerConfigMigrateV2toV27(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
// Test if a config migration from v2 to v28 is successfully done
|
||||
func TestServerConfigMigrateV2toV28(t *testing.T) {
|
||||
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
t.Fatal(err)
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
setConfigDir(rootPath)
|
||||
|
||||
objLayer, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(fsDir)
|
||||
|
||||
configPath := rootPath + "/" + minioConfigFile
|
||||
|
||||
// Create a corrupted config file
|
||||
@@ -186,8 +199,12 @@ func TestServerConfigMigrateV2toV27(t *testing.T) {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
if err := migrateConfigToMinioSys(objLayer); err != nil {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
// Initialize server config and check again if everything is fine
|
||||
if err := loadConfig(); err != nil {
|
||||
if err := loadConfig(objLayer); err != nil {
|
||||
t.Fatalf("Unable to initialize from updated config file %s", err)
|
||||
}
|
||||
|
||||
@@ -208,13 +225,11 @@ func TestServerConfigMigrateV2toV27(t *testing.T) {
|
||||
|
||||
// Test if all migrate code returns error with corrupted config files
|
||||
func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
t.Fatal(err)
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
setConfigDir(rootPath)
|
||||
configPath := rootPath + "/" + minioConfigFile
|
||||
|
||||
@@ -299,17 +314,19 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
||||
if err := migrateV26ToV27(); err == nil {
|
||||
t.Fatal("migrateConfigV26ToV27() should fail with a corrupted json")
|
||||
}
|
||||
|
||||
if err := migrateV27ToV28(); err == nil {
|
||||
t.Fatal("migrateConfigV27ToV28() should fail with a corrupted json")
|
||||
}
|
||||
}
|
||||
|
||||
// Test if all migrate code returns error with corrupted config files
|
||||
func TestServerConfigMigrateCorruptedConfig(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
t.Fatal(err)
|
||||
}
|
||||
// remove the root directory after the test ends.
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
setConfigDir(rootPath)
|
||||
configPath := rootPath + "/" + minioConfigFile
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package cmd
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/event/target"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
@@ -722,3 +723,36 @@ type serverConfigV27 struct {
|
||||
// Logger configuration
|
||||
Logger loggerConfig `json:"logger"`
|
||||
}
|
||||
|
||||
// serverConfigV28 is just like version '27', additionally
|
||||
// storing KMS config
|
||||
//
|
||||
// IMPORTANT NOTE: When updating this struct make sure that
|
||||
// serverConfig.ConfigDiff() is updated as necessary.
|
||||
type serverConfigV28 struct {
|
||||
quick.Config `json:"-"` // ignore interfaces
|
||||
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
Credential auth.Credentials `json:"credential"`
|
||||
Region string `json:"region"`
|
||||
Browser BoolFlag `json:"browser"`
|
||||
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 notifier `json:"notify"`
|
||||
|
||||
// Logger configuration
|
||||
Logger loggerConfig `json:"logger"`
|
||||
}
|
||||
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* 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"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
const (
|
||||
minioConfigPrefix = "config"
|
||||
|
||||
// Minio configuration file.
|
||||
minioConfigFile = "config.json"
|
||||
|
||||
// Minio backup file
|
||||
minioConfigBackupFile = minioConfigFile + ".backup"
|
||||
)
|
||||
|
||||
func saveServerConfig(ctx context.Context, objAPI ObjectLayer, config *serverConfig) error {
|
||||
if err := quick.CheckData(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(config, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
if globalEtcdClient != nil {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
_, err = globalEtcdClient.Put(timeoutCtx, configFile, string(data))
|
||||
defer cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a backup of the current config
|
||||
reader, err := readConfig(ctx, objAPI, configFile)
|
||||
if err == nil {
|
||||
var oldData []byte
|
||||
oldData, err = ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backupConfigFile := path.Join(minioConfigPrefix, minioConfigBackupFile)
|
||||
err = saveConfig(objAPI, backupConfigFile, oldData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err != errConfigNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the new config in the std config path
|
||||
return saveConfig(objAPI, configFile, data)
|
||||
}
|
||||
|
||||
func readConfigEtcd(configFile string) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
resp, err := globalEtcdClient.Get(ctx, configFile)
|
||||
defer cancel()
|
||||
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
|
||||
}
|
||||
|
||||
func readServerConfig(ctx context.Context, objAPI ObjectLayer) (*serverConfig, error) {
|
||||
var configData []byte
|
||||
var err error
|
||||
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||
if globalEtcdClient != nil {
|
||||
configData, err = readConfigEtcd(configFile)
|
||||
} else {
|
||||
var reader io.Reader
|
||||
reader, err = readConfig(ctx, objAPI, configFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configData, err = ioutil.ReadAll(reader)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
configData = bytes.Replace(configData, []byte("\r\n"), []byte("\n"), -1)
|
||||
}
|
||||
|
||||
if err = quick.CheckDuplicateKeys(string(configData)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config = &serverConfig{}
|
||||
if err := json.Unmarshal(configData, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := quick.CheckData(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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, ObjectOptions{}); err != nil {
|
||||
// Convert ObjectNotFound, Quorum errors into errConfigNotFound
|
||||
if isErrObjectNotFound(err) || isInsufficientReadQuorum(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, ObjectOptions{})
|
||||
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, "", ObjectOptions{}); err != nil {
|
||||
// Convert ObjectNotFound, IncompleteBody and Quorum errors into errConfigNotFound
|
||||
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.
|
||||
type ConfigSys struct{}
|
||||
|
||||
// Load - load config.json.
|
||||
func (sys *ConfigSys) Load(objAPI ObjectLayer) error {
|
||||
return sys.Init(objAPI)
|
||||
}
|
||||
|
||||
// Init - initializes config system from config.json.
|
||||
func (sys *ConfigSys) Init(objAPI ObjectLayer) error {
|
||||
if objAPI == nil {
|
||||
return errInvalidArgument
|
||||
}
|
||||
return initConfig(objAPI)
|
||||
}
|
||||
|
||||
// NewConfigSys - creates new config system object.
|
||||
func NewConfigSys() *ConfigSys {
|
||||
return &ConfigSys{}
|
||||
}
|
||||
|
||||
// Migrates ${HOME}/.minio/config.json to '<export_path>/.minio.sys/config/config.json'
|
||||
func migrateConfigToMinioSys(objAPI ObjectLayer) error {
|
||||
defer os.Rename(getConfigFile(), getConfigFile()+".deprecated")
|
||||
|
||||
// Verify if backend already has the file.
|
||||
if err := checkServerConfig(context.Background(), objAPI); 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)
|
||||
}
|
||||
|
||||
// Initialize and load config from remote etcd or local config directory
|
||||
func initConfig(objAPI ObjectLayer) error {
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
if globalEtcdClient != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
resp, err := globalEtcdClient.Get(ctx, getConfigFile())
|
||||
cancel()
|
||||
if err == nil && resp.Count > 0 {
|
||||
return migrateConfig()
|
||||
}
|
||||
} else {
|
||||
if isFile(getConfigFile()) {
|
||||
if err := migrateConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Migrates ${HOME}/.minio/config.json or config.json.deprecated
|
||||
// to '<export_path>/.minio.sys/config/config.json'
|
||||
// ignore if the file doesn't exist.
|
||||
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.
|
||||
if err = newConfig(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := migrateMinioSysConfig(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return loadConfig(objAPI)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Minio Cloud Storage, (C) 2015, 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 crypto
|
||||
|
||||
// KMSConfig has the KMS config for hashicorp vault
|
||||
type KMSConfig struct {
|
||||
Vault VaultConfig `json:"vault"`
|
||||
}
|
||||
@@ -43,10 +43,17 @@ var (
|
||||
// base64-encoded string or not 256 bits long.
|
||||
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
|
||||
// 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.
|
||||
ErrCustomerKeyMD5Mismatch = errors.New("The provided SSE-C key MD5 does not match the computed MD5 of the SSE-C key")
|
||||
// ErrIncompatibleEncryptionMethod indicates that both SSE-C headers and SSE-S3 headers were specified, and are incompatible
|
||||
// The client needs to remove the SSE-S3 header or the SSE-C headers
|
||||
ErrIncompatibleEncryptionMethod = errors.New("Server side encryption specified with both SSE-C and SSE-S3 headers")
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
+43
-5
@@ -19,11 +19,22 @@ import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SSEHeader is the general AWS SSE HTTP header key.
|
||||
const SSEHeader = "X-Amz-Server-Side-Encryption"
|
||||
|
||||
const (
|
||||
// SSEKmsID is the HTTP header key referencing the SSE-KMS
|
||||
// key ID.
|
||||
SSEKmsID = SSEHeader + "-Aws-Kms-Key-Id"
|
||||
|
||||
// SSEKmsContext is the HTTP header key referencing the
|
||||
// SSE-KMS encryption context.
|
||||
SSEKmsContext = SSEHeader + "-Context"
|
||||
)
|
||||
|
||||
const (
|
||||
// SSECAlgorithm is the HTTP header key referencing
|
||||
// the SSE-C algorithm.
|
||||
@@ -52,10 +63,16 @@ const (
|
||||
SSECopyKeyMD5 = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5"
|
||||
)
|
||||
|
||||
// SSEAlgorithmAES256 is the only supported value for the SSE-S3 or SSE-C algorithm header.
|
||||
// For SSE-S3 see: https://docs.aws.amazon.com/AmazonS3/latest/dev/SSEUsingRESTAPI.html
|
||||
// For SSE-C see: https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html
|
||||
const SSEAlgorithmAES256 = "AES256"
|
||||
const (
|
||||
// SSEAlgorithmAES256 is the only supported value for the SSE-S3 or SSE-C algorithm header.
|
||||
// For SSE-S3 see: https://docs.aws.amazon.com/AmazonS3/latest/dev/SSEUsingRESTAPI.html
|
||||
// For SSE-C see: https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html
|
||||
SSEAlgorithmAES256 = "AES256"
|
||||
|
||||
// SSEAlgorithmKMS is the value of 'X-Amz-Server-Side-Encryption' for SSE-KMS.
|
||||
// See: https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html
|
||||
SSEAlgorithmKMS = "aws:kms"
|
||||
)
|
||||
|
||||
// S3 represents AWS SSE-S3. It provides functionality to handle
|
||||
// SSE-S3 requests.
|
||||
@@ -67,7 +84,7 @@ type s3 struct{}
|
||||
// the S3 client requests SSE-S3.
|
||||
func (s3) IsRequested(h http.Header) bool {
|
||||
_, ok := h[SSEHeader]
|
||||
return ok
|
||||
return ok && strings.ToLower(h.Get(SSEHeader)) != SSEAlgorithmKMS // Return only true if the SSE header is specified and does not contain the SSE-KMS value
|
||||
}
|
||||
|
||||
// ParseHTTP parses the SSE-S3 related HTTP headers and checks
|
||||
@@ -79,6 +96,27 @@ func (s3) ParseHTTP(h http.Header) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// S3KMS represents AWS SSE-KMS. It provides functionality to
|
||||
// handle SSE-KMS requests.
|
||||
var S3KMS = s3KMS{}
|
||||
|
||||
type s3KMS struct{}
|
||||
|
||||
// IsRequested returns true if the HTTP headers indicates that
|
||||
// the S3 client requests SSE-KMS.
|
||||
func (s3KMS) IsRequested(h http.Header) bool {
|
||||
if _, ok := h[SSEKmsID]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := h[SSEKmsContext]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := h[SSEHeader]; ok {
|
||||
return strings.ToUpper(h.Get(SSEHeader)) != SSEAlgorithmAES256 // Return only true if the SSE header is specified and does not contain the SSE-S3 value
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
// SSEC represents AWS SSE-C. It provides functionality to handle
|
||||
// SSE-C requests.
|
||||
|
||||
@@ -19,14 +19,49 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var kmsIsRequestedTests = []struct {
|
||||
Header http.Header
|
||||
Expected bool
|
||||
}{
|
||||
{Header: http.Header{}, Expected: false}, // 0
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"aws:kms"}}, Expected: true}, // 1
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": []string{"0839-9047947-844842874-481"}}, Expected: true}, // 2
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption-Context": []string{"7PpPLAK26ONlVUGOWlusfg=="}}, Expected: true}, // 3
|
||||
{
|
||||
Header: http.Header{
|
||||
"X-Amz-Server-Side-Encryption": []string{""},
|
||||
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": []string{""},
|
||||
"X-Amz-Server-Side-Encryption-Context": []string{""},
|
||||
},
|
||||
Expected: true,
|
||||
}, // 4
|
||||
{
|
||||
Header: http.Header{
|
||||
"X-Amz-Server-Side-Encryption": []string{"AES256"},
|
||||
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": []string{""},
|
||||
},
|
||||
Expected: true,
|
||||
}, // 5
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES256"}}, Expected: false}, // 6
|
||||
}
|
||||
|
||||
func TestKMSIsRequested(t *testing.T) {
|
||||
for i, test := range kmsIsRequestedTests {
|
||||
if got := S3KMS.IsRequested(test.Header); got != test.Expected {
|
||||
t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var s3IsRequestedTests = []struct {
|
||||
Header http.Header
|
||||
Expected bool
|
||||
}{
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES256"}}, Expected: true}, // 0
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES-256"}}, Expected: true}, // 1
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{""}}, Expected: true}, // 2
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryptio": []string{"AES256"}}, Expected: false}, // 3
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES256"}}, Expected: true}, // 0
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES-256"}}, Expected: true}, // 1
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{""}}, Expected: true}, // 2
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryptio": []string{"AES256"}}, Expected: false}, // 3
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{SSEAlgorithmKMS}}, Expected: false}, // 4
|
||||
}
|
||||
|
||||
func TestS3IsRequested(t *testing.T) {
|
||||
|
||||
+16
-4
@@ -35,8 +35,8 @@ import (
|
||||
type ObjectKey [32]byte
|
||||
|
||||
// GenerateKey generates a unique ObjectKey from a 256 bit external key
|
||||
// and a source of randomness. If random is nil the default PRNG of system
|
||||
// (crypto/rand) is used.
|
||||
// and a source of randomness. If random is nil the default PRNG of the
|
||||
// system (crypto/rand) is used.
|
||||
func GenerateKey(extKey [32]byte, random io.Reader) (key ObjectKey) {
|
||||
if random == nil {
|
||||
random = rand.Reader
|
||||
@@ -52,6 +52,19 @@ func GenerateKey(extKey [32]byte, random io.Reader) (key ObjectKey) {
|
||||
return key
|
||||
}
|
||||
|
||||
// GenerateIV generates a new random 256 bit IV from the provided source
|
||||
// of randomness. If random is nil the default PRNG of the system
|
||||
// (crypto/rand) is used.
|
||||
func GenerateIV(random io.Reader) (iv [32]byte) {
|
||||
if random == nil {
|
||||
random = rand.Reader
|
||||
}
|
||||
if _, err := io.ReadFull(random, iv[:]); err != nil {
|
||||
logger.CriticalIf(context.Background(), errOutOfEntropy)
|
||||
}
|
||||
return iv
|
||||
}
|
||||
|
||||
// SealedKey represents a sealed object key. It can be stored
|
||||
// at an untrusted location.
|
||||
type SealedKey struct {
|
||||
@@ -74,7 +87,6 @@ func (key ObjectKey) Seal(extKey, iv [32]byte, domain, bucket, object string) Se
|
||||
mac.Write([]byte(SealAlgorithm))
|
||||
mac.Write([]byte(path.Join(bucket, object))) // use path.Join for canonical 'bucket/object'
|
||||
mac.Sum(sealingKey[:0])
|
||||
|
||||
if n, err := sio.Encrypt(&encryptedKey, bytes.NewReader(key[:]), sio.Config{Key: sealingKey[:]}); n != 64 || err != nil {
|
||||
logger.CriticalIf(context.Background(), errors.New("Unable to generate sealed key"))
|
||||
}
|
||||
@@ -112,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 {
|
||||
return err // TODO(aead): upgrade sio to use sio.Error
|
||||
return ErrSecretKeyMismatch
|
||||
}
|
||||
copy(key[:], decryptedKey.Bytes())
|
||||
return nil
|
||||
|
||||
@@ -61,6 +61,32 @@ func TestGenerateKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var generateIVTests = []struct {
|
||||
Random io.Reader
|
||||
ShouldPass bool
|
||||
}{
|
||||
{Random: nil, ShouldPass: true}, // 0
|
||||
{Random: rand.Reader, ShouldPass: true}, // 1
|
||||
{Random: shortRandom(32), ShouldPass: true}, // 2
|
||||
{Random: shortRandom(31), ShouldPass: false}, // 3
|
||||
}
|
||||
|
||||
func TestGenerateIV(t *testing.T) {
|
||||
defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
|
||||
logger.Disable = true
|
||||
|
||||
for i, test := range generateIVTests {
|
||||
i, test := i, test
|
||||
func() {
|
||||
defer recoverTest(i, test.ShouldPass, t)
|
||||
iv := GenerateIV(test.Random)
|
||||
if iv == [32]byte{} {
|
||||
t.Errorf("Test %d: generated IV is zero IV", i) // check that we generate random and unique IV
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
var sealUnsealKeyTests = []struct {
|
||||
SealExtKey, SealIV [32]byte
|
||||
SealDomain, SealBucket, SealObject string
|
||||
|
||||
@@ -65,6 +65,14 @@ const (
|
||||
InsecureSealAlgorithm = "DARE-SHA256"
|
||||
)
|
||||
|
||||
// String returns the SSE domain as string. For SSE-S3 the
|
||||
// domain is "SSE-S3".
|
||||
func (s3) String() string { return "SSE-S3" }
|
||||
|
||||
// String returns the SSE domain as string. For SSE-C the
|
||||
// domain is "SSE-C".
|
||||
func (ssec) String() string { return "SSE-C" }
|
||||
|
||||
// EncryptSinglePart encrypts an io.Reader which must be the
|
||||
// the body of a single-part PUT request.
|
||||
func EncryptSinglePart(r io.Reader, key ObjectKey) io.Reader {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Minio Cloud Storage, (C) 2015, 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 crypto
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestS3String(t *testing.T) {
|
||||
const Domain = "SSE-S3"
|
||||
if domain := S3.String(); domain != Domain {
|
||||
t.Errorf("S3's string method returns wrong domain: got '%s' - want '%s'", domain, Domain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSECString(t *testing.T) {
|
||||
const Domain = "SSE-C"
|
||||
if domain := SSEC.String(); domain != Domain {
|
||||
t.Errorf("SSEC's string method returns wrong domain: got '%s' - want '%s'", domain, Domain)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// Minio Cloud Storage, (C) 2015, 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 crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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 (
|
||||
//ErrKMSAuthLogin is raised when there is a failure authenticating to KMS
|
||||
ErrKMSAuthLogin = errors.New("Vault service did not return auth info")
|
||||
)
|
||||
|
||||
type vaultService struct {
|
||||
config *VaultConfig
|
||||
client *vault.Client
|
||||
leaseDuration time.Duration
|
||||
}
|
||||
|
||||
// return transit secret engine's path for generate data key operation
|
||||
func (v *vaultService) genDataKeyEndpoint(key string) string {
|
||||
return "/transit/datakey/plaintext/" + key
|
||||
}
|
||||
|
||||
// return transit secret engine's path for decrypt operation
|
||||
func (v *vaultService) decryptEndpoint(key string) string {
|
||||
return "/transit/decrypt/" + key
|
||||
}
|
||||
|
||||
// VaultKey represents vault encryption key-id name & version
|
||||
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" {
|
||||
return fmt.Errorf("Unsupported hashicorp vault auth type - %s", vaultAuthTypeEnv)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
if e != nil {
|
||||
return token, duration, e
|
||||
}
|
||||
if resp.Auth == nil {
|
||||
return token, duration, ErrKMSAuthLogin
|
||||
}
|
||||
return resp.Auth.ClientToken, resp.Auth.LeaseDuration, nil
|
||||
}
|
||||
|
||||
// NewVaultConfig sets KMSConfig from environment
|
||||
// variables and performs validations.
|
||||
func NewVaultConfig() (KMSConfig, error) {
|
||||
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
|
||||
go func() {
|
||||
for {
|
||||
s, err := c.Auth().Token().RenewSelf(int(v.leaseDuration))
|
||||
if err != nil {
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
}
|
||||
nextRenew := s.Auth.LeaseDuration / 2
|
||||
time.Sleep(time.Duration(nextRenew) * time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Generates a random plain text key, sealed plain text key from
|
||||
// Vault. It returns the plaintext key and sealed plaintext key on success
|
||||
func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
|
||||
contextStream := new(bytes.Buffer)
|
||||
ctx.WriteTo(contextStream)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
|
||||
}
|
||||
s, err1 := v.client.Logical().Write(v.genDataKeyEndpoint(keyID), payload)
|
||||
|
||||
if err1 != nil {
|
||||
return key, sealedKey, err1
|
||||
}
|
||||
sealKey := s.Data["ciphertext"].(string)
|
||||
plainKey, err := base64.StdEncoding.DecodeString(s.Data["plaintext"].(string))
|
||||
if err != nil {
|
||||
return key, sealedKey, err1
|
||||
}
|
||||
copy(key[:], []byte(plainKey))
|
||||
return key, []byte(sealKey), nil
|
||||
}
|
||||
|
||||
// unsealKMSKey unseals the sealedKey using the Vault master key
|
||||
// referenced by the keyID. The plain text key is returned on success.
|
||||
func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
|
||||
contextStream := new(bytes.Buffer)
|
||||
ctx.WriteTo(contextStream)
|
||||
payload := map[string]interface{}{
|
||||
"ciphertext": string(sealedKey),
|
||||
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
|
||||
}
|
||||
s, err1 := v.client.Logical().Write(v.decryptEndpoint(keyID), payload)
|
||||
if err1 != nil {
|
||||
return key, err1
|
||||
}
|
||||
base64Key := s.Data["plaintext"].(string)
|
||||
plainKey, err1 := base64.StdEncoding.DecodeString(base64Key)
|
||||
if err1 != nil {
|
||||
return key, err1
|
||||
}
|
||||
copy(key[:], []byte(plainKey))
|
||||
|
||||
return key, nil
|
||||
}
|
||||
@@ -258,7 +258,7 @@ func (cfs *cacheFSObjects) IsOnline() bool {
|
||||
}
|
||||
|
||||
// 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 *hash.Reader, metadata map[string]string, opts ObjectOptions) error {
|
||||
if cfs.diskUsageHigh() {
|
||||
select {
|
||||
case cfs.purgeChan <- struct{}{}:
|
||||
@@ -275,7 +275,7 @@ func (cfs *cacheFSObjects) Put(ctx context.Context, bucket, object string, data
|
||||
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 IsErr(err, baseErrs...) {
|
||||
cfs.setOnline(false)
|
||||
@@ -284,8 +284,8 @@ func (cfs *cacheFSObjects) Put(ctx context.Context, bucket, object string, data
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return cfs.GetObject(ctx, bucket, object, startOffset, length, writer, etag)
|
||||
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, opts)
|
||||
}
|
||||
|
||||
// Deletes the cached object
|
||||
@@ -295,13 +295,13 @@ func (cfs *cacheFSObjects) Delete(ctx context.Context, bucket, object string) (e
|
||||
|
||||
// convenience function to check if object is cached on this cacheFSObjects
|
||||
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
|
||||
}
|
||||
|
||||
// Identical to fs PutObject operation except that it uses ETag in metadata
|
||||
// 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, data *hash.Reader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, retErr error) {
|
||||
fs := cfs.FSObjects
|
||||
// Lock the object.
|
||||
objectLock := fs.nsMutex.NewNSLock(bucket, object)
|
||||
@@ -438,7 +438,7 @@ func (cfs *cacheFSObjects) PutObject(ctx context.Context, bucket string, object
|
||||
// Implements S3 compatible initiate multipart API. Operation here is identical
|
||||
// to fs backend implementation - with the exception that cache FS uses the uploadID
|
||||
// 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() {
|
||||
select {
|
||||
case cfs.purgeChan <- struct{}{}:
|
||||
|
||||
+55
-55
@@ -57,16 +57,16 @@ type cacheObjects struct {
|
||||
// file path patterns to exclude from cache
|
||||
exclude []string
|
||||
// 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)
|
||||
GetObjectInfoFn func(ctx context.Context, bucket, object string) (objInfo ObjectInfo, err error)
|
||||
PutObjectFn func(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]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)
|
||||
GetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
PutObjectFn func(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err 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)
|
||||
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)
|
||||
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)
|
||||
PutObjectPartFn func(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader) (info PartInfo, 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, opts ObjectOptions) (info PartInfo, err 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)
|
||||
DeleteBucketFn func(ctx context.Context, bucket string) error
|
||||
@@ -88,14 +88,14 @@ type CacheObjectLayer interface {
|
||||
ListBuckets(ctx context.Context) (buckets []BucketInfo, err error)
|
||||
DeleteBucket(ctx context.Context, bucket string) error
|
||||
// Object operations.
|
||||
GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) (err error)
|
||||
GetObjectInfo(ctx context.Context, bucket, object string) (objInfo ObjectInfo, err error)
|
||||
PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]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)
|
||||
GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
DeleteObject(ctx context.Context, bucket, object string) error
|
||||
|
||||
// Multipart operations.
|
||||
NewMultipartUpload(ctx context.Context, bucket, object string, metadata map[string]string) (uploadID string, err error)
|
||||
PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader) (info PartInfo, 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, opts ObjectOptions) (info PartInfo, err error)
|
||||
AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string) error
|
||||
CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart) (objInfo ObjectInfo, err error)
|
||||
|
||||
@@ -177,20 +177,20 @@ func (c cacheObjects) getMetadata(objInfo ObjectInfo) map[string]string {
|
||||
|
||||
// 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.
|
||||
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
|
||||
GetObjectInfoFn := c.GetObjectInfoFn
|
||||
|
||||
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
|
||||
dcache, err := c.cache.getCachedFSLoc(ctx, bucket, object)
|
||||
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
|
||||
objInfo, err := GetObjectInfoFn(ctx, bucket, object)
|
||||
objInfo, err := GetObjectInfoFn(ctx, bucket, object, opts)
|
||||
backendDown := backendDownError(err)
|
||||
if err != nil && !backendDown {
|
||||
if _, ok := err.(ObjectNotFound); ok {
|
||||
@@ -201,27 +201,27 @@ func (c cacheObjects) GetObject(ctx context.Context, bucket, object string, star
|
||||
}
|
||||
|
||||
if !backendDown && filterFromCache(objInfo.UserDefined) {
|
||||
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)
|
||||
cachedObjInfo, err := dcache.GetObjectInfo(ctx, bucket, object, opts)
|
||||
if err == nil {
|
||||
if backendDown {
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
if startOffset != 0 || length != objInfo.Size {
|
||||
// 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) {
|
||||
// cache only objects < 1/100th of disk capacity
|
||||
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag)
|
||||
return GetObjectFn(ctx, bucket, object, startOffset, length, writer, etag, opts)
|
||||
}
|
||||
// Initialize pipe.
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
@@ -230,13 +230,13 @@ func (c cacheObjects) GetObject(ctx context.Context, bucket, object string, star
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
if err = GetObjectFn(ctx, bucket, object, 0, objInfo.Size, io.MultiWriter(writer, pipeWriter), etag); err != nil {
|
||||
if err = GetObjectFn(ctx, bucket, object, 0, objInfo.Size, io.MultiWriter(writer, pipeWriter), etag, opts); err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
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, hashReader, c.getMetadata(objInfo), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -245,17 +245,17 @@ func (c cacheObjects) GetObject(ctx context.Context, bucket, object string, star
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
dcache, err := c.cache.getCachedFSLoc(ctx, bucket, object)
|
||||
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 _, ok := err.(ObjectNotFound); ok {
|
||||
// Delete the cached entry if backend object was deleted.
|
||||
@@ -266,14 +266,14 @@ func (c cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string)
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
// 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 {
|
||||
return cachedObjInfo, nil
|
||||
}
|
||||
return ObjectInfo{}, BackendDown{}
|
||||
}
|
||||
// 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 {
|
||||
return objInfo, nil
|
||||
}
|
||||
@@ -560,24 +560,24 @@ func (c cacheObjects) isCacheExclude(bucket, object string) bool {
|
||||
}
|
||||
|
||||
// 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 *hash.Reader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
putObjectFn := c.PutObjectFn
|
||||
dcache, err := c.cache.getCacheFS(ctx, bucket, object)
|
||||
if err != nil {
|
||||
// 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()
|
||||
|
||||
// fetch from backend if there is no space on cache drive
|
||||
if !dcache.diskAvailable(size * cacheSizeMultiplier) {
|
||||
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
|
||||
// directive set to exclude
|
||||
if c.isCacheExclude(bucket, object) || filterFromCache(metadata) {
|
||||
dcache.Delete(ctx, bucket, object)
|
||||
return putObjectFn(ctx, bucket, object, r, metadata)
|
||||
return putObjectFn(ctx, bucket, object, r, metadata, opts)
|
||||
}
|
||||
objInfo = ObjectInfo{}
|
||||
// Initialize pipe to stream data to backend
|
||||
@@ -595,7 +595,7 @@ func (c cacheObjects) PutObject(ctx context.Context, bucket, object string, r *h
|
||||
oinfoCh := make(chan ObjectInfo)
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
oinfo, perr := putObjectFn(ctx, bucket, object, hashReader, metadata)
|
||||
oinfo, perr := putObjectFn(ctx, bucket, object, hashReader, metadata, opts)
|
||||
if perr != nil {
|
||||
pipeWriter.CloseWithError(perr)
|
||||
wPipe.CloseWithError(perr)
|
||||
@@ -608,7 +608,7 @@ func (c cacheObjects) PutObject(ctx context.Context, bucket, object string, r *h
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err = dcache.Put(ctx, bucket, object, cHashReader, metadata); err != nil {
|
||||
if err = dcache.Put(ctx, bucket, object, cHashReader, metadata, opts); err != nil {
|
||||
wPipe.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
@@ -627,39 +627,39 @@ func (c cacheObjects) PutObject(ctx context.Context, bucket, object string, r *h
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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, data *hash.Reader, opts ObjectOptions) (info PartInfo, err error) {
|
||||
putObjectPartFn := c.PutObjectPartFn
|
||||
dcache, err := c.cache.getCacheFS(ctx, bucket, object)
|
||||
if err != nil {
|
||||
// disk cache could not be located,execute backend call.
|
||||
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data)
|
||||
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data, opts)
|
||||
}
|
||||
|
||||
if c.isCacheExclude(bucket, object) {
|
||||
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data)
|
||||
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data, opts)
|
||||
}
|
||||
|
||||
// make sure cache has at least cacheSizeMultiplier * size available
|
||||
@@ -669,7 +669,7 @@ func (c cacheObjects) PutObjectPart(ctx context.Context, bucket, object, uploadI
|
||||
case dcache.purgeChan <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data)
|
||||
return putObjectPartFn(ctx, bucket, object, uploadID, partID, data, opts)
|
||||
}
|
||||
|
||||
info = PartInfo{}
|
||||
@@ -688,7 +688,7 @@ func (c cacheObjects) PutObjectPart(ctx context.Context, bucket, object, uploadI
|
||||
pinfoCh := make(chan PartInfo)
|
||||
errorCh := make(chan error)
|
||||
go func() {
|
||||
info, err = putObjectPartFn(ctx, bucket, object, uploadID, partID, hashReader)
|
||||
info, err = putObjectPartFn(ctx, bucket, object, uploadID, partID, hashReader, opts)
|
||||
if err != nil {
|
||||
close(pinfoCh)
|
||||
pipeWriter.CloseWithError(err)
|
||||
@@ -700,7 +700,7 @@ func (c cacheObjects) PutObjectPart(ctx context.Context, bucket, object, uploadI
|
||||
pinfoCh <- info
|
||||
}()
|
||||
go func() {
|
||||
if _, perr := dcache.PutObjectPart(ctx, bucket, object, uploadID, partID, cHashReader); perr != nil {
|
||||
if _, perr := dcache.PutObjectPart(ctx, bucket, object, uploadID, partID, cHashReader, opts); perr != nil {
|
||||
wPipe.CloseWithError(perr)
|
||||
return
|
||||
}
|
||||
@@ -876,14 +876,14 @@ func newServerCacheObjects(config CacheConfig) (CacheObjectLayer, error) {
|
||||
cache: dcache,
|
||||
exclude: config.Exclude,
|
||||
listPool: newTreeWalkPool(globalLookupTimeout),
|
||||
GetObjectFn: func(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) error {
|
||||
return newObjectLayerFn().GetObject(ctx, bucket, object, startOffset, length, writer, etag)
|
||||
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, opts)
|
||||
},
|
||||
GetObjectInfoFn: func(ctx context.Context, bucket, object string) (ObjectInfo, error) {
|
||||
return newObjectLayerFn().GetObjectInfo(ctx, bucket, object)
|
||||
GetObjectInfoFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
|
||||
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) {
|
||||
return newObjectLayerFn().PutObject(ctx, bucket, object, data, metadata)
|
||||
PutObjectFn: func(ctx context.Context, bucket, object string, data *hash.Reader, 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 {
|
||||
return newObjectLayerFn().DeleteObject(ctx, bucket, object)
|
||||
@@ -900,11 +900,11 @@ func newServerCacheObjects(config CacheConfig) (CacheObjectLayer, error) {
|
||||
GetBucketInfoFn: func(ctx context.Context, bucket string) (bucketInfo BucketInfo, err error) {
|
||||
return newObjectLayerFn().GetBucketInfo(ctx, bucket)
|
||||
},
|
||||
NewMultipartUploadFn: func(ctx context.Context, bucket, object string, metadata map[string]string) (uploadID string, err error) {
|
||||
return newObjectLayerFn().NewMultipartUpload(ctx, bucket, object, metadata)
|
||||
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, opts)
|
||||
},
|
||||
PutObjectPartFn: func(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader) (info PartInfo, err error) {
|
||||
return newObjectLayerFn().PutObjectPart(ctx, bucket, object, uploadID, partID, data)
|
||||
PutObjectPartFn: func(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader, opts ObjectOptions) (info PartInfo, err error) {
|
||||
return newObjectLayerFn().PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
|
||||
},
|
||||
AbortMultipartUploadFn: func(ctx context.Context, bucket, object, uploadID string) error {
|
||||
return newObjectLayerFn().AbortMultipartUpload(ctx, bucket, object, uploadID)
|
||||
|
||||
+12
-22
@@ -19,7 +19,6 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -28,21 +27,15 @@ import (
|
||||
)
|
||||
|
||||
// Initialize cache FS objects.
|
||||
func initCacheFSObjects(disk string, cacheMaxUse int, t *testing.T) (*cacheFSObjects, error) {
|
||||
newTestConfig(globalMinioDefaultRegion)
|
||||
var err error
|
||||
obj, err := newCacheFSObjects(disk, globalCacheExpiry, cacheMaxUse)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return obj, nil
|
||||
func initCacheFSObjects(disk string, cacheMaxUse int) (*cacheFSObjects, error) {
|
||||
return newCacheFSObjects(disk, globalCacheExpiry, cacheMaxUse)
|
||||
}
|
||||
|
||||
// inits diskCache struct for nDisks
|
||||
func initDiskCaches(drives []string, cacheMaxUse int, t *testing.T) (*diskCache, error) {
|
||||
var cfs []*cacheFSObjects
|
||||
for _, d := range drives {
|
||||
obj, err := initCacheFSObjects(d, cacheMaxUse, t)
|
||||
obj, err := initCacheFSObjects(d, cacheMaxUse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -131,11 +124,6 @@ func TestGetCacheFSMaxUse(t *testing.T) {
|
||||
|
||||
// test wildcard patterns for excluding entries from cache
|
||||
func TestCacheExclusion(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
fsDirs, err := getRandomDisks(1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -204,17 +192,18 @@ func TestDiskCache(t *testing.T) {
|
||||
objInfo.ContentType = contentType
|
||||
objInfo.ETag = etag
|
||||
objInfo.UserDefined = httpMeta
|
||||
opts := ObjectOptions{}
|
||||
|
||||
byteReader := bytes.NewReader([]byte(content))
|
||||
hashReader, err := hash.NewReader(byteReader, int64(size), "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = cache.Put(ctx, bucketName, objectName, hashReader, httpMeta)
|
||||
err = cache.Put(ctx, bucketName, objectName, hashReader, httpMeta, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cachedObjInfo, err := cache.GetObjectInfo(ctx, bucketName, objectName)
|
||||
cachedObjInfo, err := cache.GetObjectInfo(ctx, bucketName, objectName, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -231,7 +220,7 @@ func TestDiskCache(t *testing.T) {
|
||||
t.Fatal("Cached content-type does not match")
|
||||
}
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -278,6 +267,7 @@ func TestDiskCacheMaxUse(t *testing.T) {
|
||||
objInfo.ContentType = contentType
|
||||
objInfo.ETag = etag
|
||||
objInfo.UserDefined = httpMeta
|
||||
opts := ObjectOptions{}
|
||||
|
||||
byteReader := bytes.NewReader([]byte(content))
|
||||
hashReader, err := hash.NewReader(byteReader, int64(size), "", "")
|
||||
@@ -285,16 +275,16 @@ func TestDiskCacheMaxUse(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !cache.diskAvailable(int64(size)) {
|
||||
err = cache.Put(ctx, bucketName, objectName, hashReader, httpMeta)
|
||||
err = cache.Put(ctx, bucketName, objectName, hashReader, httpMeta, opts)
|
||||
if err != errDiskFull {
|
||||
t.Fatal("Cache max-use limit violated.")
|
||||
}
|
||||
} else {
|
||||
err = cache.Put(ctx, bucketName, objectName, hashReader, httpMeta)
|
||||
err = cache.Put(ctx, bucketName, objectName, hashReader, httpMeta, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cachedObjInfo, err := cache.GetObjectInfo(ctx, bucketName, objectName)
|
||||
cachedObjInfo, err := cache.GetObjectInfo(ctx, bucketName, objectName, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -311,7 +301,7 @@ func TestDiskCacheMaxUse(t *testing.T) {
|
||||
t.Fatal("Cached content-type does not match")
|
||||
}
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
@@ -60,19 +59,19 @@ func (api *DummyObjectLayer) ListObjectsV2(ctx context.Context, bucket, prefix,
|
||||
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) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (api *DummyObjectLayer) GetObjectInfo(ctx context.Context, bucket, object string) (objInfo ObjectInfo, err error) {
|
||||
func (api *DummyObjectLayer) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
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) PutObject(ctx context.Context, bucket, object string, data *hash.Reader, 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) (objInfo ObjectInfo, err error) {
|
||||
func (api *DummyObjectLayer) CopyObject(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -84,15 +83,15 @@ func (api *DummyObjectLayer) ListMultipartUploads(ctx context.Context, bucket, p
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 *hash.Reader, opts ObjectOptions) (info PartInfo, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -132,14 +131,6 @@ func (api *DummyObjectLayer) ListObjectsHeal(ctx context.Context, bucket, prefix
|
||||
return
|
||||
}
|
||||
|
||||
func (api *DummyObjectLayer) ListLocks(ctx context.Context, bucket, prefix string, duration time.Duration) (info []VolumeLockInfo, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (api *DummyObjectLayer) ClearLocks(context.Context, []VolumeLockInfo) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (api *DummyObjectLayer) SetBucketPolicy(context.Context, string, *policy.Policy) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
+191
-374
@@ -17,13 +17,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
@@ -31,6 +28,7 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/ioutil"
|
||||
sha256 "github.com/minio/sha256-simd"
|
||||
@@ -41,32 +39,12 @@ var (
|
||||
// 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")
|
||||
errInvalidSSEAlgorithm = errors.New("The SSE-C algorithm is not valid")
|
||||
errMissingSSEKey = errors.New("The SSE-C request is missing the customer key")
|
||||
errInvalidSSEKey = errors.New("The SSE-C key is invalid")
|
||||
errMissingSSEKeyMD5 = errors.New("The SSE-C request is missing the customer key MD5")
|
||||
errSSEKeyMD5Mismatch = errors.New("The key MD5 does not match the SSE-C key")
|
||||
errSSEKeyMismatch = errors.New("The SSE-C key is not correct") // 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")
|
||||
// Additional Minio errors for SSE-C requests.
|
||||
errObjectTampered = errors.New("The requested object was modified and may be compromised")
|
||||
)
|
||||
|
||||
const (
|
||||
// SSECustomerAlgorithm is the AWS SSE-C algorithm HTTP header key.
|
||||
SSECustomerAlgorithm = "X-Amz-Server-Side-Encryption-Customer-Algorithm"
|
||||
// SSECustomerKey is the AWS SSE-C encryption key HTTP header key.
|
||||
SSECustomerKey = "X-Amz-Server-Side-Encryption-Customer-Key"
|
||||
// SSECustomerKeyMD5 is the AWS SSE-C encryption key MD5 HTTP header key.
|
||||
SSECustomerKeyMD5 = "X-Amz-Server-Side-Encryption-Customer-Key-MD5"
|
||||
|
||||
// SSECopyCustomerAlgorithm is the AWS SSE-C algorithm HTTP header key for CopyObject API.
|
||||
SSECopyCustomerAlgorithm = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm"
|
||||
// SSECopyCustomerKey is the AWS SSE-C encryption key HTTP header key for CopyObject API.
|
||||
SSECopyCustomerKey = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key"
|
||||
// SSECopyCustomerKeyMD5 is the AWS SSE-C encryption key MD5 HTTP header key for CopyObject API.
|
||||
SSECopyCustomerKeyMD5 = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-MD5"
|
||||
// error returned when invalid encryption parameters are specified
|
||||
errInvalidEncryptionParameters = errors.New("The encryption parameters are not applicable to this object")
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -77,9 +55,6 @@ const (
|
||||
// SSEIVSize is the size of the IV data
|
||||
SSEIVSize = 32 // 32 bytes
|
||||
|
||||
// SSECustomerAlgorithmAES256 the only valid S3 SSE-C encryption algorithm identifier.
|
||||
SSECustomerAlgorithmAES256 = "AES256"
|
||||
|
||||
// SSE dare package block size.
|
||||
sseDAREPackageBlockSize = 64 * 1024 // 64KiB bytes
|
||||
|
||||
@@ -88,63 +63,6 @@ const (
|
||||
|
||||
)
|
||||
|
||||
// SSE-C key derivation, key verification and key update:
|
||||
// H: Hash function [32 = |H(m)|]
|
||||
// AE: authenticated encryption scheme, AD: authenticated decryption scheme [m = AD(k, AE(k, m))]
|
||||
//
|
||||
// Key derivation:
|
||||
// Input:
|
||||
// key := 32 bytes # client provided key
|
||||
// Re, Rm := 32 bytes, 32 bytes # uniformly random
|
||||
//
|
||||
// Seal:
|
||||
// k := H(key || Re) # object encryption key
|
||||
// r := H(Rm) # save as object metadata [ServerSideEncryptionIV]
|
||||
// KeK := H(key || r) # key encryption key
|
||||
// K := AE(KeK, k) # save as object metadata [ServerSideEncryptionSealedKey]
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Key verification:
|
||||
// Input:
|
||||
// key := 32 bytes # client provided key
|
||||
// r := 32 bytes # object metadata [ServerSideEncryptionIV]
|
||||
// K := 32 bytes # object metadata [ServerSideEncryptionSealedKey]
|
||||
//
|
||||
// Open:
|
||||
// KeK := H(key || r) # key encryption key
|
||||
// k := AD(Kek, K) # object encryption key
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Key update:
|
||||
// Input:
|
||||
// key := 32 bytes # old client provided key
|
||||
// key' := 32 bytes # new client provided key
|
||||
// Rm := 32 bytes # uniformly random
|
||||
// r := 32 bytes # object metadata [ServerSideEncryptionIV]
|
||||
// K := 32 bytes # object metadata [ServerSideEncryptionSealedKey]
|
||||
//
|
||||
// Update:
|
||||
// 1. open:
|
||||
// KeK := H(key || r) # key encryption key
|
||||
// k := AD(Kek, K) # object encryption key
|
||||
// 2. seal:
|
||||
// r' := H(Rm) # save as object metadata [ServerSideEncryptionIV]
|
||||
// KeK' := H(key' || r') # new key encryption key
|
||||
// K' := AE(KeK', k) # save as object metadata [ServerSideEncryptionSealedKey]
|
||||
|
||||
const (
|
||||
// ServerSideEncryptionIV is a 32 byte randomly generated IV used to derive an
|
||||
// unique key encryption key from the client provided key. The combination of this value
|
||||
// and the client-provided key MUST be unique.
|
||||
ServerSideEncryptionIV = ReservedMetadataPrefix + "Server-Side-Encryption-Iv"
|
||||
|
||||
// ServerSideEncryptionSealAlgorithm identifies a combination of a cryptographic hash function and
|
||||
// an authenticated en/decryption scheme to seal the object encryption key.
|
||||
ServerSideEncryptionSealAlgorithm = ReservedMetadataPrefix + "Server-Side-Encryption-Seal-Algorithm"
|
||||
|
||||
// ServerSideEncryptionSealedKey is the sealed object encryption key. The sealed key can be decrypted
|
||||
// by the key encryption key derived from the client provided key and the server-side-encryption IV.
|
||||
ServerSideEncryptionSealedKey = ReservedMetadataPrefix + "Server-Side-Encryption-Sealed-Key"
|
||||
)
|
||||
|
||||
const (
|
||||
// SSESealAlgorithmDareSha256 specifies DARE as authenticated en/decryption scheme and SHA256 as cryptographic
|
||||
// hash function. The key derivation of DARE-SHA256 is not optimal and does not include the object path.
|
||||
@@ -154,27 +72,17 @@ const (
|
||||
// SSESealAlgorithmDareV2HmacSha256 specifies DAREv2 as authenticated en/decryption scheme and SHA256 as cryptographic
|
||||
// hash function for the HMAC PRF.
|
||||
SSESealAlgorithmDareV2HmacSha256 = "DAREv2-HMAC-SHA256"
|
||||
|
||||
// SSEDomain specifies the domain for the derived key - in this case the
|
||||
// key should be used for SSE-C.
|
||||
SSEDomain = "SSE-C"
|
||||
)
|
||||
|
||||
// hasSSECustomerHeader returns true if the given HTTP header
|
||||
// contains server-side-encryption with customer provided key fields.
|
||||
func hasSSECustomerHeader(header http.Header) bool {
|
||||
return header.Get(SSECustomerAlgorithm) != "" || header.Get(SSECustomerKey) != "" || header.Get(SSECustomerKeyMD5) != ""
|
||||
}
|
||||
|
||||
// hasSSECopyCustomerHeader returns true if the given HTTP header
|
||||
// contains copy source server-side-encryption with customer provided key fields.
|
||||
func hasSSECopyCustomerHeader(header http.Header) bool {
|
||||
return header.Get(SSECopyCustomerAlgorithm) != "" || header.Get(SSECopyCustomerKey) != "" || header.Get(SSECopyCustomerKeyMD5) != ""
|
||||
// hasServerSideEncryptionHeader returns true if the given HTTP header
|
||||
// contains server-side-encryption.
|
||||
func hasServerSideEncryptionHeader(header http.Header) bool {
|
||||
return crypto.S3.IsRequested(header) || crypto.SSEC.IsRequested(header)
|
||||
}
|
||||
|
||||
// ParseSSECopyCustomerRequest parses the SSE-C header fields of the provided request.
|
||||
// It returns the client provided key on success.
|
||||
func ParseSSECopyCustomerRequest(r *http.Request) (key []byte, err error) {
|
||||
func ParseSSECopyCustomerRequest(r *http.Request, metadata map[string]string) (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.
|
||||
@@ -182,36 +90,11 @@ func ParseSSECopyCustomerRequest(r *http.Request) (key []byte, err error) {
|
||||
// will always fail -> r.TLS is always nil even for TLS requests.
|
||||
return nil, errInsecureSSERequest
|
||||
}
|
||||
header := r.Header
|
||||
if algorithm := header.Get(SSECopyCustomerAlgorithm); algorithm != SSECustomerAlgorithmAES256 {
|
||||
return nil, errInvalidSSEAlgorithm
|
||||
if crypto.S3.IsEncrypted(metadata) && crypto.SSECopy.IsRequested(r.Header) {
|
||||
return nil, crypto.ErrIncompatibleEncryptionMethod
|
||||
}
|
||||
if header.Get(SSECopyCustomerKey) == "" {
|
||||
return nil, errMissingSSEKey
|
||||
}
|
||||
if header.Get(SSECopyCustomerKeyMD5) == "" {
|
||||
return nil, errMissingSSEKeyMD5
|
||||
}
|
||||
|
||||
key, err = base64.StdEncoding.DecodeString(header.Get(SSECopyCustomerKey))
|
||||
if err != nil {
|
||||
return nil, errInvalidSSEKey
|
||||
}
|
||||
|
||||
if len(key) != SSECustomerKeySize {
|
||||
return nil, errInvalidSSEKey
|
||||
}
|
||||
// Make sure we purged the keys from http headers by now.
|
||||
header.Del(SSECopyCustomerKey)
|
||||
|
||||
keyMD5, err := base64.StdEncoding.DecodeString(header.Get(SSECopyCustomerKeyMD5))
|
||||
if err != nil {
|
||||
return nil, errSSEKeyMD5Mismatch
|
||||
}
|
||||
if md5Sum := md5.Sum(key); !bytes.Equal(md5Sum[:], keyMD5) {
|
||||
return nil, errSSEKeyMD5Mismatch
|
||||
}
|
||||
return key, nil
|
||||
k, err := crypto.SSECopy.ParseHTTP(r.Header)
|
||||
return k[:], err
|
||||
}
|
||||
|
||||
// ParseSSECustomerRequest parses the SSE-C header fields of the provided request.
|
||||
@@ -230,228 +113,177 @@ func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
|
||||
// will always fail -> r.TLS is always nil even for TLS requests.
|
||||
return nil, errInsecureSSERequest
|
||||
}
|
||||
if algorithm := header.Get(SSECustomerAlgorithm); algorithm != SSECustomerAlgorithmAES256 {
|
||||
return nil, errInvalidSSEAlgorithm
|
||||
}
|
||||
if header.Get(SSECustomerKey) == "" {
|
||||
return nil, errMissingSSEKey
|
||||
}
|
||||
if header.Get(SSECustomerKeyMD5) == "" {
|
||||
return nil, errMissingSSEKeyMD5
|
||||
if crypto.S3.IsRequested(header) && crypto.SSEC.IsRequested(header) {
|
||||
return key, crypto.ErrIncompatibleEncryptionMethod
|
||||
}
|
||||
|
||||
key, err = base64.StdEncoding.DecodeString(header.Get(SSECustomerKey))
|
||||
if err != nil {
|
||||
return nil, errInvalidSSEKey
|
||||
}
|
||||
|
||||
if len(key) != SSECustomerKeySize {
|
||||
return nil, errInvalidSSEKey
|
||||
}
|
||||
// Make sure we purged the keys from http headers by now.
|
||||
header.Del(SSECustomerKey)
|
||||
|
||||
keyMD5, err := base64.StdEncoding.DecodeString(header.Get(SSECustomerKeyMD5))
|
||||
if err != nil {
|
||||
return nil, errSSEKeyMD5Mismatch
|
||||
}
|
||||
if md5Sum := md5.Sum(key); !bytes.Equal(md5Sum[:], keyMD5) {
|
||||
return nil, errSSEKeyMD5Mismatch
|
||||
}
|
||||
return key, nil
|
||||
k, err := crypto.SSEC.ParseHTTP(header)
|
||||
return k[:], err
|
||||
}
|
||||
|
||||
// This function rotates old to new key.
|
||||
func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map[string]string) error {
|
||||
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident
|
||||
delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
|
||||
|
||||
algorithm := metadata[ServerSideEncryptionSealAlgorithm]
|
||||
if algorithm != SSESealAlgorithmDareSha256 && algorithm != SSESealAlgorithmDareV2HmacSha256 {
|
||||
return errObjectTampered
|
||||
}
|
||||
iv, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionIV])
|
||||
if err != nil || len(iv) != SSEIVSize {
|
||||
return errObjectTampered
|
||||
}
|
||||
sealedKey, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionSealedKey])
|
||||
if err != nil || len(sealedKey) != 64 {
|
||||
return errObjectTampered
|
||||
}
|
||||
|
||||
var (
|
||||
minDAREVersion byte
|
||||
keyEncryptionKey [32]byte
|
||||
)
|
||||
switch algorithm {
|
||||
switch {
|
||||
default:
|
||||
return errObjectTampered
|
||||
case SSESealAlgorithmDareSha256: // legacy key-encryption-key derivation
|
||||
minDAREVersion = sio.Version10
|
||||
sha := sha256.New()
|
||||
sha.Write(oldKey)
|
||||
sha.Write(iv)
|
||||
sha.Sum(keyEncryptionKey[:0])
|
||||
case SSESealAlgorithmDareV2HmacSha256: // key-encryption-key derivation - See: crypto/doc.go
|
||||
minDAREVersion = sio.Version20
|
||||
mac := hmac.New(sha256.New, oldKey)
|
||||
mac.Write(iv)
|
||||
mac.Write([]byte(SSEDomain))
|
||||
mac.Write([]byte(SSESealAlgorithmDareV2HmacSha256))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
mac.Sum(keyEncryptionKey[:0])
|
||||
}
|
||||
|
||||
objectEncryptionKey := bytes.NewBuffer(nil) // decrypt object encryption key
|
||||
n, err := sio.Decrypt(objectEncryptionKey, bytes.NewReader(sealedKey), sio.Config{
|
||||
MinVersion: minDAREVersion,
|
||||
Key: keyEncryptionKey[:],
|
||||
})
|
||||
if n != 32 || err != nil { // Either the provided key does not match or the object was tampered.
|
||||
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {
|
||||
return errInvalidSSEParameters // AWS returns special error for equal but invalid keys.
|
||||
case crypto.SSEC.IsEncrypted(metadata):
|
||||
sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return errSSEKeyMismatch // To provide strict AWS S3 compatibility we return: access denied.
|
||||
}
|
||||
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 && algorithm != SSESealAlgorithmDareSha256 {
|
||||
return nil // we don't need to rotate keys if newKey == oldKey but we may have to upgrade KDF algorithm
|
||||
}
|
||||
|
||||
mac := hmac.New(sha256.New, newKey) // key-encryption-key derivation - See: crypto/doc.go
|
||||
mac.Write(iv)
|
||||
mac.Write([]byte(SSEDomain))
|
||||
mac.Write([]byte(SSESealAlgorithmDareV2HmacSha256))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
mac.Sum(keyEncryptionKey[:0])
|
||||
var objectKey crypto.ObjectKey
|
||||
var extKey [32]byte
|
||||
copy(extKey[:], oldKey)
|
||||
if err = objectKey.Unseal(extKey, sealedKey, crypto.SSEC.String(), bucket, object); err != nil {
|
||||
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {
|
||||
return errInvalidSSEParameters // AWS returns special error for equal but invalid keys.
|
||||
}
|
||||
return crypto.ErrInvalidCustomerKey // To provide strict AWS S3 compatibility we return: access denied.
|
||||
|
||||
sealedKeyW := bytes.NewBuffer(nil) // sealedKey := 16 byte header + 32 byte payload + 16 byte tag
|
||||
n, err = sio.Encrypt(sealedKeyW, bytes.NewReader(objectEncryptionKey.Bytes()), sio.Config{
|
||||
Key: keyEncryptionKey[:],
|
||||
})
|
||||
if n != 64 || err != nil {
|
||||
return errors.New("failed to seal object encryption key") // if this happens there's a bug in the code (may panic ?)
|
||||
}
|
||||
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 && sealedKey.Algorithm == crypto.SealAlgorithm {
|
||||
return nil // don't rotate on equal keys if seal algorithm is latest
|
||||
}
|
||||
copy(extKey[:], newKey)
|
||||
sealedKey = objectKey.Seal(extKey, sealedKey.IV, crypto.SSEC.String(), bucket, object)
|
||||
crypto.SSEC.CreateMetadata(metadata, sealedKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
metadata[ServerSideEncryptionIV] = base64.StdEncoding.EncodeToString(iv[:])
|
||||
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareV2HmacSha256
|
||||
metadata[ServerSideEncryptionSealedKey] = base64.StdEncoding.EncodeToString(sealedKeyW.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
|
||||
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident
|
||||
// See crypto/doc.go for detailed description
|
||||
nonce := make([]byte, 32+SSEIVSize) // generate random values for key derivation
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
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
|
||||
if sseS3 {
|
||||
if globalKMS == nil {
|
||||
return nil, errKMSNotConfigured
|
||||
}
|
||||
key, encKey, err := globalKMS.GenerateKey(globalKMSKeyID, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objectKey := crypto.GenerateKey(key, rand.Reader)
|
||||
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
|
||||
crypto.S3.CreateMetadata(metadata, globalKMSKeyID, encKey, sealedKey)
|
||||
return objectKey[:], nil
|
||||
}
|
||||
sha := sha256.New() // derive object encryption key
|
||||
sha.Write(key)
|
||||
sha.Write(nonce[:32])
|
||||
objectEncryptionKey := sha.Sum(nil)
|
||||
var extKey [32]byte
|
||||
copy(extKey[:], key)
|
||||
objectKey := crypto.GenerateKey(extKey, rand.Reader)
|
||||
sealedKey = objectKey.Seal(extKey, crypto.GenerateIV(rand.Reader), crypto.SSEC.String(), bucket, object)
|
||||
crypto.SSEC.CreateMetadata(metadata, sealedKey)
|
||||
return objectKey[:], nil
|
||||
|
||||
iv := sha256.Sum256(nonce[32:]) // key-encryption-key derivation - See: crypto/doc.go
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write(iv[:])
|
||||
mac.Write([]byte(SSEDomain))
|
||||
mac.Write([]byte(SSESealAlgorithmDareV2HmacSha256))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
keyEncryptionKey := mac.Sum(nil)
|
||||
|
||||
sealedKey := bytes.NewBuffer(nil) // sealedKey := 16 byte header + 32 byte payload + 16 byte tag
|
||||
n, err := sio.Encrypt(sealedKey, bytes.NewReader(objectEncryptionKey), sio.Config{
|
||||
Key: keyEncryptionKey,
|
||||
})
|
||||
if n != 64 || err != nil {
|
||||
return nil, errors.New("failed to seal object encryption key") // if this happens there's a bug in the code (may panic ?)
|
||||
}
|
||||
|
||||
metadata[ServerSideEncryptionIV] = base64.StdEncoding.EncodeToString(iv[:])
|
||||
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareV2HmacSha256
|
||||
metadata[ServerSideEncryptionSealedKey] = base64.StdEncoding.EncodeToString(sealedKey.Bytes())
|
||||
|
||||
return objectEncryptionKey, nil
|
||||
}
|
||||
|
||||
func newEncryptReader(content io.Reader, key []byte, bucket, object string, metadata map[string]string) (io.Reader, error) {
|
||||
objectEncryptionKey, err := newEncryptMetadata(key, bucket, object, metadata)
|
||||
func newEncryptReader(content io.Reader, key []byte, bucket, object string, metadata map[string]string, sseS3 bool) (io.Reader, error) {
|
||||
objectEncryptionKey, err := newEncryptMetadata(key, bucket, object, metadata, sseS3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey})
|
||||
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey[:], MinVersion: sio.Version20})
|
||||
if err != nil {
|
||||
return nil, errInvalidSSEKey
|
||||
return nil, crypto.ErrInvalidCustomerKey
|
||||
}
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// set new encryption metadata from http request headers for SSE-C and generated key from KMS in the case of
|
||||
// SSE-S3
|
||||
func setEncryptionMetadata(r *http.Request, bucket, object string, metadata map[string]string) (err error) {
|
||||
var (
|
||||
key []byte
|
||||
)
|
||||
if crypto.SSEC.IsRequested(r.Header) {
|
||||
key, err = ParseSSECustomerRequest(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err = newEncryptMetadata(key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
|
||||
return
|
||||
}
|
||||
|
||||
// EncryptRequest takes the client provided content and encrypts the data
|
||||
// with the client provided key. It also marks the object as client-side-encrypted
|
||||
// and sets the correct headers.
|
||||
func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, metadata map[string]string) (io.Reader, error) {
|
||||
key, err := ParseSSECustomerRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
var (
|
||||
key []byte
|
||||
err error
|
||||
)
|
||||
if crypto.S3.IsRequested(r.Header) && crypto.SSEC.IsRequested(r.Header) {
|
||||
return nil, crypto.ErrIncompatibleEncryptionMethod
|
||||
}
|
||||
return newEncryptReader(content, key, bucket, object, metadata)
|
||||
if crypto.SSEC.IsRequested(r.Header) {
|
||||
key, err = ParseSSECustomerRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return newEncryptReader(content, key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
|
||||
}
|
||||
|
||||
// DecryptCopyRequest decrypts the object with the client provided key. It also removes
|
||||
// the client-side-encryption metadata from the object and sets the correct headers.
|
||||
func DecryptCopyRequest(client io.Writer, r *http.Request, bucket, object string, metadata map[string]string) (io.WriteCloser, error) {
|
||||
key, err := ParseSSECopyCustomerRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var (
|
||||
key []byte
|
||||
err error
|
||||
)
|
||||
if crypto.SSECopy.IsRequested(r.Header) {
|
||||
key, err = ParseSSECopyCustomerRequest(r, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
delete(metadata, SSECopyCustomerKey) // make sure we do not save the key by accident
|
||||
delete(metadata, crypto.SSECopyKey) // make sure we do not save the key by accident
|
||||
return newDecryptWriter(client, key, bucket, object, 0, metadata)
|
||||
}
|
||||
|
||||
func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
|
||||
iv, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionIV])
|
||||
if err != nil || len(iv) != SSEIVSize {
|
||||
return nil, errObjectTampered
|
||||
}
|
||||
sealedKey, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionSealedKey])
|
||||
if err != nil || len(sealedKey) != 64 {
|
||||
return nil, errObjectTampered
|
||||
}
|
||||
|
||||
var (
|
||||
minDAREVersion byte
|
||||
keyEncryptionKey [32]byte
|
||||
)
|
||||
switch algorithm := metadata[ServerSideEncryptionSealAlgorithm]; algorithm {
|
||||
switch {
|
||||
default:
|
||||
return nil, errObjectTampered
|
||||
case SSESealAlgorithmDareSha256: // legacy key-encryption-key derivation
|
||||
minDAREVersion = sio.Version10
|
||||
sha := sha256.New()
|
||||
sha.Write(key)
|
||||
sha.Write(iv)
|
||||
sha.Sum(keyEncryptionKey[:0])
|
||||
case SSESealAlgorithmDareV2HmacSha256: // key-encryption-key derivation - See: crypto/doc.go
|
||||
minDAREVersion = sio.Version20
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write(iv)
|
||||
mac.Write([]byte(SSEDomain))
|
||||
mac.Write([]byte(SSESealAlgorithmDareV2HmacSha256))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
mac.Sum(keyEncryptionKey[:0])
|
||||
}
|
||||
case crypto.S3.IsEncrypted(metadata):
|
||||
if globalKMS == nil {
|
||||
return nil, errKMSNotConfigured
|
||||
}
|
||||
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
|
||||
|
||||
objectEncryptionKey := bytes.NewBuffer(nil) // decrypt object encryption key
|
||||
n, err := sio.Decrypt(objectEncryptionKey, bytes.NewReader(sealedKey), sio.Config{
|
||||
MinVersion: minDAREVersion,
|
||||
Key: keyEncryptionKey[:],
|
||||
})
|
||||
if n != 32 || err != nil {
|
||||
// Either the provided key does not match or the object was tampered.
|
||||
// To provide strict AWS S3 compatibility we return: access denied.
|
||||
return nil, errSSEKeyMismatch
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extKey, err := globalKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var objectKey crypto.ObjectKey
|
||||
if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), bucket, object); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return objectKey[:], nil
|
||||
case crypto.SSEC.IsEncrypted(metadata):
|
||||
var extKey [32]byte
|
||||
copy(extKey[:], key)
|
||||
sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var objectKey crypto.ObjectKey
|
||||
if err = objectKey.Unseal(extKey, sealedKey, crypto.SSEC.String(), bucket, object); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return objectKey[:], nil
|
||||
}
|
||||
return objectEncryptionKey.Bytes(), nil
|
||||
}
|
||||
|
||||
func newDecryptWriter(client io.Writer, key []byte, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||
@@ -468,29 +300,35 @@ func newDecryptWriterWithObjectKey(client io.Writer, objectEncryptionKey []byte,
|
||||
SequenceNumber: seqNumber,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errInvalidSSEKey
|
||||
return nil, crypto.ErrInvalidCustomerKey
|
||||
}
|
||||
|
||||
delete(metadata, ServerSideEncryptionIV)
|
||||
delete(metadata, ServerSideEncryptionSealAlgorithm)
|
||||
delete(metadata, ServerSideEncryptionSealedKey)
|
||||
delete(metadata, ReservedMetadataPrefix+"Encrypted-Multipart")
|
||||
delete(metadata, crypto.SSEIV)
|
||||
delete(metadata, crypto.SSESealAlgorithm)
|
||||
delete(metadata, crypto.SSECSealedKey)
|
||||
delete(metadata, crypto.SSEMultipart)
|
||||
delete(metadata, crypto.S3SealedKey)
|
||||
delete(metadata, crypto.S3KMSSealedKey)
|
||||
delete(metadata, crypto.S3KMSKeyID)
|
||||
return writer, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func DecryptRequestWithSequenceNumber(client io.Writer, r *http.Request, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||
if crypto.S3.IsEncrypted(metadata) {
|
||||
return newDecryptWriter(client, nil, bucket, object, seqNumber, metadata)
|
||||
}
|
||||
|
||||
key, err := ParseSSECustomerRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident
|
||||
delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
|
||||
return newDecryptWriter(client, key, bucket, object, seqNumber, metadata)
|
||||
}
|
||||
|
||||
// DecryptRequest decrypts the object with the client provided key. It also removes
|
||||
// the client-side-encryption metadata from the object and sets the correct headers.
|
||||
// DecryptRequest decrypts the object with client provided key for SSE-C and SSE-S3. It also removes
|
||||
// the encryption metadata from the object and sets the correct headers.
|
||||
func DecryptRequest(client io.Writer, r *http.Request, bucket, object string, metadata map[string]string) (io.WriteCloser, error) {
|
||||
return DecryptRequestWithSequenceNumber(client, r, bucket, object, 0, metadata)
|
||||
}
|
||||
@@ -527,11 +365,15 @@ func (w *DecryptBlocksWriter) buildDecrypter(partID int) error {
|
||||
var key []byte
|
||||
var err error
|
||||
if w.copySource {
|
||||
w.req.Header.Set(SSECopyCustomerKey, w.customerKeyHeader)
|
||||
key, err = ParseSSECopyCustomerRequest(w.req)
|
||||
if crypto.SSEC.IsEncrypted(w.metadata) {
|
||||
w.req.Header.Set(crypto.SSECopyKey, w.customerKeyHeader)
|
||||
key, err = ParseSSECopyCustomerRequest(w.req, w.metadata)
|
||||
}
|
||||
} else {
|
||||
w.req.Header.Set(SSECustomerKey, w.customerKeyHeader)
|
||||
key, err = ParseSSECustomerRequest(w.req)
|
||||
if crypto.SSEC.IsEncrypted(w.metadata) {
|
||||
w.req.Header.Set(crypto.SSECKey, w.customerKeyHeader)
|
||||
key, err = ParseSSECustomerRequest(w.req)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -551,9 +393,9 @@ func (w *DecryptBlocksWriter) buildDecrypter(partID int) error {
|
||||
|
||||
// make sure we do not save the key by accident
|
||||
if w.copySource {
|
||||
delete(m, SSECopyCustomerKey)
|
||||
delete(m, crypto.SSECopyKey)
|
||||
} else {
|
||||
delete(m, SSECustomerKey)
|
||||
delete(m, crypto.SSECKey)
|
||||
}
|
||||
|
||||
// make sure to provide a NopCloser such that a Close
|
||||
@@ -643,7 +485,7 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object stri
|
||||
var seqNumber uint32
|
||||
var encStartOffset, encLength int64
|
||||
|
||||
if len(objInfo.Parts) == 0 || !objInfo.IsEncryptedMultipart() {
|
||||
if len(objInfo.Parts) == 0 || !crypto.IsMultiPart(objInfo.UserDefined) {
|
||||
seqNumber, encStartOffset, encLength = getEncryptedSinglePartOffsetLength(startOffset, length, objInfo)
|
||||
|
||||
var writer io.WriteCloser
|
||||
@@ -694,7 +536,7 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object stri
|
||||
req: r,
|
||||
bucket: bucket,
|
||||
object: object,
|
||||
customerKeyHeader: r.Header.Get(SSECustomerKey),
|
||||
customerKeyHeader: r.Header.Get(crypto.SSECKey),
|
||||
copySource: copySource,
|
||||
}
|
||||
|
||||
@@ -705,13 +547,18 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object stri
|
||||
}
|
||||
|
||||
// Purge all the encryption headers.
|
||||
delete(objInfo.UserDefined, ServerSideEncryptionIV)
|
||||
delete(objInfo.UserDefined, ServerSideEncryptionSealAlgorithm)
|
||||
delete(objInfo.UserDefined, ServerSideEncryptionSealedKey)
|
||||
delete(objInfo.UserDefined, ReservedMetadataPrefix+"Encrypted-Multipart")
|
||||
delete(objInfo.UserDefined, crypto.SSEIV)
|
||||
delete(objInfo.UserDefined, crypto.SSESealAlgorithm)
|
||||
delete(objInfo.UserDefined, crypto.SSECSealedKey)
|
||||
delete(objInfo.UserDefined, crypto.SSEMultipart)
|
||||
|
||||
if crypto.S3.IsEncrypted(objInfo.UserDefined) {
|
||||
delete(objInfo.UserDefined, crypto.S3SealedKey)
|
||||
delete(objInfo.UserDefined, crypto.S3KMSKeyID)
|
||||
delete(objInfo.UserDefined, crypto.S3KMSSealedKey)
|
||||
}
|
||||
if w.copySource {
|
||||
w.customerKeyHeader = r.Header.Get(SSECopyCustomerKey)
|
||||
w.customerKeyHeader = r.Header.Get(crypto.SSECopyKey)
|
||||
}
|
||||
|
||||
if err := w.buildDecrypter(w.parts[w.partIndex].Number); err != nil {
|
||||
@@ -793,48 +640,14 @@ func getEncryptedSinglePartOffsetLength(offset, length int64, objInfo ObjectInfo
|
||||
return seqNumber, encOffset, encLength
|
||||
}
|
||||
|
||||
// IsEncryptedMultipart - is the encrypted content multiparted?
|
||||
func (o *ObjectInfo) IsEncryptedMultipart() bool {
|
||||
_, ok := o.UserDefined[ReservedMetadataPrefix+"Encrypted-Multipart"]
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsEncrypted returns true if the object is marked as encrypted.
|
||||
func (o *ObjectInfo) IsEncrypted() bool {
|
||||
if _, ok := o.UserDefined[ServerSideEncryptionIV]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := o.UserDefined[ServerSideEncryptionSealAlgorithm]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := o.UserDefined[ServerSideEncryptionSealedKey]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsEncrypted returns true if the object is marked as encrypted.
|
||||
func (li *ListPartsInfo) IsEncrypted() bool {
|
||||
if _, ok := li.UserDefined[ServerSideEncryptionIV]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := li.UserDefined[ServerSideEncryptionSealAlgorithm]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := li.UserDefined[ServerSideEncryptionSealedKey]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DecryptedSize returns the size of the object after decryption in bytes.
|
||||
// It returns an error if the object is not encrypted or marked as encrypted
|
||||
// but has an invalid size.
|
||||
func (o *ObjectInfo) DecryptedSize() (int64, error) {
|
||||
if !o.IsEncrypted() {
|
||||
if !crypto.IsEncrypted(o.UserDefined) {
|
||||
return 0, errors.New("Cannot compute decrypted size of an unencrypted object")
|
||||
}
|
||||
if len(o.Parts) == 0 || !o.IsEncryptedMultipart() {
|
||||
if len(o.Parts) == 0 || !crypto.IsMultiPart(o.UserDefined) {
|
||||
size, err := sio.DecryptedSize(uint64(o.Size))
|
||||
if err != nil {
|
||||
err = errObjectTampered // assign correct error type
|
||||
@@ -880,10 +693,11 @@ func DecryptCopyObjectInfo(info *ObjectInfo, headers http.Header) (apiErr APIErr
|
||||
if info.IsDir {
|
||||
return ErrNone, false
|
||||
}
|
||||
if apiErr, encrypted = ErrNone, info.IsEncrypted(); !encrypted && hasSSECopyCustomerHeader(headers) {
|
||||
if apiErr, encrypted = ErrNone, crypto.IsEncrypted(info.UserDefined); !encrypted && crypto.SSECopy.IsRequested(headers) {
|
||||
apiErr = ErrInvalidEncryptionParameters
|
||||
} else if encrypted {
|
||||
if !hasSSECopyCustomerHeader(headers) {
|
||||
if (!crypto.SSECopy.IsRequested(headers) && crypto.SSEC.IsEncrypted(info.UserDefined)) ||
|
||||
(crypto.SSECopy.IsRequested(headers) && crypto.S3.IsEncrypted(info.UserDefined)) {
|
||||
apiErr = ErrSSEEncryptedObject
|
||||
return
|
||||
}
|
||||
@@ -902,22 +716,25 @@ func DecryptCopyObjectInfo(info *ObjectInfo, headers http.Header) (apiErr APIErr
|
||||
// decryption succeeded.
|
||||
//
|
||||
// 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.
|
||||
if info.IsDir {
|
||||
return ErrNone, false
|
||||
return false, nil
|
||||
}
|
||||
if apiErr, encrypted = ErrNone, info.IsEncrypted(); !encrypted && hasSSECustomerHeader(headers) {
|
||||
apiErr = ErrInvalidEncryptionParameters
|
||||
// disallow X-Amz-Server-Side-Encryption header on HEAD and GET
|
||||
if crypto.S3.IsRequested(headers) {
|
||||
err = errInvalidEncryptionParameters
|
||||
return
|
||||
}
|
||||
if err, encrypted = nil, crypto.IsEncrypted(info.UserDefined); !encrypted && crypto.SSEC.IsRequested(headers) {
|
||||
err = errInvalidEncryptionParameters
|
||||
} else if encrypted {
|
||||
if !hasSSECustomerHeader(headers) {
|
||||
apiErr = ErrSSEEncryptedObject
|
||||
if (crypto.SSEC.IsEncrypted(info.UserDefined) && !crypto.SSEC.IsRequested(headers)) ||
|
||||
(crypto.S3.IsEncrypted(info.UserDefined) && crypto.SSEC.IsRequested(headers)) {
|
||||
err = errEncryptedObject
|
||||
return
|
||||
}
|
||||
var err error
|
||||
if info.Size, err = info.DecryptedSize(); err != nil {
|
||||
apiErr = toAPIErrorCode(err)
|
||||
}
|
||||
info.Size, err = info.DecryptedSize()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
+226
-164
@@ -18,21 +18,52 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
)
|
||||
|
||||
var hasServerSideEncryptionHeaderTests = []struct {
|
||||
headers map[string]string
|
||||
sseRequest bool
|
||||
}{
|
||||
{headers: map[string]string{crypto.SSECAlgorithm: "AES256", crypto.SSECKey: "key", crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 0
|
||||
{headers: map[string]string{crypto.SSECAlgorithm: "AES256"}, sseRequest: true}, // 1
|
||||
{headers: map[string]string{crypto.SSECKey: "key"}, sseRequest: true}, // 2
|
||||
{headers: map[string]string{crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 3
|
||||
{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: "", crypto.SSECopyKey: "", crypto.SSECopyKeyMD5: ""}, sseRequest: false}, // 6
|
||||
{headers: map[string]string{crypto.SSEHeader: ""}, sseRequest: true}, // 6
|
||||
}
|
||||
|
||||
func TestHasServerSideEncryptionHeader(t *testing.T) {
|
||||
for i, test := range hasServerSideEncryptionHeaderTests {
|
||||
headers := http.Header{}
|
||||
for k, v := range test.headers {
|
||||
headers.Set(k, v)
|
||||
}
|
||||
if hasServerSideEncryptionHeader(headers) != test.sseRequest {
|
||||
t.Errorf("Test %d: Expected hasServerSideEncryptionHeader to return %v", i, test.sseRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hasSSECopyCustomerHeaderTests = []struct {
|
||||
headers map[string]string
|
||||
sseRequest bool
|
||||
}{
|
||||
{headers: map[string]string{SSECopyCustomerAlgorithm: "AES256", SSECopyCustomerKey: "key", SSECopyCustomerKeyMD5: "md5"}, sseRequest: true}, // 0
|
||||
{headers: map[string]string{SSECopyCustomerAlgorithm: "AES256"}, sseRequest: true}, // 1
|
||||
{headers: map[string]string{SSECopyCustomerKey: "key"}, sseRequest: true}, // 2
|
||||
{headers: map[string]string{SSECopyCustomerKeyMD5: "md5"}, sseRequest: true}, // 3
|
||||
{headers: map[string]string{}, sseRequest: false}, // 4
|
||||
{headers: map[string]string{SSECopyCustomerAlgorithm + " ": "AES256", " " + SSECopyCustomerKey: "key", SSECopyCustomerKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
|
||||
{headers: map[string]string{SSECopyCustomerAlgorithm: "", SSECopyCustomerKey: "", SSECopyCustomerKeyMD5: ""}, sseRequest: false}, // 6
|
||||
{headers: map[string]string{crypto.SSECopyAlgorithm: "AES256", crypto.SSECopyKey: "key", crypto.SSECopyKeyMD5: "md5"}, sseRequest: true}, // 0
|
||||
{headers: map[string]string{crypto.SSECopyAlgorithm: "AES256"}, sseRequest: true}, // 1
|
||||
{headers: map[string]string{crypto.SSECopyKey: "key"}, sseRequest: true}, // 2
|
||||
{headers: map[string]string{crypto.SSECopyKeyMD5: "md5"}, sseRequest: true}, // 3
|
||||
{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: "", crypto.SSECopyKey: "", crypto.SSECopyKeyMD5: ""}, sseRequest: true}, // 6
|
||||
{headers: map[string]string{crypto.SSEHeader: ""}, sseRequest: false}, // 7
|
||||
|
||||
}
|
||||
|
||||
func TestIsSSECopyCustomerRequest(t *testing.T) {
|
||||
@@ -41,8 +72,8 @@ func TestIsSSECopyCustomerRequest(t *testing.T) {
|
||||
for k, v := range test.headers {
|
||||
headers.Set(k, v)
|
||||
}
|
||||
if hasSSECopyCustomerHeader(headers) != test.sseRequest {
|
||||
t.Errorf("Test %d: Expected hasSSECopyCustomerHeader to return %v", i, test.sseRequest)
|
||||
if crypto.SSECopy.IsRequested(headers) != test.sseRequest {
|
||||
t.Errorf("Test %d: Expected crypto.SSECopy.IsRequested to return %v", i, test.sseRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,13 +82,15 @@ var hasSSECustomerHeaderTests = []struct {
|
||||
headers map[string]string
|
||||
sseRequest bool
|
||||
}{
|
||||
{headers: map[string]string{SSECustomerAlgorithm: "AES256", SSECustomerKey: "key", SSECustomerKeyMD5: "md5"}, sseRequest: true}, // 0
|
||||
{headers: map[string]string{SSECustomerAlgorithm: "AES256"}, sseRequest: true}, // 1
|
||||
{headers: map[string]string{SSECustomerKey: "key"}, sseRequest: true}, // 2
|
||||
{headers: map[string]string{SSECustomerKeyMD5: "md5"}, sseRequest: true}, // 3
|
||||
{headers: map[string]string{crypto.SSECAlgorithm: "AES256", crypto.SSECKey: "key", crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 0
|
||||
{headers: map[string]string{crypto.SSECAlgorithm: "AES256"}, sseRequest: true}, // 1
|
||||
{headers: map[string]string{crypto.SSECKey: "key"}, sseRequest: true}, // 2
|
||||
{headers: map[string]string{crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 3
|
||||
{headers: map[string]string{}, sseRequest: false}, // 4
|
||||
{headers: map[string]string{SSECustomerAlgorithm + " ": "AES256", " " + SSECustomerKey: "key", SSECustomerKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
|
||||
{headers: map[string]string{SSECustomerAlgorithm: "", SSECustomerKey: "", SSECustomerKeyMD5: ""}, sseRequest: false}, // 6
|
||||
{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.SSEHeader: ""}, sseRequest: false}, // 7
|
||||
|
||||
}
|
||||
|
||||
func TesthasSSECustomerHeader(t *testing.T) {
|
||||
@@ -66,7 +99,7 @@ func TesthasSSECustomerHeader(t *testing.T) {
|
||||
for k, v := range test.headers {
|
||||
headers.Set(k, v)
|
||||
}
|
||||
if hasSSECustomerHeader(headers) != test.sseRequest {
|
||||
if crypto.SSEC.IsRequested(headers) != test.sseRequest {
|
||||
t.Errorf("Test %d: Expected hasSSECustomerHeader to return %v", i, test.sseRequest)
|
||||
}
|
||||
}
|
||||
@@ -79,75 +112,84 @@ var parseSSECustomerRequestTests = []struct {
|
||||
}{
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
useTLS: true, err: nil,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
|
||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
useTLS: false, err: errInsecureSSERequest,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES 256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
crypto.SSECAlgorithm: "AES 256",
|
||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
|
||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
useTLS: true, err: errInvalidSSEAlgorithm,
|
||||
useTLS: true, err: crypto.ErrInvalidCustomerAlgorithm,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
|
||||
SSECustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
|
||||
crypto.SSECKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||
},
|
||||
useTLS: true, err: errSSEKeyMD5Mismatch,
|
||||
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
|
||||
SSECustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
|
||||
crypto.SSECKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||
},
|
||||
useTLS: true, err: errInvalidSSEKey,
|
||||
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
|
||||
SSECustomerKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
|
||||
crypto.SSECKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
|
||||
},
|
||||
useTLS: true, err: errSSEKeyMD5Mismatch,
|
||||
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
|
||||
SSECustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
|
||||
crypto.SSECKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||
},
|
||||
useTLS: true, err: errInvalidSSEKey,
|
||||
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "", // 7
|
||||
SSECustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "", // 7
|
||||
crypto.SSECKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||
},
|
||||
useTLS: true, err: errMissingSSEKey,
|
||||
useTLS: true, err: crypto.ErrMissingCustomerKey,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
||||
SSECustomerKeyMD5: "",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
||||
crypto.SSECKeyMD5: "",
|
||||
},
|
||||
useTLS: true, err: errMissingSSEKeyMD5,
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -166,8 +208,8 @@ func TestParseSSECustomerRequest(t *testing.T) {
|
||||
if err != test.err {
|
||||
t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err)
|
||||
}
|
||||
key := request.Header.Get(SSECustomerKey)
|
||||
if (err == nil || err == errSSEKeyMD5Mismatch) && key != "" {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -175,81 +217,100 @@ func TestParseSSECustomerRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
var parseSSECopyCustomerRequestTests = []struct {
|
||||
headers map[string]string
|
||||
useTLS bool
|
||||
err error
|
||||
headers map[string]string
|
||||
metadata map[string]string
|
||||
useTLS bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECopyCustomerAlgorithm: "AES256",
|
||||
SSECopyCustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
||||
SSECopyCustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
crypto.SSECopyAlgorithm: "AES256",
|
||||
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
||||
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
useTLS: true, err: nil,
|
||||
metadata: map[string]string{},
|
||||
useTLS: true, err: nil,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECopyCustomerAlgorithm: "AES256",
|
||||
SSECopyCustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
|
||||
SSECopyCustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
crypto.SSECopyAlgorithm: "AES256",
|
||||
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
||||
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
useTLS: false, err: errInsecureSSERequest,
|
||||
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{
|
||||
SSECopyCustomerAlgorithm: "AES 256",
|
||||
SSECopyCustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
|
||||
SSECopyCustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
crypto.SSECopyAlgorithm: "AES256",
|
||||
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
|
||||
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
useTLS: true, err: errInvalidSSEAlgorithm,
|
||||
metadata: map[string]string{},
|
||||
useTLS: false, err: errInsecureSSERequest,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECopyCustomerAlgorithm: "AES256",
|
||||
SSECopyCustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
|
||||
SSECopyCustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||
crypto.SSECopyAlgorithm: "AES 256",
|
||||
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
|
||||
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
useTLS: true, err: errSSEKeyMD5Mismatch,
|
||||
metadata: map[string]string{},
|
||||
useTLS: true, err: crypto.ErrInvalidCustomerAlgorithm,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECopyCustomerAlgorithm: "AES256",
|
||||
SSECopyCustomerKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
|
||||
SSECopyCustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||
crypto.SSECopyAlgorithm: "AES256",
|
||||
crypto.SSECopyKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
|
||||
crypto.SSECopyKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||
},
|
||||
useTLS: true, err: errInvalidSSEKey,
|
||||
metadata: map[string]string{},
|
||||
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECopyCustomerAlgorithm: "AES256",
|
||||
SSECopyCustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
|
||||
SSECopyCustomerKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
|
||||
crypto.SSECopyAlgorithm: "AES256",
|
||||
crypto.SSECopyKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
|
||||
crypto.SSECopyKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||
},
|
||||
useTLS: true, err: errSSEKeyMD5Mismatch,
|
||||
metadata: map[string]string{},
|
||||
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECopyCustomerAlgorithm: "AES256",
|
||||
SSECopyCustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
|
||||
SSECopyCustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||
crypto.SSECopyAlgorithm: "AES256",
|
||||
crypto.SSECopyKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
|
||||
crypto.SSECopyKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
|
||||
},
|
||||
useTLS: true, err: errInvalidSSEKey,
|
||||
metadata: map[string]string{},
|
||||
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECopyCustomerAlgorithm: "AES256",
|
||||
SSECopyCustomerKey: "", // 7
|
||||
SSECopyCustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||
crypto.SSECopyAlgorithm: "AES256",
|
||||
crypto.SSECopyKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
|
||||
crypto.SSECopyKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||
},
|
||||
useTLS: true, err: errMissingSSEKey,
|
||||
metadata: map[string]string{},
|
||||
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
||||
},
|
||||
{
|
||||
headers: map[string]string{
|
||||
SSECopyCustomerAlgorithm: "AES256",
|
||||
SSECopyCustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
||||
SSECopyCustomerKeyMD5: "",
|
||||
crypto.SSECopyAlgorithm: "AES256",
|
||||
crypto.SSECopyKey: "", // 7
|
||||
crypto.SSECopyKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||
},
|
||||
useTLS: true, err: errMissingSSEKeyMD5,
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -264,12 +325,12 @@ func TestParseSSECopyCustomerRequest(t *testing.T) {
|
||||
request.Header = headers
|
||||
globalIsSSL = test.useTLS
|
||||
|
||||
_, err := ParseSSECopyCustomerRequest(request)
|
||||
_, 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(SSECopyCustomerKey)
|
||||
if (err == nil || err == errSSEKeyMD5Mismatch) && key != "" {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -281,20 +342,20 @@ var encryptRequestTests = []struct {
|
||||
}{
|
||||
{
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
metadata: map[string]string{},
|
||||
},
|
||||
{
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -309,19 +370,20 @@ func TestEncryptRequest(t *testing.T) {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
_, err := EncryptRequest(content, req, "bucket", "object", test.metadata)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
||||
}
|
||||
if key, ok := test.metadata[SSECustomerKey]; ok {
|
||||
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[ServerSideEncryptionSealAlgorithm]; !ok {
|
||||
if kdf, ok := test.metadata[crypto.SSESealAlgorithm]; !ok {
|
||||
t.Errorf("Test %d: ServerSideEncryptionKDF must be part of metadata: %v", i, kdf)
|
||||
}
|
||||
if iv, ok := test.metadata[ServerSideEncryptionIV]; !ok {
|
||||
t.Errorf("Test %d: ServerSideEncryptionIV must be part of metadata: %v", i, iv)
|
||||
if iv, ok := test.metadata[crypto.SSEIV]; !ok {
|
||||
t.Errorf("Test %d: crypto.SSEIV must be part of metadata: %v", i, iv)
|
||||
}
|
||||
if mac, ok := test.metadata[ServerSideEncryptionSealedKey]; !ok {
|
||||
if mac, ok := test.metadata[crypto.SSECSealedKey]; !ok {
|
||||
t.Errorf("Test %d: ServerSideEncryptionKeyMAC must be part of metadata: %v", i, mac)
|
||||
}
|
||||
}
|
||||
@@ -337,14 +399,14 @@ var decryptRequestTests = []struct {
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||
SSECustomerKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256,
|
||||
ServerSideEncryptionIV: "7nQqotA8xgrPx6QK7Ap3GCfjKitqJSrGP7xzgErSJlw=",
|
||||
ServerSideEncryptionSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA==",
|
||||
crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
|
||||
crypto.SSEIV: "7nQqotA8xgrPx6QK7Ap3GCfjKitqJSrGP7xzgErSJlw=",
|
||||
crypto.SSECSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA==",
|
||||
},
|
||||
shouldFail: false,
|
||||
},
|
||||
@@ -352,14 +414,14 @@ var decryptRequestTests = []struct {
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||
SSECustomerKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareV2HmacSha256,
|
||||
ServerSideEncryptionIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
||||
ServerSideEncryptionSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
||||
crypto.SSESealAlgorithm: SSESealAlgorithmDareV2HmacSha256,
|
||||
crypto.SSEIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
||||
crypto.SSECSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
||||
},
|
||||
shouldFail: false,
|
||||
},
|
||||
@@ -367,14 +429,14 @@ var decryptRequestTests = []struct {
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
ServerSideEncryptionSealAlgorithm: "HMAC-SHA3",
|
||||
ServerSideEncryptionIV: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
ServerSideEncryptionSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
||||
crypto.SSESealAlgorithm: "HMAC-SHA3",
|
||||
crypto.SSEIV: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
||||
},
|
||||
shouldFail: true,
|
||||
},
|
||||
@@ -382,14 +444,14 @@ var decryptRequestTests = []struct {
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256,
|
||||
ServerSideEncryptionIV: "RrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
ServerSideEncryptionSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
||||
crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
|
||||
crypto.SSEIV: "RrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
||||
},
|
||||
shouldFail: true,
|
||||
},
|
||||
@@ -397,14 +459,14 @@ var decryptRequestTests = []struct {
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256,
|
||||
ServerSideEncryptionIV: "XAm0dRrJsEsyPb1UuFNezv1bl9ehxuYsgUVC/MUctE2k=",
|
||||
ServerSideEncryptionSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hds4rR9z/CUuPqu5N4=",
|
||||
crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
|
||||
crypto.SSEIV: "XAm0dRrJsEsyPb1UuFNezv1bl9ehxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hds4rR9z/CUuPqu5N4=",
|
||||
},
|
||||
shouldFail: true,
|
||||
},
|
||||
@@ -412,14 +474,14 @@ var decryptRequestTests = []struct {
|
||||
bucket: "bucket",
|
||||
object: "object-2",
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||
SSECustomerKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareV2HmacSha256,
|
||||
ServerSideEncryptionIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
||||
ServerSideEncryptionSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
||||
crypto.SSESealAlgorithm: SSESealAlgorithmDareV2HmacSha256,
|
||||
crypto.SSEIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
||||
crypto.SSECSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
||||
},
|
||||
shouldFail: true,
|
||||
},
|
||||
@@ -441,16 +503,16 @@ func TestDecryptRequest(t *testing.T) {
|
||||
if err == nil && test.shouldFail {
|
||||
t.Fatalf("Test %d: should fail but passed", i)
|
||||
}
|
||||
if key, ok := test.metadata[SSECustomerKey]; ok {
|
||||
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[ServerSideEncryptionSealAlgorithm]; ok && !test.shouldFail {
|
||||
if kdf, ok := test.metadata[crypto.SSESealAlgorithm]; ok && !test.shouldFail {
|
||||
t.Errorf("Test %d: ServerSideEncryptionKDF should not be part of metadata: %v", i, kdf)
|
||||
}
|
||||
if iv, ok := test.metadata[ServerSideEncryptionIV]; ok && !test.shouldFail {
|
||||
t.Errorf("Test %d: ServerSideEncryptionIV should not be part of metadata: %v", i, iv)
|
||||
if iv, ok := test.metadata[crypto.SSEIV]; ok && !test.shouldFail {
|
||||
t.Errorf("Test %d: crypto.SSEIV should not be part of metadata: %v", i, iv)
|
||||
}
|
||||
if mac, ok := test.metadata[ServerSideEncryptionSealedKey]; ok && !test.shouldFail {
|
||||
if mac, ok := test.metadata[crypto.SSECSealedKey]; ok && !test.shouldFail {
|
||||
t.Errorf("Test %d: ServerSideEncryptionKeyMAC should not be part of metadata: %v", i, mac)
|
||||
}
|
||||
}
|
||||
@@ -459,45 +521,45 @@ func TestDecryptRequest(t *testing.T) {
|
||||
var decryptObjectInfoTests = []struct {
|
||||
info ObjectInfo
|
||||
headers http.Header
|
||||
expErr APIErrorCode
|
||||
expErr error
|
||||
}{
|
||||
{
|
||||
info: ObjectInfo{Size: 100},
|
||||
headers: http.Header{},
|
||||
expErr: ErrNone,
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}},
|
||||
expErr: ErrNone,
|
||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
info: ObjectInfo{Size: 0, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}},
|
||||
expErr: ErrNone,
|
||||
info: ObjectInfo{Size: 0, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.SSECSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA=="}},
|
||||
headers: http.Header{},
|
||||
expErr: ErrSSEEncryptedObject,
|
||||
expErr: errEncryptedObject,
|
||||
},
|
||||
{
|
||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{}},
|
||||
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}},
|
||||
expErr: ErrInvalidEncryptionParameters,
|
||||
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||
expErr: errInvalidEncryptionParameters,
|
||||
},
|
||||
{
|
||||
info: ObjectInfo{Size: 31, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}},
|
||||
expErr: ErrObjectTampered,
|
||||
info: ObjectInfo{Size: 31, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||
expErr: errObjectTampered,
|
||||
},
|
||||
}
|
||||
|
||||
func TestDecryptObjectInfo(t *testing.T) {
|
||||
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)
|
||||
} else if enc := test.info.IsEncrypted(); 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)
|
||||
} else if !encrypted && enc != encrypted {
|
||||
t.Errorf("Test %d: Decryption thinks object is not encrypted but it is", i)
|
||||
|
||||
@@ -18,6 +18,8 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
@@ -69,10 +71,26 @@ func getSetIndexes(args []string, totalSizes []uint64) (setIndexes [][]uint64, e
|
||||
return nil, errInvalidArgument
|
||||
}
|
||||
|
||||
// isValidSetSize - checks whether given count is a valid set size for erasure coding.
|
||||
isValidSetSize := func(count uint64) bool {
|
||||
return (count >= setSizes[0] && count <= setSizes[len(setSizes)-1] && count%2 == 0)
|
||||
}
|
||||
|
||||
var customSetDriveCount uint64
|
||||
if v := os.Getenv("MINIO_ERASURE_SET_DRIVE_COUNT"); v != "" {
|
||||
customSetDriveCount, err = strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
return nil, uiErrInvalidErasureSetSize(err)
|
||||
}
|
||||
if !isValidSetSize(customSetDriveCount) {
|
||||
return nil, uiErrInvalidErasureSetSize(nil)
|
||||
}
|
||||
}
|
||||
|
||||
setIndexes = make([][]uint64, len(totalSizes))
|
||||
for _, totalSize := range totalSizes {
|
||||
// Check if totalSize has minimum range upto setSize
|
||||
if totalSize < setSizes[0] {
|
||||
if totalSize < setSizes[0] || totalSize < customSetDriveCount {
|
||||
return nil, uiErrInvalidNumberOfErasureEndpoints(nil)
|
||||
}
|
||||
}
|
||||
@@ -95,9 +113,24 @@ func getSetIndexes(args []string, totalSizes []uint64) (setIndexes [][]uint64, e
|
||||
setSize = commonSize
|
||||
}
|
||||
|
||||
// isValidSetSize - checks whether given count is a valid set size for erasure coding.
|
||||
isValidSetSize := func(count uint64) bool {
|
||||
return (count >= setSizes[0] && count <= setSizes[len(setSizes)-1] && count%2 == 0)
|
||||
possibleSetCounts := func(setSize uint64) (ss []uint64) {
|
||||
for _, s := range setSizes {
|
||||
if setSize%s == 0 {
|
||||
ss = append(ss, s)
|
||||
}
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
if customSetDriveCount > 0 {
|
||||
msg := fmt.Sprintf("Invalid set drive count, leads to non-uniform distribution for the given number of disks. Possible values for custom set count are %d", possibleSetCounts(setSize))
|
||||
if customSetDriveCount > setSize {
|
||||
return nil, uiErrInvalidErasureSetSize(nil).Msg(msg)
|
||||
}
|
||||
if setSize%customSetDriveCount != 0 {
|
||||
return nil, uiErrInvalidErasureSetSize(nil).Msg(msg)
|
||||
}
|
||||
setSize = customSetDriveCount
|
||||
}
|
||||
|
||||
// Check whether setSize is with the supported range.
|
||||
|
||||
@@ -18,6 +18,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@@ -84,6 +85,65 @@ func TestGetDivisibleSize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test tests calculating set indexes with ENV override for drive count.
|
||||
func TestGetSetIndexesEnvOverride(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
totalSizes []uint64
|
||||
indexes [][]uint64
|
||||
envOverride string
|
||||
success bool
|
||||
}{
|
||||
{
|
||||
[]string{"data{1...64}"},
|
||||
[]uint64{64},
|
||||
[][]uint64{{8, 8, 8, 8, 8, 8, 8, 8}},
|
||||
"8",
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"data{1...60}"},
|
||||
nil,
|
||||
nil,
|
||||
"8",
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{"data{1...64}"},
|
||||
nil,
|
||||
nil,
|
||||
"-1",
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{"data{1...64}"},
|
||||
nil,
|
||||
nil,
|
||||
"2",
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
|
||||
if err := os.Setenv("MINIO_ERASURE_SET_DRIVE_COUNT", testCase.envOverride); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes)
|
||||
if err != nil && testCase.success {
|
||||
t.Errorf("Expected success but failed instead %s", err)
|
||||
}
|
||||
if err == nil && !testCase.success {
|
||||
t.Errorf("Expected failure but passed instead")
|
||||
}
|
||||
if !reflect.DeepEqual(testCase.indexes, gotIndexes) {
|
||||
t.Errorf("Expected %v, got %v", testCase.indexes, gotIndexes)
|
||||
}
|
||||
os.Unsetenv("MINIO_ERASURE_SET_DRIVE_COUNT")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test tests calculating set indexes.
|
||||
func TestGetSetIndexes(t *testing.T) {
|
||||
testCases := []struct {
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
// CreateFile creates a new bitrot encoded file spread over all available disks. CreateFile will create
|
||||
// the file at the given volume and path. It will read from src until an io.EOF occurs. The given algorithm will
|
||||
// be used to protect the erasure encoded file.
|
||||
func (s *ErasureStorage) CreateFile(ctx context.Context, src io.Reader, volume, path string, buffer []byte, algorithm BitrotAlgorithm, writeQuorum int) (f ErasureFileInfo, err error) {
|
||||
if !algorithm.Available() {
|
||||
logger.LogIf(ctx, errBitrotHashAlgoInvalid)
|
||||
return f, errBitrotHashAlgoInvalid
|
||||
}
|
||||
f.Checksums = make([][]byte, len(s.disks))
|
||||
hashers := make([]hash.Hash, len(s.disks))
|
||||
for i := range hashers {
|
||||
hashers[i] = algorithm.New()
|
||||
}
|
||||
errChans, errs := make([]chan error, len(s.disks)), make([]error, len(s.disks))
|
||||
for i := range errChans {
|
||||
errChans[i] = make(chan error, 1) // create buffered channel to let finished go-routines die early
|
||||
}
|
||||
|
||||
var blocks [][]byte
|
||||
var n = len(buffer)
|
||||
for n == len(buffer) {
|
||||
n, err = io.ReadFull(src, buffer)
|
||||
if n == 0 && err == io.EOF {
|
||||
if f.Size != 0 { // don't write empty block if we have written to the disks
|
||||
break
|
||||
}
|
||||
blocks = make([][]byte, len(s.disks)) // write empty block
|
||||
} else if err == nil || (n > 0 && err == io.ErrUnexpectedEOF) {
|
||||
blocks, err = s.ErasureEncode(ctx, buffer[:n])
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
} else {
|
||||
logger.LogIf(ctx, err)
|
||||
return f, err
|
||||
}
|
||||
|
||||
for i := range errChans { // span workers
|
||||
go erasureAppendFile(ctx, s.disks[i], volume, path, hashers[i], blocks[i], errChans[i])
|
||||
}
|
||||
for i := range errChans { // wait until all workers are finished
|
||||
errs[i] = <-errChans[i]
|
||||
}
|
||||
if err = reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum); err != nil {
|
||||
return f, err
|
||||
}
|
||||
s.disks = evalDisks(s.disks, errs)
|
||||
f.Size += int64(n)
|
||||
}
|
||||
|
||||
f.Algorithm = algorithm
|
||||
for i, disk := range s.disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
f.Checksums[i] = hashers[i].Sum(nil)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// erasureAppendFile appends the content of buf to the file on the given disk and updates computes
|
||||
// the hash of the written data. It sends the write error (or nil) over the error channel.
|
||||
func erasureAppendFile(ctx context.Context, disk StorageAPI, volume, path string, hash hash.Hash, buf []byte, errChan chan<- error) {
|
||||
if disk == OfflineDisk {
|
||||
logger.LogIf(ctx, errDiskNotFound)
|
||||
errChan <- errDiskNotFound
|
||||
return
|
||||
}
|
||||
err := disk.AppendFile(volume, path, buf)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
hash.Write(buf)
|
||||
errChan <- err
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
// Reads in parallel from bitrotReaders.
|
||||
type parallelReader struct {
|
||||
readers []*bitrotReader
|
||||
dataBlocks int
|
||||
offset int64
|
||||
shardSize int64
|
||||
shardFileSize int64
|
||||
}
|
||||
|
||||
// newParallelReader returns parallelReader.
|
||||
func newParallelReader(readers []*bitrotReader, dataBlocks int, offset int64, fileSize int64, blocksize int64) *parallelReader {
|
||||
shardSize := ceilFrac(blocksize, int64(dataBlocks))
|
||||
shardFileSize := getErasureShardFileSize(blocksize, fileSize, dataBlocks)
|
||||
return ¶llelReader{
|
||||
readers,
|
||||
dataBlocks,
|
||||
(offset / blocksize) * shardSize,
|
||||
shardSize,
|
||||
shardFileSize,
|
||||
}
|
||||
}
|
||||
|
||||
// Returns if buf can be erasure decoded.
|
||||
func (p *parallelReader) canDecode(buf [][]byte) bool {
|
||||
bufCount := 0
|
||||
for _, b := range buf {
|
||||
if b != nil {
|
||||
bufCount++
|
||||
}
|
||||
}
|
||||
return bufCount >= p.dataBlocks
|
||||
}
|
||||
|
||||
// Read reads from bitrotReaders in parallel. Returns p.dataBlocks number of bufs.
|
||||
func (p *parallelReader) Read() ([][]byte, error) {
|
||||
type errIdx struct {
|
||||
idx int
|
||||
buf []byte
|
||||
err error
|
||||
}
|
||||
|
||||
errCh := make(chan errIdx)
|
||||
currReaderIndex := 0
|
||||
newBuf := make([][]byte, len(p.readers))
|
||||
|
||||
if p.offset+p.shardSize > p.shardFileSize {
|
||||
p.shardSize = p.shardFileSize - p.offset
|
||||
}
|
||||
|
||||
read := func(currReaderIndex int) {
|
||||
b, err := p.readers[currReaderIndex].ReadChunk(p.offset, p.shardSize)
|
||||
errCh <- errIdx{currReaderIndex, b, err}
|
||||
}
|
||||
|
||||
readerCount := 0
|
||||
for _, r := range p.readers {
|
||||
if r != nil {
|
||||
readerCount++
|
||||
}
|
||||
}
|
||||
if readerCount < p.dataBlocks {
|
||||
return nil, errXLReadQuorum
|
||||
}
|
||||
|
||||
readerCount = 0
|
||||
for i, r := range p.readers {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
go read(i)
|
||||
readerCount++
|
||||
if readerCount == p.dataBlocks {
|
||||
currReaderIndex = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for errVal := range errCh {
|
||||
if errVal.err == nil {
|
||||
newBuf[errVal.idx] = errVal.buf
|
||||
if p.canDecode(newBuf) {
|
||||
p.offset += int64(p.shardSize)
|
||||
return newBuf, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
p.readers[errVal.idx] = nil
|
||||
for currReaderIndex < len(p.readers) {
|
||||
if p.readers[currReaderIndex] != nil {
|
||||
break
|
||||
}
|
||||
currReaderIndex++
|
||||
}
|
||||
|
||||
if currReaderIndex == len(p.readers) {
|
||||
break
|
||||
}
|
||||
go read(currReaderIndex)
|
||||
currReaderIndex++
|
||||
}
|
||||
|
||||
return nil, errXLReadQuorum
|
||||
}
|
||||
|
||||
// Decode reads from readers, reconstructs data if needed and writes the data to the writer.
|
||||
func (e Erasure) Decode(ctx context.Context, writer io.Writer, readers []*bitrotReader, offset, length, totalLength int64) error {
|
||||
if offset < 0 || length < 0 {
|
||||
logger.LogIf(ctx, errInvalidArgument)
|
||||
return errInvalidArgument
|
||||
}
|
||||
if offset+length > totalLength {
|
||||
logger.LogIf(ctx, errInvalidArgument)
|
||||
return errInvalidArgument
|
||||
}
|
||||
if length == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
reader := newParallelReader(readers, e.dataBlocks, offset, totalLength, e.blockSize)
|
||||
|
||||
startBlock := offset / e.blockSize
|
||||
endBlock := (offset + length) / e.blockSize
|
||||
|
||||
var bytesWritten int64
|
||||
for block := startBlock; block <= endBlock; block++ {
|
||||
var blockOffset, blockLength int64
|
||||
switch {
|
||||
case startBlock == endBlock:
|
||||
blockOffset = offset % e.blockSize
|
||||
blockLength = length
|
||||
case block == startBlock:
|
||||
blockOffset = offset % e.blockSize
|
||||
blockLength = e.blockSize - blockOffset
|
||||
case block == endBlock:
|
||||
blockOffset = 0
|
||||
blockLength = (offset + length) % e.blockSize
|
||||
default:
|
||||
blockOffset = 0
|
||||
blockLength = e.blockSize
|
||||
}
|
||||
if blockLength == 0 {
|
||||
break
|
||||
}
|
||||
bufs, err := reader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = e.DecodeDataBlocks(bufs); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
n, err := writeDataBlocks(ctx, writer, bufs, e.dataBlocks, blockOffset, blockLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bytesWritten += n
|
||||
}
|
||||
if bytesWritten != length {
|
||||
logger.LogIf(ctx, errLessData)
|
||||
return errLessData
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -19,11 +19,12 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"io"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
crand "crypto/rand"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
@@ -31,7 +32,7 @@ func (d badDisk) ReadFile(volume string, path string, offset int64, buf []byte,
|
||||
return 0, errFaultyDisk
|
||||
}
|
||||
|
||||
var erasureReadFileTests = []struct {
|
||||
var erasureDecodeTests = []struct {
|
||||
dataBlocks int
|
||||
onDisks, offDisks int
|
||||
blocksize, data int64
|
||||
@@ -66,50 +67,74 @@ var erasureReadFileTests = []struct {
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 23
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 24
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 25
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: 0, shouldFail: true, shouldFailQuorum: false}, // 26
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1) + 1, offset: 0, length: int64(blockSizeV1) + 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 27
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 28
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 29
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 30
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 512, length: int64(blockSizeV1) + 8*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 31
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: int64(blockSizeV1), length: int64(blockSizeV1) - 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 32
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: -1, length: 3, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 33
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: 1024, length: -1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 34
|
||||
{dataBlocks: 4, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1), offset: 0, length: int64(blockSizeV1), algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 35
|
||||
{dataBlocks: 4, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 36
|
||||
{dataBlocks: 4, onDisks: 6, offDisks: 3, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 37
|
||||
{dataBlocks: 8, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 38
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1) + 1, offset: 0, length: int64(blockSizeV1) + 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 26
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 27
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 28
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 29
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 512, length: int64(blockSizeV1) + 8*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 30
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: int64(blockSizeV1), length: int64(blockSizeV1) - 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 31
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: -1, length: 3, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 32
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: 1024, length: -1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 33
|
||||
{dataBlocks: 4, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1), offset: 0, length: int64(blockSizeV1), algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 34
|
||||
{dataBlocks: 4, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 35
|
||||
{dataBlocks: 4, onDisks: 6, offDisks: 3, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 36
|
||||
{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) {
|
||||
for i, test := range erasureReadFileTests {
|
||||
func TestErasureDecode(t *testing.T) {
|
||||
for i, test := range erasureDecodeTests {
|
||||
setup, err := newErasureTestSetup(test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
||||
}
|
||||
storage, err := NewErasureStorage(context.Background(), setup.disks, test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
erasure, err := NewErasure(context.Background(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||
}
|
||||
|
||||
disks := setup.disks
|
||||
data := make([]byte, test.data)
|
||||
if _, err = io.ReadFull(crand.Reader, data); err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
|
||||
}
|
||||
|
||||
writeAlgorithm := test.algorithm
|
||||
if !test.algorithm.Available() {
|
||||
writeAlgorithm = DefaultBitrotAlgorithm
|
||||
}
|
||||
buffer := make([]byte, test.blocksize, 2*test.blocksize)
|
||||
file, err := storage.CreateFile(context.Background(), bytes.NewReader(data[:]), "testbucket", "object", buffer, writeAlgorithm, test.dataBlocks+1)
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
for i, disk := range disks {
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", writeAlgorithm)
|
||||
}
|
||||
n, err := erasure.Encode(context.Background(), bytes.NewReader(data[:]), writers, buffer, erasure.dataBlocks+1)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create erasure test file: %v", i, err)
|
||||
}
|
||||
if n != test.data {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create erasure test file", i)
|
||||
}
|
||||
for i, w := range writers {
|
||||
if w == nil {
|
||||
disks[i] = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Get the checksums of the current part.
|
||||
bitrotReaders := make([]*bitrotReader, len(disks))
|
||||
for index, disk := range disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
endOffset := getErasureShardFileEndOffset(test.offset, test.length, test.data, test.blocksize, erasure.dataBlocks)
|
||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", writeAlgorithm, endOffset, writers[index].Sum())
|
||||
}
|
||||
|
||||
writer := bytes.NewBuffer(nil)
|
||||
readInfo, err := storage.ReadFile(context.Background(), writer, "testbucket", "object", test.offset, test.length, test.data, file.Checksums, test.algorithm, test.blocksize)
|
||||
err = erasure.Decode(context.Background(), writer, bitrotReaders, test.offset, test.length, test.data)
|
||||
if err != nil && !test.shouldFail {
|
||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||
}
|
||||
@@ -117,25 +142,32 @@ func TestErasureReadFile(t *testing.T) {
|
||||
t.Errorf("Test %d: should fail but it passed", i)
|
||||
}
|
||||
if err == nil {
|
||||
if readInfo.Size != test.length {
|
||||
t.Errorf("Test %d: read returns wrong number of bytes: got: #%d want: #%d", i, readInfo.Size, test.length)
|
||||
}
|
||||
if readInfo.Algorithm != test.algorithm {
|
||||
t.Errorf("Test %d: read returns wrong algorithm: got: %v want: %v", i, readInfo.Algorithm, test.algorithm)
|
||||
}
|
||||
if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) {
|
||||
t.Errorf("Test %d: read retruns wrong file content", i)
|
||||
}
|
||||
}
|
||||
for i, r := range bitrotReaders {
|
||||
if r == nil {
|
||||
disks[i] = OfflineDisk
|
||||
}
|
||||
}
|
||||
if err == nil && !test.shouldFail {
|
||||
writer.Reset()
|
||||
for j := range storage.disks[:test.offDisks] {
|
||||
storage.disks[j] = badDisk{nil}
|
||||
bitrotReaders = make([]*bitrotReader, len(disks))
|
||||
for index, disk := range disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
endOffset := getErasureShardFileEndOffset(test.offset, test.length, test.data, test.blocksize, erasure.dataBlocks)
|
||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", writeAlgorithm, endOffset, writers[index].Sum())
|
||||
}
|
||||
for j := range disks[:test.offDisks] {
|
||||
bitrotReaders[j].disk = badDisk{nil}
|
||||
}
|
||||
if test.offDisks > 0 {
|
||||
storage.disks[0] = OfflineDisk
|
||||
bitrotReaders[0] = nil
|
||||
}
|
||||
readInfo, err = storage.ReadFile(context.Background(), writer, "testbucket", "object", test.offset, test.length, test.data, file.Checksums, test.algorithm, test.blocksize)
|
||||
writer.Reset()
|
||||
err = erasure.Decode(context.Background(), writer, bitrotReaders, test.offset, test.length, test.data)
|
||||
if err != nil && !test.shouldFailQuorum {
|
||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||
}
|
||||
@@ -143,12 +175,6 @@ func TestErasureReadFile(t *testing.T) {
|
||||
t.Errorf("Test %d: should fail but it passed", i)
|
||||
}
|
||||
if !test.shouldFailQuorum {
|
||||
if readInfo.Size != test.length {
|
||||
t.Errorf("Test %d: read returns wrong number of bytes: got: #%d want: #%d", i, readInfo.Size, test.length)
|
||||
}
|
||||
if readInfo.Algorithm != test.algorithm {
|
||||
t.Errorf("Test %d: read returns wrong algorithm: got: %v want: %v", i, readInfo.Algorithm, test.algorithm)
|
||||
}
|
||||
if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) {
|
||||
t.Errorf("Test %d: read retruns wrong file content", i)
|
||||
}
|
||||
@@ -158,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
|
||||
// explicitly after commenting out t.Skip()
|
||||
func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
||||
func TestErasureDecodeRandomOffsetLength(t *testing.T) {
|
||||
// Comment the following line to run this test.
|
||||
t.SkipNow()
|
||||
// Initialize environment needed for the test.
|
||||
@@ -174,8 +200,8 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
||||
return
|
||||
}
|
||||
defer setup.Remove()
|
||||
|
||||
storage, err := NewErasureStorage(context.Background(), setup.disks, dataBlocks, parityBlocks, blockSize)
|
||||
disks := setup.disks
|
||||
erasure, err := NewErasure(context.Background(), dataBlocks, parityBlocks, blockSize)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create ErasureStorage: %v", err)
|
||||
}
|
||||
@@ -187,17 +213,25 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
for i, disk := range disks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
|
||||
}
|
||||
|
||||
// 10000 iterations with random offsets and lengths.
|
||||
iterations := 10000
|
||||
|
||||
// Create a test file to read from.
|
||||
buffer := make([]byte, blockSize, 2*blockSize)
|
||||
file, err := storage.CreateFile(context.Background(), bytes.NewReader(data), "testbucket", "testobject", buffer, DefaultBitrotAlgorithm, dataBlocks+1)
|
||||
n, err := erasure.Encode(context.Background(), bytes.NewReader(data), writers, buffer, erasure.dataBlocks+1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if file.Size != length {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", file.Size, length)
|
||||
if n != length {
|
||||
t.Errorf("erasureCreateFile returned %d, expected %d", n, length)
|
||||
}
|
||||
|
||||
// To generate random offset/length.
|
||||
@@ -205,14 +239,23 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
// Verify erasureReadFile() for random offsets and lengths.
|
||||
// Verify erasure.Decode() for random offsets and lengths.
|
||||
for i := 0; i < iterations; i++ {
|
||||
offset := r.Int63n(length)
|
||||
readLen := r.Int63n(length - offset)
|
||||
|
||||
expected := data[offset : offset+readLen]
|
||||
|
||||
_, err = storage.ReadFile(context.Background(), buf, "testbucket", "testobject", offset, readLen, length, file.Checksums, DefaultBitrotAlgorithm, blockSize)
|
||||
// Get the checksums of the current part.
|
||||
bitrotReaders := make([]*bitrotReader, len(disks))
|
||||
for index, disk := range disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
endOffset := getErasureShardFileEndOffset(offset, readLen, length, blockSize, erasure.dataBlocks)
|
||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", DefaultBitrotAlgorithm, endOffset, writers[index].Sum())
|
||||
}
|
||||
err = erasure.Decode(context.Background(), buf, bitrotReaders, offset, readLen, length)
|
||||
if err != nil {
|
||||
t.Fatal(err, offset, readLen)
|
||||
}
|
||||
@@ -226,89 +269,105 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create test setup: %v", err)
|
||||
}
|
||||
defer setup.Remove()
|
||||
storage, err := NewErasureStorage(context.Background(), setup.disks, data, parity, blockSizeV1)
|
||||
disks := setup.disks
|
||||
erasure, err := NewErasure(context.Background(), data, parity, blockSizeV1)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create ErasureStorage: %v", err)
|
||||
}
|
||||
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
for i, disk := range disks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
|
||||
}
|
||||
|
||||
content := make([]byte, size)
|
||||
buffer := make([]byte, blockSizeV1, 2*blockSizeV1)
|
||||
file, err := storage.CreateFile(context.Background(), bytes.NewReader(content), "testbucket", "object", buffer, DefaultBitrotAlgorithm, data+1)
|
||||
_, err = erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create erasure test file: %v", err)
|
||||
}
|
||||
checksums := file.Checksums
|
||||
|
||||
for i := 0; i < dataDown; i++ {
|
||||
storage.disks[i] = OfflineDisk
|
||||
writers[i] = nil
|
||||
}
|
||||
for i := data; i < data+parityDown; i++ {
|
||||
storage.disks[i] = OfflineDisk
|
||||
writers[i] = nil
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.SetBytes(size)
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if file, err = storage.ReadFile(context.Background(), bytes.NewBuffer(content[:0]), "testbucket", "object", 0, size, size, checksums, DefaultBitrotAlgorithm, blockSizeV1); err != nil {
|
||||
bitrotReaders := make([]*bitrotReader, len(disks))
|
||||
for index, disk := range disks {
|
||||
if writers[index] == nil {
|
||||
continue
|
||||
}
|
||||
endOffset := getErasureShardFileEndOffset(0, size, size, erasure.blockSize, erasure.dataBlocks)
|
||||
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", DefaultBitrotAlgorithm, endOffset, writers[index].Sum())
|
||||
}
|
||||
if err = erasure.Decode(context.Background(), bytes.NewBuffer(content[:0]), bitrotReaders, 0, size, size); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkErasureReadQuick(b *testing.B) {
|
||||
func BenchmarkErasureDecodeQuick(b *testing.B) {
|
||||
const size = 12 * 1024 * 1024
|
||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 0, size, b) })
|
||||
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 1, size, b) })
|
||||
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 1, 0, size, b) })
|
||||
b.Run(" X0|X0 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 1, 1, size, b) })
|
||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 0, 0, 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) { benchmarkErasureDecode(2, 2, 1, 0, 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
|
||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 0, size, b) })
|
||||
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 1, size, b) })
|
||||
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 1, 0, size, b) })
|
||||
b.Run(" X0|X0 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 1, 1, size, b) })
|
||||
b.Run(" 00|XX ", func(b *testing.B) { benchmarkErasureRead(2, 2, 0, 2, size, b) })
|
||||
b.Run(" XX|00 ", func(b *testing.B) { benchmarkErasureRead(2, 2, 2, 0, size, b) })
|
||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureDecode(2, 2, 0, 0, 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) { benchmarkErasureDecode(2, 2, 1, 0, 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) { benchmarkErasureDecode(2, 2, 0, 2, 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
|
||||
b.Run(" 0000|0000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 0, 0, size, b) })
|
||||
b.Run(" 0000|X000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 0, 1, size, b) })
|
||||
b.Run(" X000|0000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 1, 0, size, b) })
|
||||
b.Run(" X000|X000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 1, 1, size, b) })
|
||||
b.Run(" 0000|XXXX ", func(b *testing.B) { benchmarkErasureRead(4, 4, 0, 4, size, b) })
|
||||
b.Run(" XX00|XX00 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 2, 2, size, b) })
|
||||
b.Run(" XXXX|0000 ", func(b *testing.B) { benchmarkErasureRead(4, 4, 4, 0, size, b) })
|
||||
b.Run(" 0000|0000 ", func(b *testing.B) { benchmarkErasureDecode(4, 4, 0, 0, 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) { benchmarkErasureDecode(4, 4, 1, 0, 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) { benchmarkErasureDecode(4, 4, 0, 4, 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) { 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
|
||||
b.Run(" 000000|000000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 0, 0, size, b) })
|
||||
b.Run(" 000000|X00000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 0, 1, size, b) })
|
||||
b.Run(" X00000|000000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 1, 0, size, b) })
|
||||
b.Run(" X00000|X00000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 1, 1, size, b) })
|
||||
b.Run(" 000000|XXXXXX ", func(b *testing.B) { benchmarkErasureRead(6, 6, 0, 6, size, b) })
|
||||
b.Run(" XXX000|XXX000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 3, 3, size, b) })
|
||||
b.Run(" XXXXXX|000000 ", func(b *testing.B) { benchmarkErasureRead(6, 6, 6, 0, size, b) })
|
||||
b.Run(" 000000|000000 ", func(b *testing.B) { benchmarkErasureDecode(6, 6, 0, 0, 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) { benchmarkErasureDecode(6, 6, 1, 0, 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) { benchmarkErasureDecode(6, 6, 0, 6, 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) { 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
|
||||
b.Run(" 00000000|00000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 0, 0, size, b) })
|
||||
b.Run(" 00000000|X0000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 0, 1, size, b) })
|
||||
b.Run(" X0000000|00000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 1, 0, size, b) })
|
||||
b.Run(" X0000000|X0000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 1, 1, size, b) })
|
||||
b.Run(" 00000000|XXXXXXXX ", func(b *testing.B) { benchmarkErasureRead(8, 8, 0, 8, size, b) })
|
||||
b.Run(" XXXX0000|XXXX0000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 4, 4, size, b) })
|
||||
b.Run(" XXXXXXXX|00000000 ", func(b *testing.B) { benchmarkErasureRead(8, 8, 8, 0, size, b) })
|
||||
b.Run(" 00000000|00000000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 0, 0, 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) { benchmarkErasureDecode(8, 8, 1, 0, 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) { benchmarkErasureDecode(8, 8, 0, 8, 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) { benchmarkErasureDecode(8, 8, 8, 0, size, b) })
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
// Writes in parallel to bitrotWriters
|
||||
type parallelWriter struct {
|
||||
writers []*bitrotWriter
|
||||
writeQuorum int
|
||||
errs []error
|
||||
}
|
||||
|
||||
// Append appends data to bitrotWriters in parallel.
|
||||
func (p *parallelWriter) Append(ctx context.Context, blocks [][]byte) error {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := range p.writers {
|
||||
if p.writers[i] == nil {
|
||||
p.errs[i] = errDiskNotFound
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
p.errs[i] = p.writers[i].Append(blocks[i])
|
||||
if p.errs[i] != nil {
|
||||
p.writers[i] = nil
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// If nilCount >= p.writeQuorum, we return nil. This is because HealFile() uses
|
||||
// CreateFile with p.writeQuorum=1 to accommodate healing of single disk.
|
||||
// i.e if we do no return here in such a case, reduceWriteQuorumErrs() would
|
||||
// return a quorum error to HealFile().
|
||||
nilCount := 0
|
||||
for _, err := range p.errs {
|
||||
if err == nil {
|
||||
nilCount++
|
||||
}
|
||||
}
|
||||
if nilCount >= p.writeQuorum {
|
||||
return nil
|
||||
}
|
||||
return reduceWriteQuorumErrs(ctx, p.errs, objectOpIgnoredErrs, p.writeQuorum)
|
||||
}
|
||||
|
||||
// Encode reads from the reader, erasure-encodes the data and writes to the writers.
|
||||
func (e *Erasure) Encode(ctx context.Context, src io.Reader, writers []*bitrotWriter, buf []byte, quorum int) (total int64, err error) {
|
||||
writer := ¶llelWriter{
|
||||
writers: writers,
|
||||
writeQuorum: quorum,
|
||||
errs: make([]error, len(writers)),
|
||||
}
|
||||
|
||||
for {
|
||||
var blocks [][]byte
|
||||
n, err := io.ReadFull(src, buf)
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
logger.LogIf(ctx, err)
|
||||
return 0, err
|
||||
}
|
||||
eof := err == io.EOF || err == io.ErrUnexpectedEOF
|
||||
if n == 0 && total != 0 {
|
||||
// Reached EOF, nothing more to be done.
|
||||
break
|
||||
}
|
||||
// We take care of the situation where if n == 0 and total == 0 by creating empty data and parity files.
|
||||
blocks, err = e.EncodeData(ctx, buf[:n])
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err = writer.Append(ctx, blocks); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return 0, err
|
||||
}
|
||||
total += int64(n)
|
||||
if eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func (a badDisk) AppendFile(volume string, path string, buf []byte) error {
|
||||
|
||||
const oneMiByte = 1 * humanize.MiByte
|
||||
|
||||
var erasureCreateFileTests = []struct {
|
||||
var erasureEncodeTests = []struct {
|
||||
dataBlocks int
|
||||
onDisks, offDisks int
|
||||
blocksize, data int64
|
||||
@@ -54,24 +54,24 @@ var erasureCreateFileTests = []struct {
|
||||
{dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 9
|
||||
{dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 10
|
||||
{dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11
|
||||
{dataBlocks: 6, onDisks: 12, offDisks: 5, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 102, algorithm: 0, shouldFail: true, shouldFailQuorum: false}, // 12
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(oneMiByte / 2), data: oneMiByte, offset: oneMiByte/2 + 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 14
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(oneMiByte - 1), data: oneMiByte, offset: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 15
|
||||
{dataBlocks: 8, onDisks: 12, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 16
|
||||
{dataBlocks: 8, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17
|
||||
{dataBlocks: 10, onDisks: 14, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 17, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 18
|
||||
{dataBlocks: 2, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 19
|
||||
{dataBlocks: 10, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 20
|
||||
{dataBlocks: 3, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 12
|
||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(oneMiByte / 2), data: oneMiByte, offset: oneMiByte/2 + 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
|
||||
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(oneMiByte - 1), data: oneMiByte, offset: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 14
|
||||
{dataBlocks: 8, onDisks: 12, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 15
|
||||
{dataBlocks: 8, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 16
|
||||
{dataBlocks: 10, onDisks: 14, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 17, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17
|
||||
{dataBlocks: 2, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 18
|
||||
{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) {
|
||||
for i, test := range erasureCreateFileTests {
|
||||
func TestErasureEncode(t *testing.T) {
|
||||
for i, test := range erasureEncodeTests {
|
||||
setup, err := newErasureTestSetup(test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
||||
}
|
||||
storage, err := NewErasureStorage(context.Background(), setup.disks, test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
disks := setup.disks
|
||||
erasure, err := NewErasure(context.Background(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||
@@ -83,25 +83,43 @@ func TestErasureCreateFile(t *testing.T) {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
|
||||
}
|
||||
file, err := storage.CreateFile(context.Background(), bytes.NewReader(data[test.offset:]), "testbucket", "object", buffer, test.algorithm, test.dataBlocks+1)
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
for i, disk := range disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", test.algorithm)
|
||||
}
|
||||
n, err := erasure.Encode(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, erasure.dataBlocks+1)
|
||||
if err != nil && !test.shouldFail {
|
||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||
}
|
||||
if err == nil && test.shouldFail {
|
||||
t.Errorf("Test %d: should fail but it passed", i)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if length := int64(len(data[test.offset:])); file.Size != length {
|
||||
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, file.Size, length)
|
||||
for i, w := range writers {
|
||||
if w == nil {
|
||||
disks[i] = OfflineDisk
|
||||
}
|
||||
for j := range storage.disks[:test.offDisks] {
|
||||
storage.disks[j] = badDisk{nil}
|
||||
}
|
||||
if err == nil {
|
||||
if length := int64(len(data[test.offset:])); n != length {
|
||||
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, n, length)
|
||||
}
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
for i, disk := range disks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object2", test.algorithm)
|
||||
}
|
||||
for j := range disks[:test.offDisks] {
|
||||
writers[j].disk = badDisk{nil}
|
||||
}
|
||||
if test.offDisks > 0 {
|
||||
storage.disks[0] = OfflineDisk
|
||||
writers[0] = nil
|
||||
}
|
||||
file, err = storage.CreateFile(context.Background(), bytes.NewReader(data[test.offset:]), "testbucket", "object2", buffer, test.algorithm, test.dataBlocks+1)
|
||||
n, err = erasure.Encode(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, erasure.dataBlocks+1)
|
||||
if err != nil && !test.shouldFailQuorum {
|
||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||
}
|
||||
@@ -109,8 +127,8 @@ func TestErasureCreateFile(t *testing.T) {
|
||||
t.Errorf("Test %d: should fail but it passed", i)
|
||||
}
|
||||
if err == nil {
|
||||
if length := int64(len(data[test.offset:])); file.Size != length {
|
||||
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, file.Size, length)
|
||||
if length := int64(len(data[test.offset:])); n != length {
|
||||
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, n, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,74 +138,82 @@ func TestErasureCreateFile(t *testing.T) {
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create test setup: %v", err)
|
||||
}
|
||||
defer setup.Remove()
|
||||
storage, err := NewErasureStorage(context.Background(), setup.disks, data, parity, blockSizeV1)
|
||||
erasure, err := NewErasure(context.Background(), data, parity, blockSizeV1)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create ErasureStorage: %v", err)
|
||||
}
|
||||
disks := setup.disks
|
||||
buffer := make([]byte, blockSizeV1, 2*blockSizeV1)
|
||||
content := make([]byte, size)
|
||||
|
||||
for i := 0; i < dataDown; i++ {
|
||||
storage.disks[i] = OfflineDisk
|
||||
disks[i] = OfflineDisk
|
||||
}
|
||||
for i := data; i < data+parityDown; i++ {
|
||||
storage.disks[i] = OfflineDisk
|
||||
disks[i] = OfflineDisk
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.SetBytes(size)
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := storage.CreateFile(context.Background(), bytes.NewReader(content), "testbucket", "object", buffer, DefaultBitrotAlgorithm, data+1)
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
for i, disk := range disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
|
||||
}
|
||||
_, err := erasure.Encode(context.Background(), bytes.NewReader(content), writers, buffer, erasure.dataBlocks+1)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkErasureWriteQuick(b *testing.B) {
|
||||
func BenchmarkErasureEncodeQuick(b *testing.B) {
|
||||
const size = 12 * 1024 * 1024
|
||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 0, 0, size, b) })
|
||||
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 0, 1, size, b) })
|
||||
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 1, 0, size, b) })
|
||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 0, 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) { 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
|
||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 0, 0, size, b) })
|
||||
b.Run(" 00|X0 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 0, 1, size, b) })
|
||||
b.Run(" X0|00 ", func(b *testing.B) { benchmarkErasureWrite(2, 2, 1, 0, size, b) })
|
||||
b.Run(" 00|00 ", func(b *testing.B) { benchmarkErasureEncode(2, 2, 0, 0, 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) { 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
|
||||
b.Run(" 0000|0000 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 0, 0, size, b) })
|
||||
b.Run(" 0000|X000 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 0, 1, size, b) })
|
||||
b.Run(" X000|0000 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 1, 0, size, b) })
|
||||
b.Run(" 0000|XXX0 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 0, 3, size, b) })
|
||||
b.Run(" XXX0|0000 ", func(b *testing.B) { benchmarkErasureWrite(4, 4, 3, 0, size, b) })
|
||||
b.Run(" 0000|0000 ", func(b *testing.B) { benchmarkErasureEncode(4, 4, 0, 0, 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) { benchmarkErasureEncode(4, 4, 1, 0, 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) { 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
|
||||
b.Run(" 000000|000000 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 0, 0, size, b) })
|
||||
b.Run(" 000000|X00000 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 0, 1, size, b) })
|
||||
b.Run(" X00000|000000 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 1, 0, size, b) })
|
||||
b.Run(" 000000|XXXXX0 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 0, 5, size, b) })
|
||||
b.Run(" XXXXX0|000000 ", func(b *testing.B) { benchmarkErasureWrite(6, 6, 5, 0, size, b) })
|
||||
b.Run(" 000000|000000 ", func(b *testing.B) { benchmarkErasureEncode(6, 6, 0, 0, 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) { benchmarkErasureEncode(6, 6, 1, 0, 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) { 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
|
||||
b.Run(" 00000000|00000000 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 0, 0, size, b) })
|
||||
b.Run(" 00000000|X0000000 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 0, 1, size, b) })
|
||||
b.Run(" X0000000|00000000 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 1, 0, size, b) })
|
||||
b.Run(" 00000000|XXXXXXX0 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 0, 7, size, b) })
|
||||
b.Run(" XXXXXXX0|00000000 ", func(b *testing.B) { benchmarkErasureWrite(8, 8, 7, 0, size, b) })
|
||||
b.Run(" 00000000|00000000 ", func(b *testing.B) { benchmarkErasureEncode(8, 8, 0, 0, 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) { benchmarkErasureEncode(8, 8, 1, 0, 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) { benchmarkErasureEncode(8, 8, 7, 0, size, b) })
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
// 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.
|
||||
func (e Erasure) Heal(ctx context.Context, readers []*bitrotReader, writers []*bitrotWriter, size int64) error {
|
||||
r, w := io.Pipe()
|
||||
go func() {
|
||||
if err := e.Decode(ctx, w, readers, 0, size, size); err != nil {
|
||||
w.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
w.Close()
|
||||
}()
|
||||
buf := make([]byte, e.blockSize)
|
||||
// quorum is 1 because CreateFile should continue writing as long as we are writing to even 1 disk.
|
||||
n, err := e.Encode(ctx, r, writers, buf, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != size {
|
||||
logger.LogIf(ctx, errLessData)
|
||||
return errLessData
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -21,11 +21,10 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var erasureHealFileTests = []struct {
|
||||
var erasureHealTests = []struct {
|
||||
dataBlocks, disks int
|
||||
|
||||
// number of offline disks is also number of staleDisks for
|
||||
@@ -56,15 +55,13 @@ var erasureHealFileTests = []struct {
|
||||
{dataBlocks: 7, disks: 14, offDisks: 6, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 14
|
||||
{dataBlocks: 8, disks: 16, offDisks: 4, badDisks: 5, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 15
|
||||
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 16
|
||||
{dataBlocks: 2, disks: 4, offDisks: 0, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: 0, shouldFail: true}, // 17
|
||||
{dataBlocks: 12, disks: 16, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 18
|
||||
{dataBlocks: 6, disks: 8, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 19
|
||||
{dataBlocks: 7, disks: 10, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: 0, shouldFail: true}, // 20
|
||||
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte * 64, algorithm: SHA256, shouldFail: false}, // 21
|
||||
{dataBlocks: 12, disks: 16, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 17
|
||||
{dataBlocks: 6, disks: 8, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 18
|
||||
{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) {
|
||||
for i, test := range erasureHealFileTests {
|
||||
func TestErasureHeal(t *testing.T) {
|
||||
for i, test := range erasureHealTests {
|
||||
if test.offDisks < test.badStaleDisks {
|
||||
// test case sanity check
|
||||
t.Fatalf("Test %d: Bad test case - number of stale disks cannot be less than number of badstale disks", i)
|
||||
@@ -75,7 +72,8 @@ func TestErasureHealFile(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to setup XL environment: %v", i, err)
|
||||
}
|
||||
storage, err := NewErasureStorage(context.Background(), setup.disks, test.dataBlocks, test.disks-test.dataBlocks, test.blocksize)
|
||||
disks := setup.disks
|
||||
erasure, err := NewErasure(context.Background(), test.dataBlocks, test.disks-test.dataBlocks, test.blocksize)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||
@@ -85,36 +83,50 @@ func TestErasureHealFile(t *testing.T) {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
||||
}
|
||||
algorithm := test.algorithm
|
||||
if !algorithm.Available() {
|
||||
algorithm = DefaultBitrotAlgorithm
|
||||
}
|
||||
buffer := make([]byte, test.blocksize, 2*test.blocksize)
|
||||
file, err := storage.CreateFile(context.Background(), bytes.NewReader(data), "testbucket", "testobject", buffer, algorithm, test.dataBlocks+1)
|
||||
writers := make([]*bitrotWriter, len(disks))
|
||||
for i, disk := range disks {
|
||||
writers[i] = newBitrotWriter(disk, "testbucket", "testobject", test.algorithm)
|
||||
}
|
||||
_, err = erasure.Encode(context.Background(), bytes.NewReader(data), writers, buffer, erasure.dataBlocks+1)
|
||||
if err != nil {
|
||||
setup.Remove()
|
||||
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
||||
}
|
||||
|
||||
readers := make([]*bitrotReader, len(disks))
|
||||
for i, disk := range disks {
|
||||
shardFilesize := getErasureShardFileSize(test.blocksize, test.size, erasure.dataBlocks)
|
||||
readers[i] = newBitrotReader(disk, "testbucket", "testobject", test.algorithm, shardFilesize, writers[i].Sum())
|
||||
}
|
||||
|
||||
// setup stale disks for the test case
|
||||
staleDisks := make([]StorageAPI, len(storage.disks))
|
||||
copy(staleDisks, storage.disks)
|
||||
for j := 0; j < len(storage.disks); j++ {
|
||||
staleDisks := make([]StorageAPI, len(disks))
|
||||
copy(staleDisks, disks)
|
||||
for j := 0; j < len(staleDisks); j++ {
|
||||
if j < test.offDisks {
|
||||
storage.disks[j] = OfflineDisk
|
||||
readers[j] = nil
|
||||
} else {
|
||||
staleDisks[j] = nil
|
||||
}
|
||||
}
|
||||
for j := 0; j < test.badDisks; j++ {
|
||||
storage.disks[test.offDisks+j] = badDisk{nil}
|
||||
readers[test.offDisks+j].disk = badDisk{nil}
|
||||
}
|
||||
for j := 0; j < test.badStaleDisks; j++ {
|
||||
staleDisks[j] = badDisk{nil}
|
||||
}
|
||||
|
||||
staleWriters := make([]*bitrotWriter, len(staleDisks))
|
||||
for i, disk := range staleDisks {
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
staleWriters[i] = newBitrotWriter(disk, "testbucket", "testobject", test.algorithm)
|
||||
}
|
||||
|
||||
// test case setup is complete - now call Healfile()
|
||||
info, err := storage.HealFile(context.Background(), staleDisks, "testbucket", "testobject", test.blocksize, "testbucket", "healedobject", test.size, test.algorithm, file.Checksums)
|
||||
err = erasure.Heal(context.Background(), readers, staleWriters, test.size)
|
||||
if err != nil && !test.shouldFail {
|
||||
t.Errorf("Test %d: should pass but it failed with: %v", i, err)
|
||||
}
|
||||
@@ -122,19 +134,13 @@ func TestErasureHealFile(t *testing.T) {
|
||||
t.Errorf("Test %d: should fail but it passed", i)
|
||||
}
|
||||
if err == nil {
|
||||
if info.Size != test.size {
|
||||
t.Errorf("Test %d: healed wrong number of bytes: got: #%d want: #%d", i, info.Size, test.size)
|
||||
}
|
||||
if info.Algorithm != test.algorithm {
|
||||
t.Errorf("Test %d: healed with wrong algorithm: got: %v want: %v", i, info.Algorithm, test.algorithm)
|
||||
}
|
||||
// Verify that checksums of staleDisks
|
||||
// match expected values
|
||||
for i, disk := range staleDisks {
|
||||
if disk == nil || info.Checksums[i] == nil {
|
||||
for i := range staleWriters {
|
||||
if staleWriters[i] == nil {
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(info.Checksums[i], file.Checksums[i]) {
|
||||
if !bytes.Equal(staleWriters[i].Sum(), writers[i].Sum()) {
|
||||
t.Errorf("Test %d: heal returned different bitrot checksums", i)
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
// HealFile tries to reconstruct an erasure-coded file spread over all
|
||||
// available disks. HealFile will read the valid parts of the file,
|
||||
// reconstruct the missing data and write the reconstructed parts back
|
||||
// to `staleDisks` at the destination `dstVol/dstPath/`. Parts are
|
||||
// verified against the given BitrotAlgorithm and checksums.
|
||||
//
|
||||
// `staleDisks` is a slice of disks where each non-nil entry has stale
|
||||
// or no data, and so will be healed.
|
||||
//
|
||||
// It is required that `s.disks` have a (read-quorum) majority of
|
||||
// disks with valid data for healing to work.
|
||||
//
|
||||
// In addition, `staleDisks` and `s.disks` must have the same ordering
|
||||
// of disks w.r.t. erasure coding of the object.
|
||||
//
|
||||
// Errors when writing to `staleDisks` are not propagated as long as
|
||||
// writes succeed for at least one disk. This allows partial healing
|
||||
// despite stale disks being faulty.
|
||||
//
|
||||
// It returns bitrot checksums for the non-nil staleDisks on which
|
||||
// healing succeeded.
|
||||
func (s ErasureStorage) HealFile(ctx context.Context, staleDisks []StorageAPI, volume, path string, blocksize int64,
|
||||
dstVol, dstPath string, size int64, alg BitrotAlgorithm, checksums [][]byte) (
|
||||
f ErasureFileInfo, err error) {
|
||||
|
||||
if !alg.Available() {
|
||||
logger.LogIf(ctx, errBitrotHashAlgoInvalid)
|
||||
return f, errBitrotHashAlgoInvalid
|
||||
}
|
||||
|
||||
// Initialization
|
||||
f.Checksums = make([][]byte, len(s.disks))
|
||||
hashers := make([]hash.Hash, len(s.disks))
|
||||
verifiers := make([]*BitrotVerifier, len(s.disks))
|
||||
for i, disk := range s.disks {
|
||||
switch {
|
||||
case staleDisks[i] != nil:
|
||||
hashers[i] = alg.New()
|
||||
case disk == nil:
|
||||
// disregard unavailable disk
|
||||
continue
|
||||
default:
|
||||
verifiers[i] = NewBitrotVerifier(alg, checksums[i])
|
||||
}
|
||||
}
|
||||
writeErrors := make([]error, len(s.disks))
|
||||
|
||||
// Read part file data on each disk
|
||||
chunksize := ceilFrac(blocksize, int64(s.dataBlocks))
|
||||
numBlocks := ceilFrac(size, blocksize)
|
||||
|
||||
readLen := chunksize * (numBlocks - 1)
|
||||
|
||||
lastChunkSize := chunksize
|
||||
hasSmallerLastBlock := size%blocksize != 0
|
||||
if hasSmallerLastBlock {
|
||||
lastBlockLen := size % blocksize
|
||||
lastChunkSize = ceilFrac(lastBlockLen, int64(s.dataBlocks))
|
||||
}
|
||||
readLen += lastChunkSize
|
||||
var buffers [][]byte
|
||||
buffers, _, err = s.readConcurrent(ctx, volume, path, 0, readLen, verifiers)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
|
||||
// Scan part files on disk, block-by-block reconstruct it and
|
||||
// write to stale disks.
|
||||
blocks := make([][]byte, len(s.disks))
|
||||
|
||||
if numBlocks > 1 {
|
||||
// Allocate once for all the equal length blocks. The
|
||||
// last block may have a different length - allocation
|
||||
// for this happens inside the for loop below.
|
||||
for i := range blocks {
|
||||
if len(buffers[i]) == 0 {
|
||||
blocks[i] = make([]byte, chunksize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var buffOffset int64
|
||||
for blockNumber := int64(0); blockNumber < numBlocks; blockNumber++ {
|
||||
if blockNumber == numBlocks-1 && lastChunkSize != chunksize {
|
||||
for i := range blocks {
|
||||
if len(buffers[i]) == 0 {
|
||||
blocks[i] = make([]byte, lastChunkSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range blocks {
|
||||
if len(buffers[i]) == 0 {
|
||||
blocks[i] = blocks[i][0:0]
|
||||
}
|
||||
}
|
||||
|
||||
csize := chunksize
|
||||
if blockNumber == numBlocks-1 {
|
||||
csize = lastChunkSize
|
||||
}
|
||||
for i := range blocks {
|
||||
if len(buffers[i]) != 0 {
|
||||
blocks[i] = buffers[i][buffOffset : buffOffset+csize]
|
||||
}
|
||||
}
|
||||
buffOffset += csize
|
||||
|
||||
if err = s.ErasureDecodeDataAndParityBlocks(ctx, blocks); err != nil {
|
||||
return f, err
|
||||
}
|
||||
|
||||
// write computed shards as chunks on file in each
|
||||
// stale disk
|
||||
writeSucceeded := false
|
||||
for i, disk := range staleDisks {
|
||||
// skip nil disk or disk that had error on
|
||||
// previous write
|
||||
if disk == nil || writeErrors[i] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
writeErrors[i] = disk.AppendFile(dstVol, dstPath, blocks[i])
|
||||
if writeErrors[i] == nil {
|
||||
hashers[i].Write(blocks[i])
|
||||
writeSucceeded = true
|
||||
}
|
||||
}
|
||||
|
||||
// If all disks had write errors we quit.
|
||||
if !writeSucceeded {
|
||||
// build error from all write errors
|
||||
err := joinWriteErrors(writeErrors)
|
||||
logger.LogIf(ctx, err)
|
||||
return f, err
|
||||
}
|
||||
}
|
||||
|
||||
// copy computed file hashes into output variable
|
||||
f.Size = size
|
||||
f.Algorithm = alg
|
||||
for i, disk := range staleDisks {
|
||||
if disk == nil || writeErrors[i] != nil {
|
||||
continue
|
||||
}
|
||||
f.Checksums[i] = hashers[i].Sum(nil)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func joinWriteErrors(errs []error) error {
|
||||
msgs := []string{}
|
||||
for i, err := range errs {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
msgs = append(msgs, fmt.Sprintf("disk %d: %v", i+1, err))
|
||||
}
|
||||
return fmt.Errorf("all stale disks had write errors during healing: %s",
|
||||
strings.Join(msgs, ", "))
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
type errIdx struct {
|
||||
idx int
|
||||
err error
|
||||
}
|
||||
|
||||
func (s ErasureStorage) readConcurrent(ctx context.Context, volume, path string, offset, length int64,
|
||||
verifiers []*BitrotVerifier) (buffers [][]byte, needsReconstruction bool,
|
||||
err error) {
|
||||
|
||||
errChan := make(chan errIdx)
|
||||
stageBuffers := make([][]byte, len(s.disks))
|
||||
buffers = make([][]byte, len(s.disks))
|
||||
|
||||
readDisk := func(i int) {
|
||||
stageBuffers[i] = make([]byte, length)
|
||||
disk := s.disks[i]
|
||||
if disk == OfflineDisk {
|
||||
logger.LogIf(ctx, errDiskNotFound)
|
||||
errChan <- errIdx{i, errDiskNotFound}
|
||||
return
|
||||
}
|
||||
_, rerr := disk.ReadFile(volume, path, offset, stageBuffers[i], verifiers[i])
|
||||
errChan <- errIdx{i, rerr}
|
||||
}
|
||||
|
||||
var finishedCount, successCount, launchIndex int
|
||||
|
||||
for ; launchIndex < s.dataBlocks; launchIndex++ {
|
||||
go readDisk(launchIndex)
|
||||
}
|
||||
for finishedCount < launchIndex {
|
||||
select {
|
||||
case errVal := <-errChan:
|
||||
finishedCount++
|
||||
if errVal.err != nil {
|
||||
// TODO: meaningfully log the disk read error
|
||||
|
||||
// A disk failed to return data, so we
|
||||
// request an additional disk if possible
|
||||
if launchIndex < s.dataBlocks+s.parityBlocks {
|
||||
needsReconstruction = true
|
||||
// requiredBlocks++
|
||||
go readDisk(launchIndex)
|
||||
launchIndex++
|
||||
}
|
||||
} else {
|
||||
successCount++
|
||||
buffers[errVal.idx] = stageBuffers[errVal.idx]
|
||||
stageBuffers[errVal.idx] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if successCount != s.dataBlocks {
|
||||
// Not enough disks returns data.
|
||||
err = errXLReadQuorum
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ReadFile reads as much data as requested from the file under the
|
||||
// given volume and path and writes the data to the provided writer.
|
||||
// The algorithm and the keys/checksums are used to verify the
|
||||
// integrity of the given file. ReadFile will read data from the given
|
||||
// offset up to the given length. If parts of the file are corrupted
|
||||
// ReadFile tries to reconstruct the data.
|
||||
func (s ErasureStorage) ReadFile(ctx context.Context, writer io.Writer, volume, path string, offset,
|
||||
length, totalLength int64, checksums [][]byte, algorithm BitrotAlgorithm,
|
||||
blocksize int64) (f ErasureFileInfo, err error) {
|
||||
|
||||
if offset < 0 || length < 0 {
|
||||
logger.LogIf(ctx, errUnexpected)
|
||||
return f, errUnexpected
|
||||
}
|
||||
if offset+length > totalLength {
|
||||
logger.LogIf(ctx, errUnexpected)
|
||||
return f, errUnexpected
|
||||
}
|
||||
if !algorithm.Available() {
|
||||
logger.LogIf(ctx, errBitrotHashAlgoInvalid)
|
||||
return f, errBitrotHashAlgoInvalid
|
||||
}
|
||||
|
||||
f.Checksums = make([][]byte, len(s.disks))
|
||||
verifiers := make([]*BitrotVerifier, len(s.disks))
|
||||
for i, disk := range s.disks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
verifiers[i] = NewBitrotVerifier(algorithm, checksums[i])
|
||||
}
|
||||
|
||||
chunksize := ceilFrac(blocksize, int64(s.dataBlocks))
|
||||
|
||||
// We read all whole-blocks of erasure coded data containing
|
||||
// the requested data range.
|
||||
//
|
||||
// The start index of the erasure coded block containing the
|
||||
// `offset` byte of data is:
|
||||
partDataStartIndex := (offset / blocksize) * chunksize
|
||||
// The start index of the erasure coded block containing the
|
||||
// (last) byte of data at the index `offset + length - 1` is:
|
||||
blockStartIndex := ((offset + length - 1) / blocksize) * chunksize
|
||||
// However, we need the end index of the e.c. block containing
|
||||
// the last byte - we need to check if that block is the last
|
||||
// block in the part (in that case, it may be have a different
|
||||
// chunk size)
|
||||
isLastBlock := (totalLength-1)/blocksize == (offset+length-1)/blocksize
|
||||
var partDataEndIndex int64
|
||||
if isLastBlock {
|
||||
lastBlockChunkSize := chunksize
|
||||
if totalLength%blocksize != 0 {
|
||||
lastBlockChunkSize = ceilFrac(totalLength%blocksize, int64(s.dataBlocks))
|
||||
}
|
||||
partDataEndIndex = blockStartIndex + lastBlockChunkSize - 1
|
||||
} else {
|
||||
partDataEndIndex = blockStartIndex + chunksize - 1
|
||||
}
|
||||
|
||||
// Thus, the length of data to be read from the part file(s) is:
|
||||
partDataLength := partDataEndIndex - partDataStartIndex + 1
|
||||
// The calculation above does not apply when length == 0:
|
||||
if length == 0 {
|
||||
partDataLength = 0
|
||||
}
|
||||
|
||||
var buffers [][]byte
|
||||
var needsReconstruction bool
|
||||
buffers, needsReconstruction, err = s.readConcurrent(ctx, volume, path,
|
||||
partDataStartIndex, partDataLength, verifiers)
|
||||
if err != nil {
|
||||
// Could not read enough disks.
|
||||
return
|
||||
}
|
||||
|
||||
numChunks := ceilFrac(partDataLength, chunksize)
|
||||
blocks := make([][]byte, len(s.disks))
|
||||
|
||||
if needsReconstruction && numChunks > 1 {
|
||||
// Allocate once for all the equal length blocks. The
|
||||
// last block may have a different length - allocation
|
||||
// for this happens inside the for loop below.
|
||||
for i := range blocks {
|
||||
if len(buffers[i]) == 0 {
|
||||
blocks[i] = make([]byte, chunksize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var buffOffset int64
|
||||
for chunkNumber := int64(0); chunkNumber < numChunks; chunkNumber++ {
|
||||
if chunkNumber == numChunks-1 && partDataLength%chunksize != 0 {
|
||||
chunksize = partDataLength % chunksize
|
||||
// We allocate again as the last chunk has a
|
||||
// different size.
|
||||
for i := range blocks {
|
||||
if len(buffers[i]) == 0 {
|
||||
blocks[i] = make([]byte, chunksize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range blocks {
|
||||
if len(buffers[i]) == 0 {
|
||||
blocks[i] = blocks[i][0:0]
|
||||
}
|
||||
}
|
||||
|
||||
for i := range blocks {
|
||||
if len(buffers[i]) != 0 {
|
||||
blocks[i] = buffers[i][buffOffset : buffOffset+chunksize]
|
||||
}
|
||||
}
|
||||
buffOffset += chunksize
|
||||
|
||||
if needsReconstruction {
|
||||
if err = s.ErasureDecodeDataBlocks(blocks); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return f, err
|
||||
}
|
||||
}
|
||||
|
||||
var writeStart int64
|
||||
if chunkNumber == 0 {
|
||||
writeStart = offset % blocksize
|
||||
}
|
||||
|
||||
writeLength := blocksize - writeStart
|
||||
if chunkNumber == numChunks-1 {
|
||||
lastBlockLength := (offset + length) % blocksize
|
||||
if lastBlockLength != 0 {
|
||||
writeLength = lastBlockLength - writeStart
|
||||
}
|
||||
}
|
||||
n, err := writeDataBlocks(ctx, writer, blocks, s.dataBlocks, writeStart, writeLength)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
|
||||
f.Size += n
|
||||
}
|
||||
|
||||
f.Algorithm = algorithm
|
||||
for i, disk := range s.disks {
|
||||
if disk == OfflineDisk || buffers[i] == nil {
|
||||
continue
|
||||
}
|
||||
f.Checksums[i] = verifiers[i].Sum(nil)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
@@ -104,3 +104,25 @@ func writeDataBlocks(ctx context.Context, dst io.Writer, enBlocks [][]byte, data
|
||||
// Success.
|
||||
return totalWritten, nil
|
||||
}
|
||||
|
||||
// Returns shard-file size.
|
||||
func getErasureShardFileSize(blockSize int64, totalLength int64, dataBlocks int) int64 {
|
||||
shardSize := ceilFrac(int64(blockSize), int64(dataBlocks))
|
||||
numShards := totalLength / int64(blockSize)
|
||||
lastBlockSize := totalLength % int64(blockSize)
|
||||
lastShardSize := ceilFrac(lastBlockSize, int64(dataBlocks))
|
||||
return shardSize*numShards + lastShardSize
|
||||
}
|
||||
|
||||
// Returns the endOffset till which bitrotReader should read data using disk.ReadFile()
|
||||
// partOffset, partLength and partSize are values of the object's part file.
|
||||
func getErasureShardFileEndOffset(partOffset int64, partLength int64, partSize int64, erasureBlockSize int64, dataBlocks int) int64 {
|
||||
shardSize := ceilFrac(erasureBlockSize, int64(dataBlocks))
|
||||
shardFileSize := getErasureShardFileSize(erasureBlockSize, partSize, dataBlocks)
|
||||
endShard := (partOffset + int64(partLength)) / erasureBlockSize
|
||||
endOffset := endShard*shardSize + shardSize
|
||||
if endOffset > shardFileSize {
|
||||
endOffset = shardFileSize
|
||||
}
|
||||
return endOffset
|
||||
}
|
||||
|
||||
+34
-60
@@ -18,105 +18,79 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"hash"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
// OfflineDisk represents an unavailable disk.
|
||||
var OfflineDisk StorageAPI // zero value is nil
|
||||
|
||||
// ErasureFileInfo contains information about an erasure file operation (create, read, heal).
|
||||
type ErasureFileInfo struct {
|
||||
Size int64
|
||||
Algorithm BitrotAlgorithm
|
||||
Checksums [][]byte
|
||||
}
|
||||
|
||||
// ErasureStorage represents an array of disks.
|
||||
// The disks contain erasure coded and bitrot-protected data.
|
||||
type ErasureStorage struct {
|
||||
disks []StorageAPI
|
||||
erasure reedsolomon.Encoder
|
||||
// Erasure - erasure encoding details.
|
||||
type Erasure struct {
|
||||
encoder reedsolomon.Encoder
|
||||
dataBlocks, parityBlocks int
|
||||
blockSize int64
|
||||
}
|
||||
|
||||
// NewErasureStorage creates a new ErasureStorage. The storage erasure codes and protects all data written to
|
||||
// the disks.
|
||||
func NewErasureStorage(ctx context.Context, disks []StorageAPI, dataBlocks, parityBlocks int, blockSize int64) (s ErasureStorage, err error) {
|
||||
shardsize := (int(blockSize) + dataBlocks - 1) / dataBlocks
|
||||
// NewErasure creates a new ErasureStorage.
|
||||
func NewErasure(ctx context.Context, dataBlocks, parityBlocks int, blockSize int64) (e Erasure, err error) {
|
||||
shardsize := int(ceilFrac(blockSize, int64(dataBlocks)))
|
||||
erasure, err := reedsolomon.New(dataBlocks, parityBlocks, reedsolomon.WithAutoGoroutines(shardsize))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return s, err
|
||||
return e, err
|
||||
}
|
||||
s = ErasureStorage{
|
||||
disks: make([]StorageAPI, len(disks)),
|
||||
erasure: erasure,
|
||||
e = Erasure{
|
||||
encoder: erasure,
|
||||
dataBlocks: dataBlocks,
|
||||
parityBlocks: parityBlocks,
|
||||
blockSize: blockSize,
|
||||
}
|
||||
copy(s.disks, disks)
|
||||
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.
|
||||
func (s *ErasureStorage) ErasureEncode(ctx context.Context, data []byte) ([][]byte, error) {
|
||||
encoded, err := s.erasure.Split(data)
|
||||
func (e *Erasure) EncodeData(ctx context.Context, data []byte) ([][]byte, error) {
|
||||
if len(data) == 0 {
|
||||
return make([][]byte, e.dataBlocks+e.parityBlocks), nil
|
||||
}
|
||||
encoded, err := e.encoder.Split(data)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
if err = s.erasure.Encode(encoded); err != nil {
|
||||
if err = e.encoder.Encode(encoded); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
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 returns an error if the decoding failed.
|
||||
func (s *ErasureStorage) ErasureDecodeDataBlocks(data [][]byte) error {
|
||||
if err := s.erasure.ReconstructData(data); err != nil {
|
||||
func (e *Erasure) DecodeDataBlocks(data [][]byte) error {
|
||||
needsReconstruction := false
|
||||
for _, b := range data[:e.dataBlocks] {
|
||||
if b == nil {
|
||||
needsReconstruction = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !needsReconstruction {
|
||||
return nil
|
||||
}
|
||||
if err := e.encoder.ReconstructData(data); err != nil {
|
||||
return err
|
||||
}
|
||||
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.
|
||||
func (s *ErasureStorage) ErasureDecodeDataAndParityBlocks(ctx context.Context, data [][]byte) error {
|
||||
if err := s.erasure.Reconstruct(data); err != nil {
|
||||
func (e *Erasure) DecodeDataAndParityBlocks(ctx context.Context, data [][]byte) error {
|
||||
if err := e.encoder.Reconstruct(data); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewBitrotVerifier returns a new BitrotVerifier implementing the given algorithm.
|
||||
func NewBitrotVerifier(algorithm BitrotAlgorithm, checksum []byte) *BitrotVerifier {
|
||||
return &BitrotVerifier{algorithm.New(), algorithm, checksum, false}
|
||||
}
|
||||
|
||||
// BitrotVerifier can be used to verify protected data.
|
||||
type BitrotVerifier struct {
|
||||
hash.Hash
|
||||
|
||||
algorithm BitrotAlgorithm
|
||||
sum []byte
|
||||
verified bool
|
||||
}
|
||||
|
||||
// Verify returns true iff the computed checksum of the verifier matches the the checksum provided when the verifier
|
||||
// was created.
|
||||
func (v *BitrotVerifier) Verify() bool {
|
||||
v.verified = true
|
||||
return subtle.ConstantTimeCompare(v.Sum(nil), v.sum) == 1
|
||||
}
|
||||
|
||||
// IsVerified returns true iff Verify was called at least once.
|
||||
func (v *BitrotVerifier) IsVerified() bool { return v.verified }
|
||||
|
||||
+8
-9
@@ -25,7 +25,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var erasureDecodeTests = []struct {
|
||||
var erasureEncodeDecodeTests = []struct {
|
||||
dataBlocks, parityBlocks int
|
||||
missingData, missingParity int
|
||||
reconstructParity bool
|
||||
@@ -43,21 +43,20 @@ var erasureDecodeTests = []struct {
|
||||
{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)
|
||||
if _, err := io.ReadFull(rand.Reader, data); err != nil {
|
||||
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))
|
||||
copy(buffer, data)
|
||||
|
||||
disks := make([]StorageAPI, test.dataBlocks+test.parityBlocks)
|
||||
storage, err := NewErasureStorage(context.Background(), disks, test.dataBlocks, test.parityBlocks, blockSizeV1)
|
||||
erasure, err := NewErasure(context.Background(), test.dataBlocks, test.parityBlocks, blockSizeV1)
|
||||
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 {
|
||||
t.Fatalf("Test %d: failed to encode data: %v", i, err)
|
||||
}
|
||||
@@ -70,9 +69,9 @@ func TestErasureDecode(t *testing.T) {
|
||||
}
|
||||
|
||||
if test.reconstructParity {
|
||||
err = storage.ErasureDecodeDataAndParityBlocks(context.Background(), encoded)
|
||||
err = erasure.DecodeDataAndParityBlocks(context.Background(), encoded)
|
||||
} else {
|
||||
err = storage.ErasureDecodeDataBlocks(encoded)
|
||||
err = erasure.DecodeDataBlocks(encoded)
|
||||
}
|
||||
|
||||
if err == nil && test.shouldFail {
|
||||
|
||||
+3
-3
@@ -21,7 +21,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
@@ -267,10 +266,11 @@ func formatXLMigrateV2ToV3(export string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.RemoveAll(pathJoin(export, minioMetaMultipartBucket)); err != nil {
|
||||
if err = removeAll(pathJoin(export, minioMetaMultipartBucket)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.MkdirAll(pathJoin(export, minioMetaMultipartBucket), 0755); err != nil {
|
||||
|
||||
if err = mkdirAll(pathJoin(export, minioMetaMultipartBucket), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+12
-8
@@ -64,7 +64,7 @@ func fsRemoveAll(ctx context.Context, dirPath string) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.RemoveAll(dirPath); err != nil {
|
||||
if err = removeAll(dirPath); err != nil {
|
||||
if os.IsPermission(err) {
|
||||
logger.LogIf(ctx, errVolumeAccessDenied)
|
||||
return errVolumeAccessDenied
|
||||
@@ -121,23 +121,25 @@ func fsMkdir(ctx context.Context, dirPath string) (err error) {
|
||||
}
|
||||
|
||||
if err = os.Mkdir((dirPath), 0777); err != nil {
|
||||
if os.IsExist(err) {
|
||||
switch {
|
||||
case os.IsExist(err):
|
||||
return errVolumeExists
|
||||
} else if os.IsPermission(err) {
|
||||
case os.IsPermission(err):
|
||||
logger.LogIf(ctx, errDiskAccessDenied)
|
||||
return errDiskAccessDenied
|
||||
} else if isSysErrNotDir(err) {
|
||||
case isSysErrNotDir(err):
|
||||
// File path cannot be verified since
|
||||
// one of the parents is a file.
|
||||
logger.LogIf(ctx, errDiskAccessDenied)
|
||||
return errDiskAccessDenied
|
||||
} else if isSysErrPathNotFound(err) {
|
||||
case isSysErrPathNotFound(err):
|
||||
// Add specific case for windows.
|
||||
logger.LogIf(ctx, errDiskAccessDenied)
|
||||
return errDiskAccessDenied
|
||||
default:
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -343,7 +345,9 @@ func fsCreateFile(ctx context.Context, filePath string, reader io.Reader, buf []
|
||||
if buf != nil {
|
||||
bytesWritten, err = io.CopyBuffer(writer, reader, buf)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
if err != io.ErrUnexpectedEOF {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestReadFSMetadata(t *testing.T) {
|
||||
if err := obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil {
|
||||
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, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil, ObjectOptions{}); err != nil {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func TestWriteFSMetadata(t *testing.T) {
|
||||
if err := obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil {
|
||||
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, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil, ObjectOptions{}); err != nil {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ func (fs *FSObjects) ListMultipartUploads(ctx context.Context, bucket, object, k
|
||||
// subsequent request each UUID is unique.
|
||||
//
|
||||
// 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 {
|
||||
return "", toObjectErr(err, bucket)
|
||||
}
|
||||
@@ -249,7 +249,7 @@ func (fs *FSObjects) NewMultipartUpload(ctx context.Context, bucket, object stri
|
||||
// object. Internally incoming data is written to '.minio.sys/tmp' location
|
||||
// and safely renamed to '.minio.sys/multipart' for reach parts.
|
||||
func (fs *FSObjects) CopyObjectPart(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 {
|
||||
return pi, toObjectErr(err)
|
||||
@@ -257,7 +257,7 @@ func (fs *FSObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, d
|
||||
|
||||
// Initialize pipe.
|
||||
go func() {
|
||||
if gerr := fs.GetObject(ctx, srcBucket, srcObject, startOffset, length, srcInfo.Writer, srcInfo.ETag); gerr != nil {
|
||||
if gerr := fs.GetObject(ctx, srcBucket, srcObject, startOffset, length, srcInfo.Writer, srcInfo.ETag, srcOpts); gerr != nil {
|
||||
if gerr = srcInfo.Writer.Close(); gerr != nil {
|
||||
logger.LogIf(ctx, gerr)
|
||||
return
|
||||
@@ -271,7 +271,7 @@ func (fs *FSObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, d
|
||||
}
|
||||
}()
|
||||
|
||||
partInfo, err := fs.PutObjectPart(ctx, dstBucket, dstObject, uploadID, partID, srcInfo.Reader)
|
||||
partInfo, err := fs.PutObjectPart(ctx, dstBucket, dstObject, uploadID, partID, srcInfo.Reader, dstOpts)
|
||||
if err != nil {
|
||||
return pi, toObjectErr(err, dstBucket, dstObject)
|
||||
}
|
||||
@@ -283,7 +283,7 @@ func (fs *FSObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, d
|
||||
// an ongoing multipart transaction. Internally incoming data is
|
||||
// written to '.minio.sys/tmp' location and safely renamed to
|
||||
// '.minio.sys/multipart' for reach parts.
|
||||
func (fs *FSObjects) PutObjectPart(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, data *hash.Reader, opts ObjectOptions) (pi PartInfo, e error) {
|
||||
if err := checkPutObjectPartArgs(ctx, bucket, object, fs); err != nil {
|
||||
return pi, toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
+12
-17
@@ -42,7 +42,7 @@ func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
|
||||
objectName := "object"
|
||||
|
||||
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 {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func TestNewMultipartUploadFaultyDisk(t *testing.T) {
|
||||
|
||||
// Test with disk removed.
|
||||
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{}) {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
@@ -90,27 +90,22 @@ func TestNewMultipartUploadFaultyDisk(t *testing.T) {
|
||||
|
||||
// TestPutObjectPartFaultyDisk - test PutObjectPart with faulty disks
|
||||
func TestPutObjectPartFaultyDisk(t *testing.T) {
|
||||
root, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer os.RemoveAll(disk)
|
||||
obj := initFSObjects(disk, t)
|
||||
|
||||
fs := obj.(*FSObjects)
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
data := []byte("12345")
|
||||
dataLen := int64(len(data))
|
||||
|
||||
if err = obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil {
|
||||
if err := obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil {
|
||||
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 {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
@@ -119,7 +114,7 @@ func TestPutObjectPartFaultyDisk(t *testing.T) {
|
||||
sha256sum := ""
|
||||
|
||||
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, mustGetHashReader(t, bytes.NewReader(data), dataLen, md5Hex, sha256sum), ObjectOptions{})
|
||||
if !isSameType(err, BucketNotFound{}) {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
@@ -141,7 +136,7 @@ func TestCompleteMultipartUploadFaultyDisk(t *testing.T) {
|
||||
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 {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
@@ -173,14 +168,14 @@ func TestCompleteMultipartUpload(t *testing.T) {
|
||||
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 {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
|
||||
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, mustGetHashReader(t, bytes.NewReader(data), 5, md5Hex, ""), ObjectOptions{}); err != nil {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
|
||||
@@ -207,14 +202,14 @@ func TestAbortMultipartUpload(t *testing.T) {
|
||||
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 {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
|
||||
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, mustGetHashReader(t, bytes.NewReader(data), 5, md5Hex, ""), ObjectOptions{}); err != nil {
|
||||
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."
|
||||
@@ -239,7 +234,7 @@ func TestListMultipartUploadsFaultyDisk(t *testing.T) {
|
||||
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 {
|
||||
t.Fatal("Unexpected error ", err)
|
||||
}
|
||||
|
||||
+20
-14
@@ -93,18 +93,20 @@ func (fsi *fsIOPool) Open(path string) (*lock.RLockedFile, error) {
|
||||
// Open file for reading with read lock.
|
||||
newRlkFile, err := lock.RLockedOpenFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
return nil, errFileNotFound
|
||||
} else if os.IsPermission(err) {
|
||||
case os.IsPermission(err):
|
||||
return nil, errFileAccessDenied
|
||||
} else if isSysErrIsDir(err) {
|
||||
case isSysErrIsDir(err):
|
||||
return nil, errIsNotRegular
|
||||
} else if isSysErrNotDir(err) {
|
||||
case isSysErrNotDir(err):
|
||||
return nil, errFileAccessDenied
|
||||
} else if isSysErrPathNotFound(err) {
|
||||
case isSysErrPathNotFound(err):
|
||||
return nil, errFileNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
/// Save new reader on the map.
|
||||
@@ -148,14 +150,16 @@ func (fsi *fsIOPool) Write(path string) (wlk *lock.LockedFile, err error) {
|
||||
|
||||
wlk, err = lock.LockedOpenFile(path, os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
return nil, errFileNotFound
|
||||
} else if os.IsPermission(err) {
|
||||
case os.IsPermission(err):
|
||||
return nil, errFileAccessDenied
|
||||
} else if isSysErrIsDir(err) {
|
||||
case isSysErrIsDir(err):
|
||||
return nil, errIsNotRegular
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return wlk, nil
|
||||
}
|
||||
@@ -175,14 +179,16 @@ func (fsi *fsIOPool) Create(path string) (wlk *lock.LockedFile, err error) {
|
||||
// Attempt to create the file.
|
||||
wlk, err = lock.LockedOpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
switch {
|
||||
case os.IsPermission(err):
|
||||
return nil, errFileAccessDenied
|
||||
} else if isSysErrIsDir(err) {
|
||||
case isSysErrIsDir(err):
|
||||
return nil, errIsNotRegular
|
||||
} else if isSysErrPathNotFound(err) {
|
||||
case isSysErrPathNotFound(err):
|
||||
return nil, errFileAccessDenied
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Success.
|
||||
|
||||
+7
-19
@@ -261,22 +261,10 @@ func (fs *FSObjects) StorageInfo(ctx context.Context) StorageInfo {
|
||||
storageInfo := StorageInfo{
|
||||
Used: used,
|
||||
}
|
||||
storageInfo.Backend.Type = FS
|
||||
storageInfo.Backend.Type = BackendFS
|
||||
return storageInfo
|
||||
}
|
||||
|
||||
// Locking operations
|
||||
|
||||
// ListLocks - List namespace locks held in object layer
|
||||
func (fs *FSObjects) ListLocks(ctx context.Context, bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
|
||||
return []VolumeLockInfo{}, NotImplemented{}
|
||||
}
|
||||
|
||||
// ClearLocks - Clear namespace locks held in object layer
|
||||
func (fs *FSObjects) ClearLocks(ctx context.Context, info []VolumeLockInfo) error {
|
||||
return NotImplemented{}
|
||||
}
|
||||
|
||||
/// Bucket operations
|
||||
|
||||
// getBucketDir - will convert incoming bucket names to
|
||||
@@ -426,7 +414,7 @@ func (fs *FSObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
// CopyObject - copy object source object to destination object.
|
||||
// if source object and destination object are same we only
|
||||
// update metadata.
|
||||
func (fs *FSObjects) CopyObject(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))
|
||||
// Hold write lock on destination since in both cases
|
||||
// - if source and destination are same
|
||||
@@ -468,7 +456,7 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
|
||||
|
||||
// Save objects' metadata in `fs.json`.
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -516,7 +504,7 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
|
||||
//
|
||||
// startOffset indicates the starting read location 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 {
|
||||
return err
|
||||
}
|
||||
@@ -672,7 +660,7 @@ func (fs *FSObjects) getObjectInfo(ctx context.Context, bucket, object string) (
|
||||
// Read from fs metadata only if it exists.
|
||||
_, rerr := fsMeta.ReadFrom(ctx, rlk.LockedFile)
|
||||
fs.rwPool.Close(fsMetaPath)
|
||||
if rerr != nil {
|
||||
if rerr != nil && rerr != io.EOF {
|
||||
return oi, rerr
|
||||
}
|
||||
}
|
||||
@@ -722,7 +710,7 @@ func (fs *FSObjects) getObjectInfoWithLock(ctx context.Context, bucket, object s
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err == errCorruptedFormat || err == io.EOF {
|
||||
objectLock := fs.nsMutex.NewNSLock(bucket, object)
|
||||
@@ -766,7 +754,7 @@ func (fs *FSObjects) parentDirIsObject(ctx context.Context, bucket, parent strin
|
||||
// until EOF, writes data directly to configured filesystem path.
|
||||
// Additionally writes `fs.json` which carries the necessary metadata
|
||||
// for future object operations.
|
||||
func (fs *FSObjects) PutObject(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, data *hash.Reader, metadata map[string]string, opts ObjectOptions) (objInfo ObjectInfo, retErr error) {
|
||||
if err := checkPutObjectArgs(ctx, bucket, object, fs, data.Size()); err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
|
||||
+9
-20
@@ -26,12 +26,6 @@ import (
|
||||
|
||||
// Tests for if parent directory is object
|
||||
func TestFSParentDirIsObject(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
obj, disk, err := prepareFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -46,7 +40,7 @@ func TestFSParentDirIsObject(t *testing.T) {
|
||||
}
|
||||
objectContent := "12345"
|
||||
objInfo, err := obj.PutObject(context.Background(), bucketName, objectName,
|
||||
mustGetHashReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), nil)
|
||||
mustGetHashReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), nil, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -120,12 +114,6 @@ func TestNewFS(t *testing.T) {
|
||||
// TestFSShutdown - initialize a new FS object layer then calls
|
||||
// Shutdown to check returned results
|
||||
func TestFSShutdown(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(rootPath)
|
||||
|
||||
bucketName := "testbucket"
|
||||
objectName := "object"
|
||||
// Create and return an fsObject with its path in the disk
|
||||
@@ -133,9 +121,10 @@ func TestFSShutdown(t *testing.T) {
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
obj := initFSObjects(disk, t)
|
||||
fs := obj.(*FSObjects)
|
||||
|
||||
objectContent := "12345"
|
||||
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, mustGetHashReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), nil, ObjectOptions{})
|
||||
return fs, disk
|
||||
}
|
||||
|
||||
@@ -214,7 +203,7 @@ func TestFSPutObject(t *testing.T) {
|
||||
}
|
||||
|
||||
// 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, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil, ObjectOptions{})
|
||||
if err == nil {
|
||||
t.Fatal("Unexpected should fail here, bucket doesn't exist")
|
||||
}
|
||||
@@ -223,7 +212,7 @@ func TestFSPutObject(t *testing.T) {
|
||||
}
|
||||
|
||||
// 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+"/", mustGetHashReader(t, bytes.NewReader([]byte("abcd")), 0, "", ""), nil, ObjectOptions{})
|
||||
if err == nil {
|
||||
t.Fatal("Unexpected should fail here, bucket doesn't exist")
|
||||
}
|
||||
@@ -231,11 +220,11 @@ func TestFSPutObject(t *testing.T) {
|
||||
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, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil, ObjectOptions{})
|
||||
if err != nil {
|
||||
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", mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil, ObjectOptions{})
|
||||
if err == nil {
|
||||
t.Fatal("Unexpected should fail here, backend corruption occurred")
|
||||
}
|
||||
@@ -250,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/", mustGetHashReader(t, bytes.NewReader([]byte("abcd")), 0, "", ""), nil, ObjectOptions{})
|
||||
if err == nil {
|
||||
t.Fatal("Unexpected should fail here, backned corruption occurred")
|
||||
}
|
||||
@@ -278,7 +267,7 @@ func TestFSDeleteObject(t *testing.T) {
|
||||
objectName := "object"
|
||||
|
||||
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, mustGetHashReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), nil, ObjectOptions{})
|
||||
|
||||
// Test with invalid bucket name
|
||||
if err := fs.DeleteObject(context.Background(), "fo", objectName); !isSameType(err, BucketNameInvalid{}) {
|
||||
|
||||
+35
-23
@@ -19,10 +19,10 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
@@ -83,17 +83,6 @@ func ValidateGatewayArguments(serverAddr, endpointAddr string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
_, port := mustSplitHostPort(serverAddr)
|
||||
// On macOS, if a process already listens on LOCALIPADDR:PORT, net.Listen() falls back
|
||||
// to IPv6 address i.e minio will start listening on IPv6 address whereas another
|
||||
// (non-)minio process is listening on IPv4 of given port.
|
||||
// To avoid this error situation we check for port availability only for macOS.
|
||||
if err := checkPortAvailability(port); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if endpointAddr != "" {
|
||||
// Reject the endpoint if it points to the gateway handler itself.
|
||||
sameTarget, err := sameLocalAddrs(endpointAddr, serverAddr)
|
||||
@@ -148,19 +137,47 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
// Handle common env vars.
|
||||
handleCommonEnvVars()
|
||||
|
||||
// Get port to listen on from gateway address
|
||||
_, gatewayPort, pErr := net.SplitHostPort(gatewayAddr)
|
||||
if pErr != nil {
|
||||
logger.FatalIf(pErr, "Unable to start gateway")
|
||||
}
|
||||
|
||||
// On macOS, if a process already listens on LOCALIPADDR:PORT, net.Listen() falls back
|
||||
// to IPv6 address ie minio will start listening on IPv6 address whereas another
|
||||
// (non-)minio process is listening on IPv4 of given port.
|
||||
// To avoid this error situation we check for port availability.
|
||||
logger.FatalIf(checkPortAvailability(gatewayPort), "Unable to start the gateway")
|
||||
|
||||
// Validate if we have access, secret set through environment.
|
||||
if !globalIsEnvCreds {
|
||||
logger.Fatal(uiErrEnvCredentialsMissing(nil), "Unable to start gateway")
|
||||
logger.Fatal(uiErrEnvCredentialsMissingGateway(nil), "Unable to start gateway")
|
||||
}
|
||||
|
||||
// Create certs path.
|
||||
logger.FatalIf(createConfigDir(), "Unable to create configuration directories")
|
||||
|
||||
// Initialize gateway config.
|
||||
initConfig()
|
||||
// Initialize server config.
|
||||
srvCfg := newServerConfig()
|
||||
|
||||
// Load logger subsystem
|
||||
loadLoggers()
|
||||
// Override any values from ENVs.
|
||||
srvCfg.loadFromEnvs()
|
||||
|
||||
// Load values to cached global values.
|
||||
srvCfg.loadToCachedConfigs()
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
globalServerConfigMu.Lock()
|
||||
globalServerConfig = srvCfg
|
||||
globalServerConfigMu.Unlock()
|
||||
|
||||
var cacheConfig = globalServerConfig.GetCacheConfig()
|
||||
if len(cacheConfig.Drives) > 0 {
|
||||
var err error
|
||||
// initialize the new disk cache objects.
|
||||
globalCacheObjectAPI, err = newServerCacheObjects(cacheConfig)
|
||||
logger.FatalIf(err, "Unable to initialize disk caching")
|
||||
}
|
||||
|
||||
// Check and load SSL certificates.
|
||||
var err error
|
||||
@@ -217,12 +234,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
logger.FatalIf(err, "Unable to initialize gateway backend")
|
||||
}
|
||||
|
||||
if gw.Name() != "nas" {
|
||||
// Initialize policy sys for all gateways. NAS gateway already
|
||||
// initializes policy sys internally, avoid double initialization.
|
||||
// Additionally also don't block the initialization of gateway.
|
||||
go globalPolicySys.Init(newObject)
|
||||
}
|
||||
go globalPolicySys.Init(newObject)
|
||||
|
||||
// Once endpoints are finalized, initialize the new object api.
|
||||
globalObjLayerMutex.Lock()
|
||||
|
||||
@@ -23,11 +23,14 @@ import (
|
||||
|
||||
// Test printing Gateway common message.
|
||||
func TestPrintGatewayCommonMessage(t *testing.T) {
|
||||
root, err := newTestConfig(globalMinioDefaultRegion)
|
||||
obj, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
defer os.RemoveAll(fsDir)
|
||||
if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
apiEndpoints := []string{"http://127.0.0.1:9000"}
|
||||
printGatewayCommonMsg(apiEndpoints)
|
||||
@@ -35,11 +38,14 @@ func TestPrintGatewayCommonMessage(t *testing.T) {
|
||||
|
||||
// Test print gateway startup message.
|
||||
func TestPrintGatewayStartupMessage(t *testing.T) {
|
||||
root, err := newTestConfig(globalMinioDefaultRegion)
|
||||
obj, fsDir, err := prepareFS()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
defer os.RemoveAll(fsDir)
|
||||
if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
apiEndpoints := []string{"http://127.0.0.1:9000"}
|
||||
printGatewayStartupMessage(apiEndpoints, "azure")
|
||||
|
||||
@@ -18,7 +18,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
@@ -36,19 +35,19 @@ func (a GatewayUnsupported) ListMultipartUploads(ctx context.Context, bucket str
|
||||
}
|
||||
|
||||
// NewMultipartUpload upload object in multiple parts
|
||||
func (a GatewayUnsupported) NewMultipartUpload(ctx context.Context, bucket string, object string, metadata map[string]string) (uploadID string, err error) {
|
||||
func (a GatewayUnsupported) NewMultipartUpload(ctx context.Context, bucket string, object string, metadata map[string]string, opts ObjectOptions) (uploadID string, err error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return "", NotImplemented{}
|
||||
}
|
||||
|
||||
// CopyObjectPart copy part of object to uploadID for another object
|
||||
func (a GatewayUnsupported) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject, uploadID string, partID int, startOffset, length int64, srcInfo ObjectInfo) (pi PartInfo, err error) {
|
||||
func (a GatewayUnsupported) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject, uploadID string, partID int, startOffset, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (pi PartInfo, err error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return pi, NotImplemented{}
|
||||
}
|
||||
|
||||
// PutObjectPart puts a part of object in bucket
|
||||
func (a GatewayUnsupported) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, data *hash.Reader) (pi PartInfo, err error) {
|
||||
func (a GatewayUnsupported) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, data *hash.Reader, opts ObjectOptions) (pi PartInfo, err error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return pi, NotImplemented{}
|
||||
}
|
||||
@@ -132,25 +131,11 @@ func (a GatewayUnsupported) ListObjectsHeal(ctx context.Context, bucket, prefix,
|
||||
|
||||
// CopyObject copies a blob from source container to destination container.
|
||||
func (a GatewayUnsupported) CopyObject(ctx context.Context, srcBucket string, srcObject string, destBucket string, destObject string,
|
||||
srcInfo ObjectInfo) (objInfo ObjectInfo, err error) {
|
||||
srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return objInfo, NotImplemented{}
|
||||
}
|
||||
|
||||
// Locking operations
|
||||
|
||||
// ListLocks lists namespace locks held in object layer
|
||||
func (a GatewayUnsupported) ListLocks(ctx context.Context, bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return []VolumeLockInfo{}, NotImplemented{}
|
||||
}
|
||||
|
||||
// ClearLocks clears namespace locks held in object layer
|
||||
func (a GatewayUnsupported) ClearLocks(ctx context.Context, info []VolumeLockInfo) error {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return NotImplemented{}
|
||||
}
|
||||
|
||||
// RefreshBucketPolicy refreshes cache policy with what's on disk.
|
||||
func (a GatewayUnsupported) RefreshBucketPolicy(ctx context.Context, bucket string) error {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
|
||||
@@ -193,7 +193,6 @@ func s3MetaToAzureProperties(ctx context.Context, s3Metadata map[string]string)
|
||||
storage.BlobProperties, error) {
|
||||
for k := range s3Metadata {
|
||||
if strings.Contains(k, "--") {
|
||||
logger.LogIf(ctx, minio.UnsupportedMetadata{})
|
||||
return storage.BlobMetadata{}, storage.BlobProperties{}, minio.UnsupportedMetadata{}
|
||||
}
|
||||
}
|
||||
@@ -377,18 +376,12 @@ func getAzureUploadID() (string, error) {
|
||||
// checkAzureUploadID - returns error in case of given string is upload ID.
|
||||
func checkAzureUploadID(ctx context.Context, uploadID string) (err error) {
|
||||
if len(uploadID) != 16 {
|
||||
logger.LogIf(ctx, minio.MalformedUploadID{
|
||||
UploadID: uploadID,
|
||||
})
|
||||
return minio.MalformedUploadID{
|
||||
UploadID: uploadID,
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = hex.DecodeString(uploadID); err != nil {
|
||||
logger.LogIf(ctx, minio.MalformedUploadID{
|
||||
UploadID: uploadID,
|
||||
})
|
||||
return minio.MalformedUploadID{
|
||||
UploadID: uploadID,
|
||||
}
|
||||
@@ -449,7 +442,6 @@ func (a *azureObjects) MakeBucketWithLocation(ctx context.Context, bucket, locat
|
||||
// in azure documentation, so we will simply use the same function here.
|
||||
// Ref - https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata
|
||||
if !minio.IsValidBucketName(bucket) {
|
||||
logger.LogIf(ctx, minio.BucketNameInvalid{Bucket: bucket})
|
||||
return minio.BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
|
||||
@@ -457,7 +449,6 @@ func (a *azureObjects) MakeBucketWithLocation(ctx context.Context, bucket, locat
|
||||
err := container.Create(&storage.CreateContainerOptions{
|
||||
Access: storage.ContainerAccessTypePrivate,
|
||||
})
|
||||
logger.LogIf(ctx, err)
|
||||
return azureToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
@@ -469,7 +460,6 @@ func (a *azureObjects) GetBucketInfo(ctx context.Context, bucket string) (bi min
|
||||
Prefix: bucket,
|
||||
})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return bi, azureToObjectError(err, bucket)
|
||||
}
|
||||
for _, container := range resp.Containers {
|
||||
@@ -490,7 +480,6 @@ func (a *azureObjects) GetBucketInfo(ctx context.Context, bucket string) (bi min
|
||||
func (a *azureObjects) ListBuckets(ctx context.Context) (buckets []minio.BucketInfo, err error) {
|
||||
resp, err := a.client.ListContainers(storage.ListContainersParameters{})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, azureToObjectError(err)
|
||||
}
|
||||
for _, container := range resp.Containers {
|
||||
@@ -511,7 +500,6 @@ func (a *azureObjects) ListBuckets(ctx context.Context) (buckets []minio.BucketI
|
||||
func (a *azureObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
container := a.client.GetContainerReference(bucket)
|
||||
err := container.Delete(nil)
|
||||
logger.LogIf(ctx, err)
|
||||
return azureToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
@@ -549,7 +537,6 @@ func (a *azureObjects) ListObjects(ctx context.Context, bucket, prefix, marker,
|
||||
MaxResults: uint(maxKeys),
|
||||
})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return result, azureToObjectError(err, bucket, prefix)
|
||||
}
|
||||
|
||||
@@ -634,10 +621,9 @@ func (a *azureObjects) ListObjectsV2(ctx context.Context, bucket, prefix, contin
|
||||
//
|
||||
// startOffset indicates the starting read location of the object.
|
||||
// length indicates the total length of the object.
|
||||
func (a *azureObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) error {
|
||||
func (a *azureObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||
// startOffset cannot be negative.
|
||||
if startOffset < 0 {
|
||||
logger.LogIf(ctx, minio.InvalidRange{})
|
||||
return azureToObjectError(minio.InvalidRange{}, bucket, object)
|
||||
}
|
||||
|
||||
@@ -657,7 +643,6 @@ func (a *azureObjects) GetObject(ctx context.Context, bucket, object string, sta
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return azureToObjectError(err, bucket, object)
|
||||
}
|
||||
_, err = io.Copy(writer, rc)
|
||||
@@ -668,11 +653,10 @@ func (a *azureObjects) GetObject(ctx context.Context, bucket, object string, sta
|
||||
|
||||
// GetObjectInfo - reads blob metadata properties and replies back minio.ObjectInfo,
|
||||
// uses zure equivalent GetBlobProperties.
|
||||
func (a *azureObjects) GetObjectInfo(ctx context.Context, bucket, object string) (objInfo minio.ObjectInfo, err error) {
|
||||
func (a *azureObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
||||
err = blob.GetProperties(nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
@@ -690,7 +674,7 @@ func (a *azureObjects) GetObjectInfo(ctx context.Context, bucket, object string)
|
||||
|
||||
// PutObject - Create a new blob with the incoming data,
|
||||
// uses Azure equivalent CreateBlockBlobFromReader.
|
||||
func (a *azureObjects) PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string) (objInfo minio.ObjectInfo, err error) {
|
||||
func (a *azureObjects) PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
||||
blob.Metadata, blob.Properties, err = s3MetaToAzureProperties(ctx, metadata)
|
||||
if err != nil {
|
||||
@@ -698,15 +682,14 @@ func (a *azureObjects) PutObject(ctx context.Context, bucket, object string, dat
|
||||
}
|
||||
err = blob.CreateBlockBlobFromReader(data, nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
return a.GetObjectInfo(ctx, bucket, object)
|
||||
return a.GetObjectInfo(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
// CopyObject - Copies a blob from source container to destination container.
|
||||
// Uses Azure equivalent CopyBlob API.
|
||||
func (a *azureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo minio.ObjectInfo) (objInfo minio.ObjectInfo, err error) {
|
||||
func (a *azureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
srcBlobURL := a.client.GetContainerReference(srcBucket).GetBlobReference(srcObject).GetURL()
|
||||
destBlob := a.client.GetContainerReference(destBucket).GetBlobReference(destObject)
|
||||
azureMeta, props, err := s3MetaToAzureProperties(ctx, srcInfo.UserDefined)
|
||||
@@ -716,7 +699,6 @@ func (a *azureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, des
|
||||
destBlob.Metadata = azureMeta
|
||||
err = destBlob.Copy(srcBlobURL, nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, azureToObjectError(err, srcBucket, srcObject)
|
||||
}
|
||||
// Azure will copy metadata from the source object when an empty metadata map is provided.
|
||||
@@ -726,17 +708,15 @@ func (a *azureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, des
|
||||
destBlob.Metadata = azureMeta
|
||||
err = destBlob.SetMetadata(nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, azureToObjectError(err, srcBucket, srcObject)
|
||||
}
|
||||
}
|
||||
destBlob.Properties = props
|
||||
err = destBlob.SetProperties(nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, azureToObjectError(err, srcBucket, srcObject)
|
||||
}
|
||||
return a.GetObjectInfo(ctx, destBucket, destObject)
|
||||
return a.GetObjectInfo(ctx, destBucket, destObject, dstOpts)
|
||||
}
|
||||
|
||||
// DeleteObject - Deletes a blob on azure container, uses Azure
|
||||
@@ -769,14 +749,12 @@ func (a *azureObjects) checkUploadIDExists(ctx context.Context, bucketName, obje
|
||||
blob := a.client.GetContainerReference(bucketName).GetBlobReference(
|
||||
getAzureMetadataObjectName(objectName, uploadID))
|
||||
err = blob.GetMetadata(nil)
|
||||
logger.LogIf(ctx, err)
|
||||
err = azureToObjectError(err, bucketName, objectName)
|
||||
oerr := minio.ObjectNotFound{
|
||||
Bucket: bucketName,
|
||||
Object: objectName,
|
||||
}
|
||||
if err == oerr {
|
||||
logger.LogIf(ctx, minio.InvalidUploadID{UploadID: uploadID})
|
||||
err = minio.InvalidUploadID{
|
||||
UploadID: uploadID,
|
||||
}
|
||||
@@ -785,7 +763,7 @@ func (a *azureObjects) checkUploadIDExists(ctx context.Context, bucketName, obje
|
||||
}
|
||||
|
||||
// NewMultipartUpload - Use Azure equivalent CreateBlockBlob.
|
||||
func (a *azureObjects) NewMultipartUpload(ctx context.Context, bucket, object string, metadata map[string]string) (uploadID string, err error) {
|
||||
func (a *azureObjects) NewMultipartUpload(ctx context.Context, bucket, object string, metadata map[string]string, opts minio.ObjectOptions) (uploadID string, err error) {
|
||||
uploadID, err = getAzureUploadID()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
@@ -802,7 +780,6 @@ func (a *azureObjects) NewMultipartUpload(ctx context.Context, bucket, object st
|
||||
blob := a.client.GetContainerReference(bucket).GetBlobReference(metadataObject)
|
||||
err = blob.CreateBlockBlobFromReader(bytes.NewBuffer(jsonData), nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return "", azureToObjectError(err, bucket, metadataObject)
|
||||
}
|
||||
|
||||
@@ -810,7 +787,7 @@ func (a *azureObjects) NewMultipartUpload(ctx context.Context, bucket, object st
|
||||
}
|
||||
|
||||
// PutObjectPart - Use Azure equivalent PutBlockWithLength.
|
||||
func (a *azureObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader) (info minio.PartInfo, err error) {
|
||||
func (a *azureObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader, opts minio.ObjectOptions) (info minio.PartInfo, err error) {
|
||||
if err = a.checkUploadIDExists(ctx, bucket, object, uploadID); err != nil {
|
||||
return info, err
|
||||
}
|
||||
@@ -839,7 +816,6 @@ func (a *azureObjects) PutObjectPart(ctx context.Context, bucket, object, upload
|
||||
blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
||||
err = blob.PutBlockWithLength(id, uint64(subPartSize), io.LimitReader(data, subPartSize), nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return info, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
subPartNumber++
|
||||
@@ -871,7 +847,6 @@ func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uplo
|
||||
return result, nil
|
||||
}
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return result, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
// Build a sorted list of parts and return the requested entries.
|
||||
@@ -881,7 +856,6 @@ func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uplo
|
||||
var parsedUploadID string
|
||||
var md5Hex string
|
||||
if partNumber, _, parsedUploadID, md5Hex, err = azureParseBlockID(block.Name); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unexpected error"))
|
||||
return result, azureToObjectError(fmt.Errorf("Unexpected error"), bucket, object)
|
||||
}
|
||||
if parsedUploadID != uploadID {
|
||||
@@ -899,7 +873,6 @@ func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uplo
|
||||
if part.ETag != md5Hex {
|
||||
// If two parts of same partNumber were uploaded with different contents
|
||||
// return error as we won't be able to decide which the latest part is.
|
||||
logger.LogIf(ctx, fmt.Errorf("Unexpected error"))
|
||||
return result, azureToObjectError(fmt.Errorf("Unexpected error"), bucket, object)
|
||||
}
|
||||
part.Size += block.Size
|
||||
@@ -966,7 +939,6 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
|
||||
var metadataReader io.Reader
|
||||
blob := a.client.GetContainerReference(bucket).GetBlobReference(metadataObject)
|
||||
if metadataReader, err = blob.Get(nil); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, azureToObjectError(err, bucket, metadataObject)
|
||||
}
|
||||
|
||||
@@ -990,7 +962,6 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
|
||||
objBlob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
||||
resp, err := objBlob.GetBlockList(storage.BlockListTypeUncommitted, nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
@@ -1038,11 +1009,6 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
|
||||
// Error out if parts except last part sizing < 5MiB.
|
||||
for i, size := range partSizes[:len(partSizes)-1] {
|
||||
if size < azureS3MinPartSize {
|
||||
logger.LogIf(ctx, minio.PartTooSmall{
|
||||
PartNumber: uploadedParts[i].PartNumber,
|
||||
PartSize: size,
|
||||
PartETag: uploadedParts[i].ETag,
|
||||
})
|
||||
return objInfo, minio.PartTooSmall{
|
||||
PartNumber: uploadedParts[i].PartNumber,
|
||||
PartSize: size,
|
||||
@@ -1053,27 +1019,23 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
|
||||
|
||||
err = objBlob.PutBlockList(allBlocks, nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
if len(metadata.Metadata) > 0 {
|
||||
objBlob.Metadata, objBlob.Properties, err = s3MetaToAzureProperties(ctx, metadata.Metadata)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
err = objBlob.SetProperties(nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
err = objBlob.SetMetadata(nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
}
|
||||
return a.GetObjectInfo(ctx, bucket, object)
|
||||
return a.GetObjectInfo(ctx, bucket, object, minio.ObjectOptions{})
|
||||
}
|
||||
|
||||
// SetBucketPolicy - Azure supports three types of container policies:
|
||||
@@ -1099,15 +1061,12 @@ func (a *azureObjects) SetBucketPolicy(ctx context.Context, bucket string, bucke
|
||||
}
|
||||
prefix := bucket + "/*" // For all objects inside the bucket.
|
||||
if len(policies) != 1 {
|
||||
logger.LogIf(ctx, minio.NotImplemented{})
|
||||
return minio.NotImplemented{}
|
||||
}
|
||||
if policies[0].Prefix != prefix {
|
||||
logger.LogIf(ctx, minio.NotImplemented{})
|
||||
return minio.NotImplemented{}
|
||||
}
|
||||
if policies[0].Policy != miniogopolicy.BucketPolicyReadOnly {
|
||||
logger.LogIf(ctx, minio.NotImplemented{})
|
||||
return minio.NotImplemented{}
|
||||
}
|
||||
perm := storage.ContainerPermissions{
|
||||
@@ -1116,7 +1075,6 @@ func (a *azureObjects) SetBucketPolicy(ctx context.Context, bucket string, bucke
|
||||
}
|
||||
container := a.client.GetContainerReference(bucket)
|
||||
err = container.SetPermissions(perm, nil)
|
||||
logger.LogIf(ctx, err)
|
||||
return azureToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
@@ -1125,14 +1083,12 @@ func (a *azureObjects) GetBucketPolicy(ctx context.Context, bucket string) (*pol
|
||||
container := a.client.GetContainerReference(bucket)
|
||||
perm, err := container.GetPermissions(nil)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, azureToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
if perm.AccessType == storage.ContainerAccessTypePrivate {
|
||||
return nil, minio.BucketPolicyNotFound{Bucket: bucket}
|
||||
} else if perm.AccessType != storage.ContainerAccessTypeContainer {
|
||||
logger.LogIf(ctx, minio.NotImplemented{})
|
||||
return nil, azureToObjectError(minio.NotImplemented{})
|
||||
}
|
||||
|
||||
@@ -1165,6 +1121,5 @@ func (a *azureObjects) DeleteBucketPolicy(ctx context.Context, bucket string) er
|
||||
}
|
||||
container := a.client.GetContainerReference(bucket)
|
||||
err := container.SetPermissions(perm, nil)
|
||||
logger.LogIf(ctx, err)
|
||||
return azureToObjectError(err)
|
||||
}
|
||||
|
||||
@@ -400,7 +400,7 @@ func (l *b2Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuat
|
||||
//
|
||||
// startOffset indicates the starting read location of the object.
|
||||
// length indicates the total length of the object.
|
||||
func (l *b2Objects) GetObject(ctx context.Context, bucket string, object string, startOffset int64, length int64, writer io.Writer, etag string) error {
|
||||
func (l *b2Objects) GetObject(ctx context.Context, bucket string, object string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -417,7 +417,7 @@ func (l *b2Objects) GetObject(ctx context.Context, bucket string, object string,
|
||||
}
|
||||
|
||||
// GetObjectInfo reads object info and replies back ObjectInfo
|
||||
func (l *b2Objects) GetObjectInfo(ctx context.Context, bucket string, object string) (objInfo minio.ObjectInfo, err error) {
|
||||
func (l *b2Objects) GetObjectInfo(ctx context.Context, bucket string, object string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return objInfo, err
|
||||
@@ -508,7 +508,7 @@ func (nb *Reader) Read(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
// PutObject uploads the single upload to B2 backend by using *b2_upload_file* API, uploads upto 5GiB.
|
||||
func (l *b2Objects) PutObject(ctx context.Context, bucket string, object string, data *h2.Reader, metadata map[string]string) (objInfo minio.ObjectInfo, err error) {
|
||||
func (l *b2Objects) PutObject(ctx context.Context, bucket string, object string, data *h2.Reader, metadata map[string]string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return objInfo, err
|
||||
@@ -608,7 +608,7 @@ func (l *b2Objects) ListMultipartUploads(ctx context.Context, bucket string, pre
|
||||
// Each large file must consist of at least 2 parts, and all of the parts except the
|
||||
// last one must be at least 5MB in size. The last part must contain at least one byte.
|
||||
// For more information - https://www.backblaze.com/b2/docs/large_files.html
|
||||
func (l *b2Objects) NewMultipartUpload(ctx context.Context, bucket string, object string, metadata map[string]string) (string, error) {
|
||||
func (l *b2Objects) NewMultipartUpload(ctx context.Context, bucket string, object string, metadata map[string]string, o minio.ObjectOptions) (string, error) {
|
||||
var uploadID string
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
@@ -627,7 +627,7 @@ func (l *b2Objects) NewMultipartUpload(ctx context.Context, bucket string, objec
|
||||
}
|
||||
|
||||
// PutObjectPart puts a part of object in bucket, uses B2's LargeFile upload API.
|
||||
func (l *b2Objects) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, data *h2.Reader) (pi minio.PartInfo, err error) {
|
||||
func (l *b2Objects) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, data *h2.Reader, opts minio.ObjectOptions) (pi minio.PartInfo, err error) {
|
||||
bkt, err := l.Bucket(ctx, bucket)
|
||||
if err != nil {
|
||||
return pi, err
|
||||
@@ -723,7 +723,7 @@ func (l *b2Objects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
return oi, b2ToObjectError(err, bucket, object, uploadID)
|
||||
}
|
||||
|
||||
return l.GetObjectInfo(ctx, bucket, object)
|
||||
return l.GetObjectInfo(ctx, bucket, object, minio.ObjectOptions{})
|
||||
}
|
||||
|
||||
// SetBucketPolicy - B2 supports 2 types of bucket policies:
|
||||
|
||||
+198
-33
@@ -20,11 +20,15 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"math"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -738,7 +742,7 @@ func (l *gcsGateway) ListObjectsV2(ctx context.Context, bucket, prefix, continua
|
||||
//
|
||||
// startOffset indicates the starting read location of the object.
|
||||
// length indicates the total length of the object.
|
||||
func (l *gcsGateway) GetObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string) error {
|
||||
func (l *gcsGateway) GetObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||
// if we want to mimic S3 behavior exactly, we need to verify if bucket exists first,
|
||||
// otherwise gcs will just return object not exist in case of non-existing bucket
|
||||
if _, err := l.client.Bucket(bucket).Attrs(l.ctx); err != nil {
|
||||
@@ -768,22 +772,27 @@ func fromGCSAttrsToObjectInfo(attrs *storage.ObjectAttrs) minio.ObjectInfo {
|
||||
// Refer https://cloud.google.com/storage/docs/hashes-etags. Use CRC32C for ETag
|
||||
metadata := make(map[string]string)
|
||||
for k, v := range attrs.Metadata {
|
||||
k = http.CanonicalHeaderKey(k)
|
||||
// Translate the GCS custom metadata prefix
|
||||
if strings.HasPrefix(k, "X-Goog-Meta-") {
|
||||
k = strings.Replace(k, "X-Goog-Meta-", "X-Amz-Meta-", 1)
|
||||
}
|
||||
metadata[k] = v
|
||||
}
|
||||
if attrs.ContentType != "" {
|
||||
metadata["content-type"] = attrs.ContentType
|
||||
metadata["Content-Type"] = attrs.ContentType
|
||||
}
|
||||
if attrs.ContentEncoding != "" {
|
||||
metadata["content-encoding"] = attrs.ContentEncoding
|
||||
metadata["Content-Encoding"] = attrs.ContentEncoding
|
||||
}
|
||||
if attrs.CacheControl != "" {
|
||||
metadata["cache-control"] = attrs.CacheControl
|
||||
metadata["Cache-Control"] = attrs.CacheControl
|
||||
}
|
||||
if attrs.ContentDisposition != "" {
|
||||
metadata["content-disposition"] = attrs.ContentDisposition
|
||||
metadata["Content-Disposition"] = attrs.ContentDisposition
|
||||
}
|
||||
if attrs.ContentLanguage != "" {
|
||||
metadata["content-language"] = attrs.ContentLanguage
|
||||
metadata["Content-Language"] = attrs.ContentLanguage
|
||||
}
|
||||
return minio.ObjectInfo{
|
||||
Name: attrs.Name,
|
||||
@@ -799,26 +808,30 @@ func fromGCSAttrsToObjectInfo(attrs *storage.ObjectAttrs) minio.ObjectInfo {
|
||||
|
||||
// applyMetadataToGCSAttrs applies metadata to a GCS ObjectAttrs instance
|
||||
func applyMetadataToGCSAttrs(metadata map[string]string, attrs *storage.ObjectAttrs) {
|
||||
attrs.ContentType = metadata["content-type"]
|
||||
attrs.ContentEncoding = metadata["content-encoding"]
|
||||
attrs.CacheControl = metadata["cache-control"]
|
||||
attrs.ContentDisposition = metadata["content-disposition"]
|
||||
attrs.ContentLanguage = metadata["content-language"]
|
||||
|
||||
attrs.Metadata = make(map[string]string)
|
||||
for k, v := range metadata {
|
||||
attrs.Metadata[k] = v
|
||||
}
|
||||
// Filter metadata which is stored as a unique attribute
|
||||
for _, key := range []string{
|
||||
"content-type", "content-encoding", "cache-control", "content-disposition", "content-language",
|
||||
} {
|
||||
delete(attrs.Metadata, key)
|
||||
k = http.CanonicalHeaderKey(k)
|
||||
switch {
|
||||
case strings.HasPrefix(k, "X-Amz-Meta-"):
|
||||
// Translate the S3 user-defined metadata prefix
|
||||
k = strings.Replace(k, "X-Amz-Meta-", "x-goog-meta-", 1)
|
||||
attrs.Metadata[k] = v
|
||||
case k == "Content-Type":
|
||||
attrs.ContentType = v
|
||||
case k == "Content-Encoding":
|
||||
attrs.ContentEncoding = v
|
||||
case k == "Cache-Control":
|
||||
attrs.CacheControl = v
|
||||
case k == "Content-Disposition":
|
||||
attrs.ContentDisposition = v
|
||||
case k == "Content-Language":
|
||||
attrs.ContentLanguage = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetObjectInfo - reads object info and replies back ObjectInfo
|
||||
func (l *gcsGateway) GetObjectInfo(ctx context.Context, bucket string, object string) (minio.ObjectInfo, error) {
|
||||
func (l *gcsGateway) GetObjectInfo(ctx context.Context, bucket string, object string, opts minio.ObjectOptions) (minio.ObjectInfo, error) {
|
||||
// if we want to mimic S3 behavior exactly, we need to verify if bucket exists first,
|
||||
// otherwise gcs will just return object not exist in case of non-existing bucket
|
||||
if _, err := l.client.Bucket(bucket).Attrs(l.ctx); err != nil {
|
||||
@@ -836,7 +849,7 @@ func (l *gcsGateway) GetObjectInfo(ctx context.Context, bucket string, object st
|
||||
}
|
||||
|
||||
// PutObject - Create a new object with the incoming data,
|
||||
func (l *gcsGateway) PutObject(ctx context.Context, bucket string, key string, data *hash.Reader, metadata map[string]string) (minio.ObjectInfo, error) {
|
||||
func (l *gcsGateway) PutObject(ctx context.Context, bucket string, key string, data *hash.Reader, metadata map[string]string, opts minio.ObjectOptions) (minio.ObjectInfo, error) {
|
||||
// if we want to mimic S3 behavior exactly, we need to verify if bucket exists first,
|
||||
// otherwise gcs will just return object not exist in case of non-existing bucket
|
||||
if _, err := l.client.Bucket(bucket).Attrs(l.ctx); err != nil {
|
||||
@@ -875,7 +888,7 @@ func (l *gcsGateway) PutObject(ctx context.Context, bucket string, key string, d
|
||||
|
||||
// CopyObject - Copies a blob from source container to destination container.
|
||||
func (l *gcsGateway) CopyObject(ctx context.Context, srcBucket string, srcObject string, destBucket string, destObject string,
|
||||
srcInfo minio.ObjectInfo) (minio.ObjectInfo, error) {
|
||||
srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (minio.ObjectInfo, error) {
|
||||
|
||||
src := l.client.Bucket(srcBucket).Object(srcObject)
|
||||
dst := l.client.Bucket(destBucket).Object(destObject)
|
||||
@@ -904,7 +917,7 @@ func (l *gcsGateway) DeleteObject(ctx context.Context, bucket string, object str
|
||||
}
|
||||
|
||||
// NewMultipartUpload - upload object in multiple parts
|
||||
func (l *gcsGateway) NewMultipartUpload(ctx context.Context, bucket string, key string, metadata map[string]string) (uploadID string, err error) {
|
||||
func (l *gcsGateway) NewMultipartUpload(ctx context.Context, bucket string, key string, metadata map[string]string, o minio.ObjectOptions) (uploadID string, err error) {
|
||||
// generate new uploadid
|
||||
uploadID = minio.MustGetUUID()
|
||||
|
||||
@@ -927,14 +940,84 @@ func (l *gcsGateway) NewMultipartUpload(ctx context.Context, bucket string, key
|
||||
return uploadID, nil
|
||||
}
|
||||
|
||||
// ListMultipartUploads - lists all multipart uploads.
|
||||
// ListMultipartUploads - lists the (first) multipart upload for an object
|
||||
// matched _exactly_ by the prefix
|
||||
func (l *gcsGateway) ListMultipartUploads(ctx context.Context, bucket string, prefix string, keyMarker string, uploadIDMarker string, delimiter string, maxUploads int) (minio.ListMultipartsInfo, error) {
|
||||
// List objects under <bucket>/gcsMinioMultipartPathV1
|
||||
it := l.client.Bucket(bucket).Objects(ctx, &storage.Query{
|
||||
Prefix: gcsMinioMultipartPathV1,
|
||||
})
|
||||
|
||||
var uploads []minio.MultipartInfo
|
||||
|
||||
for {
|
||||
attrs, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return minio.ListMultipartsInfo{
|
||||
KeyMarker: keyMarker,
|
||||
UploadIDMarker: uploadIDMarker,
|
||||
MaxUploads: maxUploads,
|
||||
Prefix: prefix,
|
||||
Delimiter: delimiter,
|
||||
}, gcsToObjectError(err)
|
||||
}
|
||||
|
||||
// Skip entries other than gcs.json
|
||||
if !strings.HasSuffix(attrs.Name, gcsMinioMultipartMeta) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract multipart upload information from gcs.json
|
||||
obj := l.client.Bucket(bucket).Object(attrs.Name)
|
||||
objReader, rErr := obj.NewReader(ctx)
|
||||
if rErr != nil {
|
||||
logger.LogIf(ctx, rErr)
|
||||
return minio.ListMultipartsInfo{}, rErr
|
||||
}
|
||||
defer objReader.Close()
|
||||
|
||||
var mpMeta gcsMultipartMetaV1
|
||||
dec := json.NewDecoder(objReader)
|
||||
decErr := dec.Decode(&mpMeta)
|
||||
if decErr != nil {
|
||||
logger.LogIf(ctx, decErr)
|
||||
return minio.ListMultipartsInfo{}, decErr
|
||||
}
|
||||
|
||||
if prefix == mpMeta.Object {
|
||||
// Extract uploadId
|
||||
// E.g minio.sys.tmp/multipart/v1/d063ad89-fdc4-4ea3-a99e-22dba98151f5/gcs.json
|
||||
components := strings.SplitN(attrs.Name, "/", 5)
|
||||
if len(components) != 5 {
|
||||
compErr := errors.New("Invalid multipart upload format")
|
||||
logger.LogIf(ctx, compErr)
|
||||
return minio.ListMultipartsInfo{}, compErr
|
||||
}
|
||||
upload := minio.MultipartInfo{
|
||||
Object: mpMeta.Object,
|
||||
UploadID: components[3],
|
||||
Initiated: attrs.Created,
|
||||
}
|
||||
uploads = []minio.MultipartInfo{upload}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return minio.ListMultipartsInfo{
|
||||
KeyMarker: keyMarker,
|
||||
UploadIDMarker: uploadIDMarker,
|
||||
MaxUploads: maxUploads,
|
||||
Prefix: prefix,
|
||||
Delimiter: delimiter,
|
||||
KeyMarker: keyMarker,
|
||||
UploadIDMarker: uploadIDMarker,
|
||||
MaxUploads: maxUploads,
|
||||
Prefix: prefix,
|
||||
Delimiter: delimiter,
|
||||
Uploads: uploads,
|
||||
NextKeyMarker: "",
|
||||
NextUploadIDMarker: "",
|
||||
IsTruncated: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -947,7 +1030,7 @@ func (l *gcsGateway) checkUploadIDExists(ctx context.Context, bucket string, key
|
||||
}
|
||||
|
||||
// PutObjectPart puts a part of object in bucket
|
||||
func (l *gcsGateway) PutObjectPart(ctx context.Context, bucket string, key string, uploadID string, partNumber int, data *hash.Reader) (minio.PartInfo, error) {
|
||||
func (l *gcsGateway) PutObjectPart(ctx context.Context, bucket string, key string, uploadID string, partNumber int, data *hash.Reader, opts minio.ObjectOptions) (minio.PartInfo, error) {
|
||||
if err := l.checkUploadIDExists(ctx, bucket, key, uploadID); err != nil {
|
||||
return minio.PartInfo{}, err
|
||||
}
|
||||
@@ -978,9 +1061,91 @@ func (l *gcsGateway) PutObjectPart(ctx context.Context, bucket string, key strin
|
||||
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket
|
||||
// gcsGetPartInfo returns PartInfo of a given object part
|
||||
func gcsGetPartInfo(ctx context.Context, attrs *storage.ObjectAttrs) (minio.PartInfo, error) {
|
||||
components := strings.SplitN(attrs.Name, "/", 5)
|
||||
if len(components) != 5 {
|
||||
logger.LogIf(ctx, errors.New("Invalid multipart upload format"))
|
||||
return minio.PartInfo{}, errors.New("Invalid multipart upload format")
|
||||
}
|
||||
|
||||
partComps := strings.SplitN(components[4], ".", 2)
|
||||
if len(partComps) != 2 {
|
||||
logger.LogIf(ctx, errors.New("Invalid multipart part format"))
|
||||
return minio.PartInfo{}, errors.New("Invalid multipart part format")
|
||||
}
|
||||
|
||||
partNum, pErr := strconv.Atoi(partComps[0])
|
||||
if pErr != nil {
|
||||
logger.LogIf(ctx, pErr)
|
||||
return minio.PartInfo{}, errors.New("Invalid part number")
|
||||
}
|
||||
|
||||
return minio.PartInfo{
|
||||
PartNumber: partNum,
|
||||
LastModified: attrs.Updated,
|
||||
Size: attrs.Size,
|
||||
ETag: partComps[1],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListObjectParts returns all object parts for specified object in specified bucket
|
||||
func (l *gcsGateway) ListObjectParts(ctx context.Context, bucket string, key string, uploadID string, partNumberMarker int, maxParts int) (minio.ListPartsInfo, error) {
|
||||
return minio.ListPartsInfo{}, l.checkUploadIDExists(ctx, bucket, key, uploadID)
|
||||
it := l.client.Bucket(bucket).Objects(ctx, &storage.Query{
|
||||
Prefix: path.Join(gcsMinioMultipartPathV1, uploadID),
|
||||
})
|
||||
|
||||
var (
|
||||
count int
|
||||
partInfos []minio.PartInfo
|
||||
)
|
||||
|
||||
isTruncated := true
|
||||
for count < maxParts {
|
||||
attrs, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
isTruncated = false
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return minio.ListPartsInfo{}, gcsToObjectError(err)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(attrs.Name, gcsMinioMultipartMeta) {
|
||||
continue
|
||||
}
|
||||
|
||||
partInfo, pErr := gcsGetPartInfo(ctx, attrs)
|
||||
if pErr != nil {
|
||||
logger.LogIf(ctx, pErr)
|
||||
return minio.ListPartsInfo{}, pErr
|
||||
}
|
||||
|
||||
if partInfo.PartNumber <= partNumberMarker {
|
||||
continue
|
||||
}
|
||||
|
||||
partInfos = append(partInfos, partInfo)
|
||||
count++
|
||||
}
|
||||
|
||||
nextPartNumberMarker := 0
|
||||
if isTruncated {
|
||||
nextPartNumberMarker = partInfos[maxParts-1].PartNumber
|
||||
}
|
||||
|
||||
return minio.ListPartsInfo{
|
||||
Bucket: bucket,
|
||||
Object: key,
|
||||
UploadID: uploadID,
|
||||
PartNumberMarker: partNumberMarker,
|
||||
NextPartNumberMarker: nextPartNumberMarker,
|
||||
MaxParts: maxParts,
|
||||
Parts: partInfos,
|
||||
IsTruncated: isTruncated,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Called by AbortMultipartUpload and CompleteMultipartUpload for cleaning up.
|
||||
|
||||
@@ -23,7 +23,9 @@ import (
|
||||
"path"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/storage"
|
||||
"google.golang.org/api/googleapi"
|
||||
|
||||
miniogo "github.com/minio/minio-go"
|
||||
@@ -393,3 +395,105 @@ func TestGCSToObjectError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3MetaToGCSAttributes(t *testing.T) {
|
||||
headers := map[string]string{
|
||||
"accept-encoding": "gzip",
|
||||
"content-encoding": "gzip",
|
||||
"cache-control": "age: 3600",
|
||||
"content-disposition": "dummy",
|
||||
"content-type": "application/javascript",
|
||||
"Content-Language": "en",
|
||||
"X-Amz-Meta-Hdr": "value",
|
||||
"X-Amz-Meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
||||
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||
}
|
||||
// Only X-Amz-Meta- prefixed entries will be returned in
|
||||
// Metadata (without the prefix!)
|
||||
expectedHeaders := map[string]string{
|
||||
"x-goog-meta-Hdr": "value",
|
||||
"x-goog-meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||
"x-goog-meta-X-Amz-Matdesc": "{}",
|
||||
"x-goog-meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||
}
|
||||
|
||||
attrs := storage.ObjectAttrs{}
|
||||
applyMetadataToGCSAttrs(headers, &attrs)
|
||||
|
||||
if !reflect.DeepEqual(attrs.Metadata, expectedHeaders) {
|
||||
t.Fatalf("Test failed, expected %#v, got %#v", expectedHeaders, attrs.Metadata)
|
||||
}
|
||||
|
||||
if attrs.CacheControl != headers["cache-control"] {
|
||||
t.Fatalf("Test failed with Cache-Control mistmatch, expected %s, got %s", headers["cache-control"], attrs.CacheControl)
|
||||
}
|
||||
if attrs.ContentDisposition != headers["content-disposition"] {
|
||||
t.Fatalf("Test failed with Content-Disposition mistmatch, expected %s, got %s", headers["content-disposition"], attrs.ContentDisposition)
|
||||
}
|
||||
if attrs.ContentEncoding != headers["content-encoding"] {
|
||||
t.Fatalf("Test failed with Content-Encoding mistmatch, expected %s, got %s", headers["content-encoding"], attrs.ContentEncoding)
|
||||
}
|
||||
if attrs.ContentLanguage != headers["Content-Language"] {
|
||||
t.Fatalf("Test failed with Content-Language mistmatch, expected %s, got %s", headers["Content-Language"], attrs.ContentLanguage)
|
||||
}
|
||||
if attrs.ContentType != headers["content-type"] {
|
||||
t.Fatalf("Test failed with Content-Type mistmatch, expected %s, got %s", headers["content-type"], attrs.ContentType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGCSAttrsToObjectInfo(t *testing.T) {
|
||||
metadata := map[string]string{
|
||||
"x-goog-meta-Hdr": "value",
|
||||
"x-goog-meta-x_amz_key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||
"x-goog-meta-x-amz-matdesc": "{}",
|
||||
"x-goog-meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||
}
|
||||
expectedMeta := map[string]string{
|
||||
"X-Amz-Meta-Hdr": "value",
|
||||
"X-Amz-Meta-X_amz_key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
||||
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||
"Cache-Control": "max-age: 3600",
|
||||
"Content-Disposition": "dummy",
|
||||
"Content-Encoding": "gzip",
|
||||
"Content-Language": "en",
|
||||
"Content-Type": "application/javascript",
|
||||
}
|
||||
|
||||
attrs := storage.ObjectAttrs{
|
||||
Name: "test-obj",
|
||||
Bucket: "test-bucket",
|
||||
Updated: time.Now(),
|
||||
Size: 123,
|
||||
CRC32C: 45312398,
|
||||
CacheControl: "max-age: 3600",
|
||||
ContentDisposition: "dummy",
|
||||
ContentEncoding: "gzip",
|
||||
ContentLanguage: "en",
|
||||
ContentType: "application/javascript",
|
||||
Metadata: metadata,
|
||||
}
|
||||
expectedETag := minio.ToS3ETag(fmt.Sprintf("%d", attrs.CRC32C))
|
||||
|
||||
objInfo := fromGCSAttrsToObjectInfo(&attrs)
|
||||
if !reflect.DeepEqual(objInfo.UserDefined, expectedMeta) {
|
||||
t.Fatalf("Test failed, expected %#v, got %#v", expectedMeta, objInfo.UserDefined)
|
||||
}
|
||||
|
||||
if objInfo.Name != attrs.Name {
|
||||
t.Fatalf("Test failed with Name mistmatch, expected %s, got %s", attrs.Name, objInfo.Name)
|
||||
}
|
||||
if objInfo.Bucket != attrs.Bucket {
|
||||
t.Fatalf("Test failed with Bucket mistmatch, expected %s, got %s", attrs.Bucket, objInfo.Bucket)
|
||||
}
|
||||
if objInfo.ModTime != attrs.Updated {
|
||||
t.Fatalf("Test failed with ModTime mistmatch, expected %s, got %s", attrs.Updated, objInfo.ModTime)
|
||||
}
|
||||
if objInfo.Size != attrs.Size {
|
||||
t.Fatalf("Test failed with Size mistmatch, expected %d, got %d", attrs.Size, objInfo.Size)
|
||||
}
|
||||
if objInfo.ETag != expectedETag {
|
||||
t.Fatalf("Test failed with ETag mistmatch, expected %s, got %s", expectedETag, objInfo.ETag)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,12 +117,16 @@ EXAMPLES:
|
||||
}
|
||||
|
||||
func mantaGatewayMain(ctx *cli.Context) {
|
||||
// Validate gateway arguments.
|
||||
host := ctx.Args().First()
|
||||
// Validate gateway arguments.
|
||||
logger.FatalIf(minio.ValidateGatewayArguments(ctx.GlobalString("address"), host), "Invalid argument")
|
||||
args := ctx.Args()
|
||||
if !ctx.Args().Present() {
|
||||
args = cli.Args{"https://us-east.manta.joyent.com"}
|
||||
}
|
||||
|
||||
minio.StartGateway(ctx, &Manta{host})
|
||||
// Validate gateway arguments.
|
||||
logger.FatalIf(minio.ValidateGatewayArguments(ctx.GlobalString("address"), args.First()), "Invalid argument")
|
||||
|
||||
// Start the gateway..
|
||||
minio.StartGateway(ctx, &Manta{args.First()})
|
||||
}
|
||||
|
||||
// Manta implements Gateway.
|
||||
@@ -139,17 +143,22 @@ func (g *Manta) Name() string {
|
||||
// talk to manta remote backend.
|
||||
func (g *Manta) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, error) {
|
||||
var err error
|
||||
var secure bool
|
||||
var signer authentication.Signer
|
||||
var endpoint = defaultMantaURL
|
||||
ctx := context.Background()
|
||||
|
||||
if g.host != "" {
|
||||
endpoint, _, err = minio.ParseGatewayEndpoint(g.host)
|
||||
endpoint, secure, err = minio.ParseGatewayEndpoint(g.host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if secure {
|
||||
endpoint = "https://" + endpoint
|
||||
} else {
|
||||
endpoint = "http://" + endpoint
|
||||
}
|
||||
}
|
||||
|
||||
if overrideRoot, ok := os.LookupEnv("MANTA_ROOT"); ok {
|
||||
mantaRoot = overrideRoot
|
||||
}
|
||||
@@ -504,7 +513,7 @@ func (t *tritonObjects) ListObjectsV2(ctx context.Context, bucket, prefix, conti
|
||||
// indicates the total length of the object.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#GetObject
|
||||
func (t *tritonObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) error {
|
||||
func (t *tritonObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||
// Start offset cannot be negative.
|
||||
if startOffset < 0 {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unexpected error"))
|
||||
@@ -536,7 +545,7 @@ func (t *tritonObjects) GetObject(ctx context.Context, bucket, object string, st
|
||||
// uses Triton equivalent GetBlobProperties.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#GetObject
|
||||
func (t *tritonObjects) GetObjectInfo(ctx context.Context, bucket, object string) (objInfo minio.ObjectInfo, err error) {
|
||||
func (t *tritonObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
info, err := t.client.Objects().GetInfo(ctx, &storage.GetInfoInput{
|
||||
ObjectPath: path.Join(mantaRoot, bucket, object),
|
||||
})
|
||||
@@ -574,7 +583,7 @@ func (d dummySeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
// CreateBlockBlobFromReader.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#PutObject
|
||||
func (t *tritonObjects) PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string) (objInfo minio.ObjectInfo, err error) {
|
||||
func (t *tritonObjects) PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
if err = t.client.Objects().Put(ctx, &storage.PutObjectInput{
|
||||
ContentLength: uint64(data.Size()),
|
||||
ObjectPath: path.Join(mantaRoot, bucket, object),
|
||||
@@ -593,14 +602,14 @@ func (t *tritonObjects) PutObject(ctx context.Context, bucket, object string, da
|
||||
return objInfo, err
|
||||
}
|
||||
|
||||
return t.GetObjectInfo(ctx, bucket, object)
|
||||
return t.GetObjectInfo(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
// CopyObject - Copies a blob from source container to destination container.
|
||||
// Uses Manta Snaplinks API.
|
||||
//
|
||||
// https://apidocs.joyent.com/manta/api.html#PutSnapLink
|
||||
func (t *tritonObjects) CopyObject(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo minio.ObjectInfo) (objInfo minio.ObjectInfo, err error) {
|
||||
func (t *tritonObjects) CopyObject(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
if err = t.client.SnapLinks().Put(ctx, &storage.PutSnapLinkInput{
|
||||
SourcePath: path.Join(mantaRoot, srcBucket, srcObject),
|
||||
LinkPath: path.Join(mantaRoot, destBucket, destObject),
|
||||
@@ -609,7 +618,7 @@ func (t *tritonObjects) CopyObject(ctx context.Context, srcBucket, srcObject, de
|
||||
return objInfo, err
|
||||
}
|
||||
|
||||
return t.GetObjectInfo(ctx, destBucket, destObject)
|
||||
return t.GetObjectInfo(ctx, destBucket, destObject, dstOpts)
|
||||
}
|
||||
|
||||
// DeleteObject - Delete a blob in Manta, uses Triton equivalent DeleteBlob API.
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -553,7 +552,7 @@ func ossGetObject(ctx context.Context, client *oss.Client, bucket, key string, s
|
||||
//
|
||||
// startOffset indicates the starting read location of the object.
|
||||
// length indicates the total length of the object.
|
||||
func (l *ossObjects) GetObject(ctx context.Context, bucket, key string, startOffset, length int64, writer io.Writer, etag string) error {
|
||||
func (l *ossObjects) GetObject(ctx context.Context, bucket, key string, startOffset, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||
return ossGetObject(ctx, l.Client, bucket, key, startOffset, length, writer, etag)
|
||||
}
|
||||
|
||||
@@ -603,7 +602,7 @@ func ossGetObjectInfo(ctx context.Context, client *oss.Client, bucket, object st
|
||||
}
|
||||
|
||||
// GetObjectInfo reads object info and replies back ObjectInfo.
|
||||
func (l *ossObjects) GetObjectInfo(ctx context.Context, bucket, object string) (objInfo minio.ObjectInfo, err error) {
|
||||
func (l *ossObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
return ossGetObjectInfo(ctx, l.Client, bucket, object)
|
||||
}
|
||||
|
||||
@@ -631,12 +630,12 @@ func ossPutObject(ctx context.Context, client *oss.Client, bucket, object string
|
||||
}
|
||||
|
||||
// PutObject creates a new object with the incoming data.
|
||||
func (l *ossObjects) PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string) (objInfo minio.ObjectInfo, err error) {
|
||||
func (l *ossObjects) PutObject(ctx context.Context, bucket, object string, data *hash.Reader, metadata map[string]string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
return ossPutObject(ctx, l.Client, bucket, object, data, metadata)
|
||||
}
|
||||
|
||||
// CopyObject copies an object from source bucket to a destination bucket.
|
||||
func (l *ossObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo minio.ObjectInfo) (objInfo minio.ObjectInfo, err error) {
|
||||
func (l *ossObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
bkt, err := l.Client.Bucket(srcBucket)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
@@ -660,7 +659,7 @@ func (l *ossObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, ossToObjectError(err, srcBucket, srcObject)
|
||||
}
|
||||
return l.GetObjectInfo(ctx, dstBucket, dstObject)
|
||||
return l.GetObjectInfo(ctx, dstBucket, dstObject, dstOpts)
|
||||
}
|
||||
|
||||
// DeleteObject deletes a blob in bucket.
|
||||
@@ -726,7 +725,7 @@ func (l *ossObjects) ListMultipartUploads(ctx context.Context, bucket, prefix, k
|
||||
}
|
||||
|
||||
// NewMultipartUpload upload object in multiple parts.
|
||||
func (l *ossObjects) NewMultipartUpload(ctx context.Context, bucket, object string, metadata map[string]string) (uploadID string, err error) {
|
||||
func (l *ossObjects) NewMultipartUpload(ctx context.Context, bucket, object string, metadata map[string]string, o minio.ObjectOptions) (uploadID string, err error) {
|
||||
bkt, err := l.Client.Bucket(bucket)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
@@ -749,7 +748,7 @@ func (l *ossObjects) NewMultipartUpload(ctx context.Context, bucket, object stri
|
||||
}
|
||||
|
||||
// PutObjectPart puts a part of object in bucket.
|
||||
func (l *ossObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader) (pi minio.PartInfo, err error) {
|
||||
func (l *ossObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *hash.Reader, opts minio.ObjectOptions) (pi minio.PartInfo, err error) {
|
||||
bkt, err := l.Client.Bucket(bucket)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
@@ -816,11 +815,8 @@ func ossListObjectParts(client *oss.Client, bucket, object, uploadID string, par
|
||||
return lupr, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// always drain output (response body)
|
||||
io.CopyN(ioutil.Discard, resp.Body, 512)
|
||||
resp.Body.Close()
|
||||
}()
|
||||
// always drain output (response body)
|
||||
defer minio.CloseResponse(resp.Body)
|
||||
|
||||
err = xml.NewDecoder(resp.Body).Decode(&lupr)
|
||||
if err != nil {
|
||||
@@ -832,7 +828,7 @@ func ossListObjectParts(client *oss.Client, bucket, object, uploadID string, par
|
||||
// CopyObjectPart creates a part in a multipart upload by copying
|
||||
// existing object or a part of it.
|
||||
func (l *ossObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject, uploadID string,
|
||||
partID int, startOffset, length int64, srcInfo minio.ObjectInfo) (p minio.PartInfo, err error) {
|
||||
partID int, startOffset, length int64, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (p minio.PartInfo, err error) {
|
||||
|
||||
bkt, err := l.Client.Bucket(destBucket)
|
||||
if err != nil {
|
||||
@@ -958,7 +954,7 @@ func (l *ossObjects) CompleteMultipartUpload(ctx context.Context, bucket, object
|
||||
return oi, ossToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
return l.GetObjectInfo(ctx, bucket, object)
|
||||
return l.GetObjectInfo(ctx, bucket, object, minio.ObjectOptions{})
|
||||
}
|
||||
|
||||
// SetBucketPolicy sets policy on bucket.
|
||||
|
||||
@@ -223,13 +223,11 @@ func (l *s3Objects) MakeBucketWithLocation(ctx context.Context, bucket, location
|
||||
// access to these buckets.
|
||||
// Ref - http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
|
||||
if s3utils.CheckValidBucketName(bucket) != nil {
|
||||
logger.LogIf(ctx, minio.BucketNameInvalid{Bucket: bucket})
|
||||
return minio.BucketNameInvalid{Bucket: bucket}
|
||||
}
|
||||
|
||||
err := l.Client.MakeBucket(bucket, location)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return minio.ErrorRespToObjectError(err, bucket)
|
||||
}
|
||||
return err
|
||||
@@ -239,7 +237,6 @@ func (l *s3Objects) MakeBucketWithLocation(ctx context.Context, bucket, location
|
||||
func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.BucketInfo, e error) {
|
||||
buckets, err := l.Client.ListBuckets()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return bi, minio.ErrorRespToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
@@ -261,7 +258,6 @@ func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.
|
||||
func (l *s3Objects) ListBuckets(ctx context.Context) ([]minio.BucketInfo, error) {
|
||||
buckets, err := l.Client.ListBuckets()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
|
||||
@@ -280,7 +276,6 @@ func (l *s3Objects) ListBuckets(ctx context.Context) ([]minio.BucketInfo, error)
|
||||
func (l *s3Objects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
err := l.Client.RemoveBucket(bucket)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return minio.ErrorRespToObjectError(err, bucket)
|
||||
}
|
||||
return nil
|
||||
@@ -290,7 +285,6 @@ func (l *s3Objects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
func (l *s3Objects) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (loi minio.ListObjectsInfo, e error) {
|
||||
result, err := l.Client.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return loi, minio.ErrorRespToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
@@ -301,7 +295,6 @@ func (l *s3Objects) ListObjects(ctx context.Context, bucket string, prefix strin
|
||||
func (l *s3Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, e error) {
|
||||
result, err := l.Client.ListObjectsV2(bucket, prefix, continuationToken, fetchOwner, delimiter, maxKeys, startAfter)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return loi, minio.ErrorRespToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
@@ -314,13 +307,14 @@ func (l *s3Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuat
|
||||
//
|
||||
// startOffset indicates the starting read location of the object.
|
||||
// length indicates the total length of the object.
|
||||
func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string) error {
|
||||
func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string, o minio.ObjectOptions) error {
|
||||
if length < 0 && length != -1 {
|
||||
logger.LogIf(ctx, minio.InvalidRange{})
|
||||
return minio.ErrorRespToObjectError(minio.InvalidRange{}, bucket, key)
|
||||
}
|
||||
|
||||
opts := miniogo.GetObjectOptions{}
|
||||
opts.ServerSideEncryption = o.ServerSideEncryption
|
||||
|
||||
if startOffset >= 0 && length >= 0 {
|
||||
if err := opts.SetRange(startOffset, startOffset+length-1); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
@@ -329,7 +323,6 @@ func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, st
|
||||
}
|
||||
object, _, err := l.Client.GetObject(bucket, key, opts)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return minio.ErrorRespToObjectError(err, bucket, key)
|
||||
}
|
||||
defer object.Close()
|
||||
@@ -342,10 +335,9 @@ func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, st
|
||||
}
|
||||
|
||||
// GetObjectInfo reads object info and replies back ObjectInfo
|
||||
func (l *s3Objects) GetObjectInfo(ctx context.Context, bucket string, object string) (objInfo minio.ObjectInfo, err error) {
|
||||
oi, err := l.Client.StatObject(bucket, object, miniogo.StatObjectOptions{})
|
||||
func (l *s3Objects) GetObjectInfo(ctx context.Context, bucket string, object string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
oi, err := l.Client.StatObject(bucket, object, miniogo.StatObjectOptions{miniogo.GetObjectOptions{ServerSideEncryption: opts.ServerSideEncryption}})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return minio.ObjectInfo{}, minio.ErrorRespToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
@@ -353,10 +345,9 @@ func (l *s3Objects) GetObjectInfo(ctx context.Context, bucket string, object str
|
||||
}
|
||||
|
||||
// PutObject creates a new object with the incoming data,
|
||||
func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string, data *hash.Reader, metadata map[string]string) (objInfo minio.ObjectInfo, err error) {
|
||||
oi, err := l.Client.PutObject(bucket, object, data, data.Size(), data.MD5Base64String(), data.SHA256HexString(), minio.ToMinioClientMetadata(metadata))
|
||||
func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string, data *hash.Reader, metadata map[string]string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
oi, err := l.Client.PutObject(bucket, object, data, data.Size(), data.MD5Base64String(), data.SHA256HexString(), minio.ToMinioClientMetadata(metadata), opts.ServerSideEncryption)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, minio.ErrorRespToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
@@ -364,7 +355,7 @@ func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string,
|
||||
}
|
||||
|
||||
// CopyObject copies an object from source bucket to a destination bucket.
|
||||
func (l *s3Objects) CopyObject(ctx context.Context, srcBucket string, srcObject string, dstBucket string, dstObject string, srcInfo minio.ObjectInfo) (objInfo minio.ObjectInfo, err error) {
|
||||
func (l *s3Objects) CopyObject(ctx context.Context, srcBucket string, srcObject string, dstBucket string, dstObject string, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
// Set this header such that following CopyObject() always sets the right metadata on the destination.
|
||||
// metadata input is already a trickled down value from interpreting x-amz-metadata-directive at
|
||||
// handler layer. So what we have right now is supposed to be applied on the destination object anyways.
|
||||
@@ -372,17 +363,15 @@ func (l *s3Objects) CopyObject(ctx context.Context, srcBucket string, srcObject
|
||||
srcInfo.UserDefined["x-amz-metadata-directive"] = "REPLACE"
|
||||
srcInfo.UserDefined["x-amz-copy-source-if-match"] = srcInfo.ETag
|
||||
if _, err = l.Client.CopyObject(srcBucket, srcObject, dstBucket, dstObject, srcInfo.UserDefined); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return objInfo, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
||||
}
|
||||
return l.GetObjectInfo(ctx, dstBucket, dstObject)
|
||||
return l.GetObjectInfo(ctx, dstBucket, dstObject, dstOpts)
|
||||
}
|
||||
|
||||
// DeleteObject deletes a blob in bucket
|
||||
func (l *s3Objects) DeleteObject(ctx context.Context, bucket string, object string) error {
|
||||
err := l.Client.RemoveObject(bucket, object)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return minio.ErrorRespToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
@@ -400,22 +389,20 @@ func (l *s3Objects) ListMultipartUploads(ctx context.Context, bucket string, pre
|
||||
}
|
||||
|
||||
// NewMultipartUpload upload object in multiple parts
|
||||
func (l *s3Objects) NewMultipartUpload(ctx context.Context, bucket string, object string, metadata map[string]string) (uploadID string, err error) {
|
||||
func (l *s3Objects) NewMultipartUpload(ctx context.Context, bucket string, object string, metadata map[string]string, o minio.ObjectOptions) (uploadID string, err error) {
|
||||
// Create PutObject options
|
||||
opts := miniogo.PutObjectOptions{UserMetadata: metadata}
|
||||
opts := miniogo.PutObjectOptions{UserMetadata: metadata, ServerSideEncryption: o.ServerSideEncryption}
|
||||
uploadID, err = l.Client.NewMultipartUpload(bucket, object, opts)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return uploadID, minio.ErrorRespToObjectError(err, bucket, object)
|
||||
}
|
||||
return uploadID, nil
|
||||
}
|
||||
|
||||
// PutObjectPart puts a part of object in bucket
|
||||
func (l *s3Objects) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, data *hash.Reader) (pi minio.PartInfo, e error) {
|
||||
info, err := l.Client.PutObjectPart(bucket, object, uploadID, partID, data, data.Size(), data.MD5Base64String(), data.SHA256HexString())
|
||||
func (l *s3Objects) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, data *hash.Reader, opts minio.ObjectOptions) (pi minio.PartInfo, e error) {
|
||||
info, err := l.Client.PutObjectPart(bucket, object, uploadID, partID, data, data.Size(), data.MD5Base64String(), data.SHA256HexString(), opts.ServerSideEncryption)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return pi, minio.ErrorRespToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
@@ -425,7 +412,7 @@ func (l *s3Objects) PutObjectPart(ctx context.Context, bucket string, object str
|
||||
// CopyObjectPart creates a part in a multipart upload by copying
|
||||
// existing object or a part of it.
|
||||
func (l *s3Objects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject, uploadID string,
|
||||
partID int, startOffset, length int64, srcInfo minio.ObjectInfo) (p minio.PartInfo, err error) {
|
||||
partID int, startOffset, length int64, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (p minio.PartInfo, err error) {
|
||||
|
||||
srcInfo.UserDefined = map[string]string{
|
||||
"x-amz-copy-source-if-match": srcInfo.ETag,
|
||||
@@ -433,7 +420,6 @@ func (l *s3Objects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, de
|
||||
completePart, err := l.Client.CopyObjectPart(srcBucket, srcObject, destBucket, destObject,
|
||||
uploadID, partID, startOffset, length, srcInfo.UserDefined)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return p, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
||||
}
|
||||
p.PartNumber = completePart.PartNumber
|
||||
@@ -454,7 +440,6 @@ func (l *s3Objects) ListObjectParts(ctx context.Context, bucket string, object s
|
||||
// AbortMultipartUpload aborts a ongoing multipart upload
|
||||
func (l *s3Objects) AbortMultipartUpload(ctx context.Context, bucket string, object string, uploadID string) error {
|
||||
err := l.Client.AbortMultipartUpload(bucket, object, uploadID)
|
||||
logger.LogIf(ctx, err)
|
||||
return minio.ErrorRespToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
@@ -462,11 +447,10 @@ func (l *s3Objects) AbortMultipartUpload(ctx context.Context, bucket string, obj
|
||||
func (l *s3Objects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, uploadedParts []minio.CompletePart) (oi minio.ObjectInfo, e error) {
|
||||
err := l.Client.CompleteMultipartUpload(bucket, object, uploadID, minio.ToMinioClientCompleteParts(uploadedParts))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return oi, minio.ErrorRespToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
return l.GetObjectInfo(ctx, bucket, object)
|
||||
return l.GetObjectInfo(ctx, bucket, object, minio.ObjectOptions{})
|
||||
}
|
||||
|
||||
// SetBucketPolicy sets policy on bucket
|
||||
@@ -479,7 +463,6 @@ func (l *s3Objects) SetBucketPolicy(ctx context.Context, bucket string, bucketPo
|
||||
}
|
||||
|
||||
if err := l.Client.SetBucketPolicy(bucket, string(data)); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return minio.ErrorRespToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
@@ -500,7 +483,6 @@ func (l *s3Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy
|
||||
// DeleteBucketPolicy deletes all policies on bucket
|
||||
func (l *s3Objects) DeleteBucketPolicy(ctx context.Context, bucket string) error {
|
||||
if err := l.Client.SetBucketPolicy(bucket, ""); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return minio.ErrorRespToObjectError(err, bucket, "")
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -19,7 +19,6 @@ package sia
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -40,6 +39,7 @@ import (
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -233,13 +233,13 @@ func apiGet(ctx context.Context, addr, call, apiPassword string) (*http.Response
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
resp.Body.Close()
|
||||
minio.CloseResponse(resp.Body)
|
||||
logger.LogIf(ctx, MethodNotSupported{call})
|
||||
return nil, MethodNotSupported{call}
|
||||
}
|
||||
if non2xx(resp.StatusCode) {
|
||||
err := decodeError(resp)
|
||||
resp.Body.Close()
|
||||
minio.CloseResponse(resp.Body)
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
@@ -266,13 +266,13 @@ func apiPost(ctx context.Context, addr, call, vals, apiPassword string) (*http.R
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
resp.Body.Close()
|
||||
minio.CloseResponse(resp.Body)
|
||||
return nil, MethodNotSupported{call}
|
||||
}
|
||||
|
||||
if non2xx(resp.StatusCode) {
|
||||
err := decodeError(resp)
|
||||
resp.Body.Close()
|
||||
minio.CloseResponse(resp.Body)
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
@@ -285,7 +285,7 @@ func post(ctx context.Context, addr, call, vals, apiPassword string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
minio.CloseResponse(resp.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ func list(ctx context.Context, addr string, apiPassword string, obj *renterFiles
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer minio.CloseResponse(resp.Body)
|
||||
|
||||
if resp.StatusCode == http.StatusNoContent {
|
||||
logger.LogIf(ctx, fmt.Errorf("Expecting a response, but API returned %s", resp.Status))
|
||||
@@ -313,7 +313,7 @@ func get(ctx context.Context, addr, call, apiPassword string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
minio.CloseResponse(resp.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -431,7 +431,7 @@ func (s *siaObjects) ListObjects(ctx context.Context, bucket string, prefix stri
|
||||
return loi, nil
|
||||
}
|
||||
|
||||
func (s *siaObjects) GetObject(ctx context.Context, bucket string, object string, startOffset int64, length int64, writer io.Writer, etag string) error {
|
||||
func (s *siaObjects) GetObject(ctx context.Context, bucket string, object string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error {
|
||||
dstFile := path.Join(s.TempDir, minio.MustGetUUID())
|
||||
defer os.Remove(dstFile)
|
||||
|
||||
@@ -512,7 +512,7 @@ func (s *siaObjects) findSiaObject(ctx context.Context, bucket, object string) (
|
||||
}
|
||||
|
||||
// GetObjectInfo reads object info and replies back ObjectInfo
|
||||
func (s *siaObjects) GetObjectInfo(ctx context.Context, bucket string, object string) (minio.ObjectInfo, error) {
|
||||
func (s *siaObjects) GetObjectInfo(ctx context.Context, bucket string, object string, opts minio.ObjectOptions) (minio.ObjectInfo, error) {
|
||||
so, err := s.findSiaObject(ctx, bucket, object)
|
||||
if err != nil {
|
||||
return minio.ObjectInfo{}, err
|
||||
@@ -529,7 +529,7 @@ func (s *siaObjects) GetObjectInfo(ctx context.Context, bucket string, object st
|
||||
}
|
||||
|
||||
// PutObject creates a new object with the incoming data,
|
||||
func (s *siaObjects) PutObject(ctx context.Context, bucket string, object string, data *hash.Reader, metadata map[string]string) (objInfo minio.ObjectInfo, err error) {
|
||||
func (s *siaObjects) PutObject(ctx context.Context, bucket string, object string, data *hash.Reader, metadata map[string]string, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
srcFile := path.Join(s.TempDir, minio.MustGetUUID())
|
||||
writer, err := os.Create(srcFile)
|
||||
if err != nil {
|
||||
|
||||
@@ -475,7 +475,6 @@ var notimplementedObjectResourceNames = map[string]bool{
|
||||
"policy": true,
|
||||
"tagging": true,
|
||||
"restore": true,
|
||||
"select": true,
|
||||
}
|
||||
|
||||
// Resource handler ServeHTTP() wrapper
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user