mirror of
https://github.com/pgsty/minio.git
synced 2026-11-06 15:52:01 +03:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff7799452b | |||
| 97be455f63 | |||
| d16a409943 | |||
| 74efbb4153 | |||
| 0cfd5a21ba | |||
| 1ce2d29bbb | |||
| 48ffb7731a | |||
| 1cfd4a48d9 | |||
| 993a79d9c6 | |||
| 0c16b1c9a7 | |||
| 900cc27b51 | |||
| 8528017ad3 | |||
| a73da7755e | |||
| 2c0b3cadfc | |||
| 763fce909b | |||
| da8214845a | |||
| 74e2fe0879 | |||
| cb7f9ba286 | |||
| 7906f464fd | |||
| 7e4c9a9e1e | |||
| f6fd407e47 | |||
| 46ced81f41 | |||
| 7a02faab72 | |||
| e252114f06 | |||
| 39b3e4f9b3 | |||
| 158b8c2e86 | |||
| c4c79f61ce | |||
| 59e1d94770 | |||
| 59e847aebe | |||
| a13b58f630 | |||
| 9e7a19d6b9 | |||
| 16c648b109 | |||
| c871456269 | |||
| 55aa20595f | |||
| 707ed2b302 | |||
| 78be3f8947 | |||
| 67d508b214 | |||
| 0022c9d210 | |||
| 9b4a81ee60 | |||
| b3f22eac56 | |||
| a343d14f19 | |||
| 9c90a28546 | |||
| d9a7f80f68 | |||
| 502456db91 | |||
| 3eb7a8bde8 | |||
| ac3b59645e | |||
| 72929ec05b | |||
| 757c639044 | |||
| 589df3d5e7 | |||
| a71e08efb4 | |||
| c422f7f412 | |||
| 08b9244c48 | |||
| efbc665ad3 | |||
| 998f01fadc |
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: community, triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--- Provide a general summary of the issue in the Title above -->
|
||||
|
||||
## Expected Behavior
|
||||
<!--- If you're describing a bug, tell us what should happen -->
|
||||
<!--- If you're suggesting a change/improvement, tell us how it should work -->
|
||||
|
||||
## Current Behavior
|
||||
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
|
||||
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
|
||||
|
||||
## Possible Solution
|
||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||
<!--- or ideas how to implement the addition or change -->
|
||||
|
||||
## Steps to Reproduce (for bugs)
|
||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Context
|
||||
<!--- 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`):
|
||||
* Environment name and version (e.g. nginx 1.9.1):
|
||||
* Server type and version:
|
||||
* Operating System and version (`uname -a`):
|
||||
* Link to your project:
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: community, triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# MinIO Contribution Guide [](https://slack.min.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/) [](https://codecov.io/gh/minio/minio)
|
||||
# MinIO Contribution Guide [](https://slack.min.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/)
|
||||
|
||||
``MinIO`` community welcomes your contribution. To make the process as seamless as possible, we recommend you read this contribution guide.
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ docker: build
|
||||
# Builds minio and installs it to $GOPATH/bin.
|
||||
install: build
|
||||
@echo "Installing minio binary to '$(GOPATH)/bin/minio'"
|
||||
@mkdir -p $(GOPATH)/bin && cp -uf $(PWD)/minio $(GOPATH)/bin/minio
|
||||
@mkdir -p $(GOPATH)/bin && cp -f $(PWD)/minio $(GOPATH)/bin/minio
|
||||
@echo "Installation successful. To learn more, try \"minio --help\"."
|
||||
|
||||
clean:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# MinIO Quickstart Guide
|
||||
[](https://slack.min.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/) [](https://codecov.io/gh/minio/minio)
|
||||
[](https://slack.min.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/)
|
||||
|
||||
MinIO is an object storage server released under Apache License v2.0. It is compatible with Amazon S3 cloud storage service. It is best suited for storing unstructured data such as photos, videos, log files, backups and container / VM images. Size of an object can range from a few KBs to a maximum of 5TB.
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# MinIO Quickstart Guide [](https://slack.min.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/) [](https://codecov.io/gh/minio/minio)
|
||||
# MinIO Quickstart Guide [](https://slack.min.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/)
|
||||
|
||||
MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
|
||||
|
||||
|
||||
@@ -18,157 +18,237 @@ import React from "react"
|
||||
import { connect } from "react-redux"
|
||||
import web from "../web"
|
||||
import * as alertActions from "../alert/actions"
|
||||
import { getRandomAccessKey, getRandomSecretKey } from "../utils"
|
||||
import jwtDecode from "jwt-decode"
|
||||
import classNames from "classnames"
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
OverlayTrigger
|
||||
} from "react-bootstrap"
|
||||
import { Modal, ModalBody, ModalHeader } from "react-bootstrap"
|
||||
import InputGroup from "./InputGroup"
|
||||
import { ACCESS_KEY_MIN_LENGTH, SECRET_KEY_MIN_LENGTH } from "../constants"
|
||||
|
||||
export class ChangePasswordModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
accessKey: "",
|
||||
secretKey: "",
|
||||
keysReadOnly: false
|
||||
currentAccessKey: "",
|
||||
currentSecretKey: "",
|
||||
currentSecretKeyVisible: false,
|
||||
newAccessKey: "",
|
||||
newSecretKey: "",
|
||||
newSecretKeyVisible: false
|
||||
}
|
||||
}
|
||||
// When its shown, it loads the access key and secret key.
|
||||
// When its shown, it loads the access key from JWT token
|
||||
componentWillMount() {
|
||||
const { serverInfo } = this.props
|
||||
|
||||
// Check environment variables first.
|
||||
if (serverInfo.info.isEnvCreds || serverInfo.info.isWorm) {
|
||||
this.setState({
|
||||
accessKey: "xxxxxxxxx",
|
||||
secretKey: "xxxxxxxxx",
|
||||
keysReadOnly: true
|
||||
})
|
||||
} else {
|
||||
web.GetAuth().then(data => {
|
||||
this.setState({
|
||||
accessKey: data.accessKey,
|
||||
secretKey: data.secretKey
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Handle field changes from inside the modal.
|
||||
accessKeyChange(e) {
|
||||
const token = jwtDecode(web.GetToken())
|
||||
this.setState({
|
||||
accessKey: e.target.value
|
||||
})
|
||||
}
|
||||
|
||||
secretKeyChange(e) {
|
||||
this.setState({
|
||||
secretKey: e.target.value
|
||||
})
|
||||
}
|
||||
|
||||
secretKeyVisible(secretKeyVisible) {
|
||||
this.setState({
|
||||
secretKeyVisible
|
||||
currentAccessKey: token.sub,
|
||||
newAccessKey: token.sub
|
||||
})
|
||||
}
|
||||
|
||||
// Save the auth params and set them.
|
||||
setAuth(e) {
|
||||
const { showAlert } = this.props
|
||||
const accessKey = this.state.accessKey
|
||||
const secretKey = this.state.secretKey
|
||||
web
|
||||
.SetAuth({
|
||||
accessKey,
|
||||
secretKey
|
||||
})
|
||||
.then(data => {
|
||||
showAlert({
|
||||
type: "success",
|
||||
message: "Changed credentials"
|
||||
|
||||
if (this.canUpdateCredentials()) {
|
||||
const currentAccessKey = this.state.currentAccessKey
|
||||
const currentSecretKey = this.state.currentSecretKey
|
||||
const newAccessKey = this.state.newAccessKey
|
||||
const newSecretKey = this.state.newSecretKey
|
||||
web
|
||||
.SetAuth({
|
||||
currentAccessKey,
|
||||
currentSecretKey,
|
||||
newAccessKey,
|
||||
newSecretKey
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
showAlert({
|
||||
type: "danger",
|
||||
message: err.message
|
||||
.then(data => {
|
||||
showAlert({
|
||||
type: "success",
|
||||
message: "Credentials updated successfully."
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
showAlert({
|
||||
type: "danger",
|
||||
message: err.message
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
generateAuth(e) {
|
||||
web.GenerateAuth().then(data => {
|
||||
const { serverInfo } = this.props
|
||||
// Generate random access key only for root user
|
||||
if (!serverInfo.userInfo.isIAMUser) {
|
||||
this.setState({
|
||||
accessKey: data.accessKey,
|
||||
secretKey: data.secretKey,
|
||||
secretKeyVisible: true
|
||||
newAccessKey: getRandomAccessKey()
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
newSecretKey: getRandomSecretKey(),
|
||||
newSecretKeyVisible: true
|
||||
})
|
||||
}
|
||||
|
||||
canChangePassword() {
|
||||
const { serverInfo } = this.props
|
||||
// Password change is not allowed in WORM mode
|
||||
if (serverInfo.info.isWorm) {
|
||||
return false
|
||||
}
|
||||
|
||||
// When credentials are set on ENV, password change not allowed for owner
|
||||
if (serverInfo.info.isEnvCreds && !serverInfo.userInfo.isIAMUser) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
canUpdateCredentials() {
|
||||
return (
|
||||
this.state.currentAccessKey.length > 0 &&
|
||||
this.state.currentSecretKey.length > 0 &&
|
||||
this.state.newAccessKey.length >= ACCESS_KEY_MIN_LENGTH &&
|
||||
this.state.newSecretKey.length >= SECRET_KEY_MIN_LENGTH
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hideChangePassword } = this.props
|
||||
const { hideChangePassword, serverInfo } = this.props
|
||||
const allowChangePassword = this.canChangePassword()
|
||||
|
||||
if (!allowChangePassword) {
|
||||
return (
|
||||
<Modal bsSize="sm" animation={false} show={true}>
|
||||
<ModalHeader>Change Password</ModalHeader>
|
||||
<ModalBody>
|
||||
Credentials of this user cannot be updated through MinIO Browser.
|
||||
</ModalBody>
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
id="cancel-change-password"
|
||||
className="btn btn-link"
|
||||
onClick={hideChangePassword}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal bsSize="sm" animation={false} show={true}>
|
||||
<ModalHeader>Change Password</ModalHeader>
|
||||
<ModalBody className="m-t-20">
|
||||
<InputGroup
|
||||
value={this.state.accessKey}
|
||||
onChange={this.accessKeyChange.bind(this)}
|
||||
id="accessKey"
|
||||
label="Access Key"
|
||||
name="accesskey"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
required="required"
|
||||
autoComplete="false"
|
||||
align="ig-left"
|
||||
readonly={this.state.keysReadOnly}
|
||||
/>
|
||||
<i
|
||||
onClick={this.secretKeyVisible.bind(
|
||||
this,
|
||||
!this.state.secretKeyVisible
|
||||
<div className="has-toggle-password">
|
||||
<InputGroup
|
||||
value={this.state.currentAccessKey}
|
||||
id="currentAccessKey"
|
||||
label="Current Access Key"
|
||||
name="currentAccesskey"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
required="required"
|
||||
autoComplete="false"
|
||||
align="ig-left"
|
||||
readonly={true}
|
||||
/>
|
||||
|
||||
<i
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
currentSecretKeyVisible: !this.state.currentSecretKeyVisible
|
||||
})
|
||||
}}
|
||||
className={
|
||||
"toggle-password fa fa-eye " +
|
||||
(this.state.currentSecretKeyVisible ? "toggled" : "")
|
||||
}
|
||||
/>
|
||||
<InputGroup
|
||||
value={this.state.currentSecretKey}
|
||||
onChange={e => {
|
||||
this.setState({ currentSecretKey: e.target.value })
|
||||
}}
|
||||
id="currentSecretKey"
|
||||
label="Current Secret Key"
|
||||
name="currentSecretKey"
|
||||
type={this.state.currentSecretKeyVisible ? "text" : "password"}
|
||||
spellCheck="false"
|
||||
required="required"
|
||||
autoComplete="false"
|
||||
align="ig-left"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="has-toggle-password m-t-30">
|
||||
{!serverInfo.userInfo.isIAMUser && (
|
||||
<InputGroup
|
||||
value={this.state.newAccessKey}
|
||||
id="newAccessKey"
|
||||
label={"New Access Key"}
|
||||
name="newAccesskey"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
required="required"
|
||||
autoComplete="false"
|
||||
align="ig-left"
|
||||
onChange={e => {
|
||||
this.setState({ newAccessKey: e.target.value })
|
||||
}}
|
||||
readonly={serverInfo.userInfo.isIAMUser}
|
||||
/>
|
||||
)}
|
||||
className={
|
||||
"toggle-password fa fa-eye " +
|
||||
(this.state.secretKeyVisible ? "toggled" : "")
|
||||
}
|
||||
/>
|
||||
<InputGroup
|
||||
value={this.state.secretKey}
|
||||
onChange={this.secretKeyChange.bind(this)}
|
||||
id="secretKey"
|
||||
label="Secret Key"
|
||||
name="accesskey"
|
||||
type={this.state.secretKeyVisible ? "text" : "password"}
|
||||
spellCheck="false"
|
||||
required="required"
|
||||
autoComplete="false"
|
||||
align="ig-left"
|
||||
readonly={this.state.keysReadOnly}
|
||||
/>
|
||||
|
||||
<i
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
newSecretKeyVisible: !this.state.newSecretKeyVisible
|
||||
})
|
||||
}}
|
||||
className={
|
||||
"toggle-password fa fa-eye " +
|
||||
(this.state.newSecretKeyVisible ? "toggled" : "")
|
||||
}
|
||||
/>
|
||||
<InputGroup
|
||||
value={this.state.newSecretKey}
|
||||
onChange={e => {
|
||||
this.setState({ newSecretKey: e.target.value })
|
||||
}}
|
||||
id="newSecretKey"
|
||||
label="New Secret Key"
|
||||
name="newSecretKey"
|
||||
type={this.state.newSecretKeyVisible ? "text" : "password"}
|
||||
spellCheck="false"
|
||||
required="required"
|
||||
autoComplete="false"
|
||||
align="ig-left"
|
||||
onChange={e => {
|
||||
this.setState({ newSecretKey: e.target.value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
id="generate-keys"
|
||||
className={
|
||||
"btn btn-primary " + (this.state.keysReadOnly ? "hidden" : "")
|
||||
}
|
||||
className={"btn btn-primary"}
|
||||
onClick={this.generateAuth.bind(this)}
|
||||
>
|
||||
Generate
|
||||
</button>
|
||||
<button
|
||||
id="update-keys"
|
||||
className={
|
||||
"btn btn-success " + (this.state.keysReadOnly ? "hidden" : "")
|
||||
}
|
||||
className={classNames({
|
||||
btn: true,
|
||||
"btn-success": this.canUpdateCredentials()
|
||||
})}
|
||||
disabled={!this.canUpdateCredentials()}
|
||||
onClick={this.setAuth.bind(this)}
|
||||
>
|
||||
Update
|
||||
@@ -198,4 +278,7 @@ const mapDispatchToProps = dispatch => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChangePasswordModal)
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ChangePasswordModal)
|
||||
|
||||
@@ -17,21 +17,38 @@
|
||||
import React from "react"
|
||||
import { shallow, mount } from "enzyme"
|
||||
import { ChangePasswordModal } from "../ChangePasswordModal"
|
||||
import jwtDecode from "jwt-decode"
|
||||
|
||||
jest.mock("jwt-decode")
|
||||
|
||||
jwtDecode.mockImplementation(() => ({ sub: "minio" }))
|
||||
|
||||
jest.mock("../../web", () => ({
|
||||
GetAuth: jest.fn(() => {
|
||||
return Promise.resolve({ accessKey: "test1", secretKey: "test2" })
|
||||
}),
|
||||
GenerateAuth: jest.fn(() => {
|
||||
return Promise.resolve({ accessKey: "gen1", secretKey: "gen2" })
|
||||
}),
|
||||
SetAuth: jest.fn(({ accessKey, secretKey }) => {
|
||||
if (accessKey == "test3" && secretKey == "test4") {
|
||||
return Promise.resolve({})
|
||||
} else {
|
||||
return Promise.reject({ message: "Error" })
|
||||
SetAuth: jest.fn(
|
||||
({ currentAccessKey, currentSecretKey, newAccessKey, newSecretKey }) => {
|
||||
if (
|
||||
currentAccessKey == "minio" &&
|
||||
currentSecretKey == "minio123" &&
|
||||
newAccessKey == "test" &&
|
||||
newSecretKey == "test1234"
|
||||
) {
|
||||
return Promise.resolve({})
|
||||
} else {
|
||||
return Promise.reject({
|
||||
message: "Error"
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
GetToken: jest.fn(() => "")
|
||||
}))
|
||||
|
||||
jest.mock("../../utils", () => ({
|
||||
getRandomAccessKey: () => "raccesskey",
|
||||
getRandomSecretKey: () => "rsecretkey"
|
||||
}))
|
||||
|
||||
describe("ChangePasswordModal", () => {
|
||||
@@ -40,57 +57,112 @@ describe("ChangePasswordModal", () => {
|
||||
memory: "test",
|
||||
platform: "test",
|
||||
runtime: "test",
|
||||
info: { isEnvCreds: false }
|
||||
info: { isEnvCreds: false },
|
||||
userInfo: { isIAMUser: false }
|
||||
}
|
||||
|
||||
it("should render without crashing", () => {
|
||||
shallow(<ChangePasswordModal serverInfo={serverInfo} />)
|
||||
})
|
||||
|
||||
it("should get the keys when its rendered", () => {
|
||||
const wrapper = shallow(<ChangePasswordModal serverInfo={serverInfo} />)
|
||||
setImmediate(() => {
|
||||
expect(wrapper.state("accessKey")).toBe("test1")
|
||||
expect(wrapper.state("secretKey")).toBe("test2")
|
||||
})
|
||||
it("should not allow changing password when isWorm is true", () => {
|
||||
const newServerInfo = { ...serverInfo, info: { isWorm: true } }
|
||||
const wrapper = shallow(<ChangePasswordModal serverInfo={newServerInfo} />)
|
||||
expect(
|
||||
wrapper
|
||||
.find("ModalBody")
|
||||
.childAt(0)
|
||||
.text()
|
||||
).toBe("Credentials of this user cannot be updated through MinIO Browser.")
|
||||
})
|
||||
|
||||
it("should show readonly keys when isEnvCreds is true", () => {
|
||||
const newServerInfo = { ...serverInfo, info: { isEnvCreds: true } }
|
||||
it("should not allow changing password when isEnvCreds is true and not IAM user", () => {
|
||||
const newServerInfo = {
|
||||
...serverInfo,
|
||||
info: { isEnvCreds: true },
|
||||
userInfo: { isIAMUser: false }
|
||||
}
|
||||
const wrapper = shallow(<ChangePasswordModal serverInfo={newServerInfo} />)
|
||||
expect(wrapper.state("accessKey")).toBe("xxxxxxxxx")
|
||||
expect(wrapper.state("secretKey")).toBe("xxxxxxxxx")
|
||||
expect(wrapper.find("#accessKey").prop("readonly")).toBeTruthy()
|
||||
expect(wrapper.find("#secretKey").prop("readonly")).toBeTruthy()
|
||||
expect(wrapper.find("#generate-keys").hasClass("hidden")).toBeTruthy()
|
||||
expect(wrapper.find("#update-keys").hasClass("hidden")).toBeTruthy()
|
||||
expect(
|
||||
wrapper
|
||||
.find("ModalBody")
|
||||
.childAt(0)
|
||||
.text()
|
||||
).toBe("Credentials of this user cannot be updated through MinIO Browser.")
|
||||
})
|
||||
|
||||
it("should generate accessKey and secretKey when Generate buttons is clicked", () => {
|
||||
const wrapper = shallow(<ChangePasswordModal serverInfo={serverInfo} />)
|
||||
wrapper.find("#generate-keys").simulate("click")
|
||||
setImmediate(() => {
|
||||
expect(wrapper.state("accessKey")).toBe("gen1")
|
||||
expect(wrapper.state("secretKey")).toBe("gen2")
|
||||
expect(wrapper.state("newAccessKey")).toBe("raccesskey")
|
||||
expect(wrapper.state("newSecretKey")).toBe("rsecretkey")
|
||||
})
|
||||
})
|
||||
|
||||
it("should not generate accessKey for IAM User", () => {
|
||||
const newServerInfo = {
|
||||
...serverInfo,
|
||||
userInfo: { isIAMUser: true }
|
||||
}
|
||||
const wrapper = shallow(<ChangePasswordModal serverInfo={newServerInfo} />)
|
||||
wrapper.find("#generate-keys").simulate("click")
|
||||
setImmediate(() => {
|
||||
expect(wrapper.state("newAccessKey")).toBe("minio")
|
||||
expect(wrapper.state("newSecretKey")).toBe("rsecretkey")
|
||||
})
|
||||
})
|
||||
|
||||
it("should not show new accessKey field for IAM User", () => {
|
||||
const newServerInfo = {
|
||||
...serverInfo,
|
||||
userInfo: { isIAMUser: true }
|
||||
}
|
||||
const wrapper = shallow(<ChangePasswordModal serverInfo={newServerInfo} />)
|
||||
expect(wrapper.find("#newAccesskey").exists()).toBeFalsy()
|
||||
})
|
||||
|
||||
it("should disble Update button for invalid accessKey or secretKey", () => {
|
||||
const showAlert = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ChangePasswordModal serverInfo={serverInfo} showAlert={showAlert} />
|
||||
)
|
||||
wrapper
|
||||
.find("#currentAccessKey")
|
||||
.simulate("change", { target: { value: "minio" } })
|
||||
wrapper
|
||||
.find("#currentSecretKey")
|
||||
.simulate("change", { target: { value: "minio123" } })
|
||||
wrapper.find("#newAccessKey").simulate("change", { target: { value: "t" } })
|
||||
wrapper
|
||||
.find("#newSecretKey")
|
||||
.simulate("change", { target: { value: "t1" } })
|
||||
expect(wrapper.find("#update-keys").prop("disabled")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should update accessKey and secretKey when Update button is clicked", () => {
|
||||
const showAlert = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ChangePasswordModal serverInfo={serverInfo} showAlert={showAlert} />
|
||||
)
|
||||
wrapper
|
||||
.find("#accessKey")
|
||||
.simulate("change", { target: { value: "test3" } })
|
||||
.find("#currentAccessKey")
|
||||
.simulate("change", { target: { value: "minio" } })
|
||||
wrapper
|
||||
.find("#secretKey")
|
||||
.simulate("change", { target: { value: "test4" } })
|
||||
.find("#currentSecretKey")
|
||||
.simulate("change", { target: { value: "minio123" } })
|
||||
wrapper
|
||||
.find("#newAccessKey")
|
||||
.simulate("change", { target: { value: "test" } })
|
||||
wrapper
|
||||
.find("#newSecretKey")
|
||||
.simulate("change", { target: { value: "test1234" } })
|
||||
expect(wrapper.find("#update-keys").prop("disabled")).toBeFalsy()
|
||||
wrapper.find("#update-keys").simulate("click")
|
||||
setImmediate(() => {
|
||||
expect(showAlert).toHaveBeenCalledWith({
|
||||
type: "success",
|
||||
message: "Changed credentials"
|
||||
message: "Credentials updated successfully."
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ export const fetchStorageInfo = () => {
|
||||
return web.StorageInfo().then(res => {
|
||||
const storageInfo = {
|
||||
total: res.storageInfo.Total,
|
||||
used: res.storageInfo.Used
|
||||
used: res.storageInfo.Used
|
||||
}
|
||||
dispatch(setStorageInfo(storageInfo))
|
||||
})
|
||||
@@ -54,7 +54,8 @@ export const fetchServerInfo = () => {
|
||||
memory: res.MinioMemory,
|
||||
platform: res.MinioPlatform,
|
||||
runtime: res.MinioRuntime,
|
||||
info: res.MinioGlobalInfo
|
||||
info: res.MinioGlobalInfo,
|
||||
userInfo: res.MinioUserInfo
|
||||
}
|
||||
dispatch(setServerInfo(serverInfo))
|
||||
})
|
||||
|
||||
@@ -28,3 +28,6 @@ export const NONE = "none"
|
||||
export const SHARE_OBJECT_EXPIRY_DAYS = 5
|
||||
export const SHARE_OBJECT_EXPIRY_HOURS = 0
|
||||
export const SHARE_OBJECT_EXPIRY_MINUTES = 0
|
||||
|
||||
export const ACCESS_KEY_MIN_LENGTH = 3
|
||||
export const SECRET_KEY_MIN_LENGTH = 8
|
||||
|
||||
@@ -208,7 +208,8 @@ export const shareObject = (object, days, hours, minutes) => {
|
||||
.PresignedGet({
|
||||
host: location.host,
|
||||
bucket: currentBucket,
|
||||
object: objectName
|
||||
object: objectName,
|
||||
expiry: expiry
|
||||
})
|
||||
.then(obj => {
|
||||
dispatch(showShareObject(object, obj.url))
|
||||
|
||||
+46
-22
@@ -14,11 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { minioBrowserPrefix } from './constants.js'
|
||||
import { minioBrowserPrefix } from "./constants.js"
|
||||
|
||||
export const sortObjectsByName = (objects, order) => {
|
||||
let folders = objects.filter(object => object.name.endsWith('/'))
|
||||
let files = objects.filter(object => !object.name.endsWith('/'))
|
||||
let folders = objects.filter(object => object.name.endsWith("/"))
|
||||
let files = objects.filter(object => !object.name.endsWith("/"))
|
||||
folders = folders.sort((a, b) => {
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1
|
||||
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1
|
||||
@@ -37,32 +37,34 @@ export const sortObjectsByName = (objects, order) => {
|
||||
}
|
||||
|
||||
export const sortObjectsBySize = (objects, order) => {
|
||||
let folders = objects.filter(object => object.name.endsWith('/'))
|
||||
let files = objects.filter(object => !object.name.endsWith('/'))
|
||||
let folders = objects.filter(object => object.name.endsWith("/"))
|
||||
let files = objects.filter(object => !object.name.endsWith("/"))
|
||||
files = files.sort((a, b) => a.size - b.size)
|
||||
if (order)
|
||||
files = files.reverse()
|
||||
if (order) files = files.reverse()
|
||||
return [...folders, ...files]
|
||||
}
|
||||
|
||||
export const sortObjectsByDate = (objects, order) => {
|
||||
let folders = objects.filter(object => object.name.endsWith('/'))
|
||||
let files = objects.filter(object => !object.name.endsWith('/'))
|
||||
files = files.sort((a, b) => new Date(a.lastModified).getTime() - new Date(b.lastModified).getTime())
|
||||
if (order)
|
||||
files = files.reverse()
|
||||
let folders = objects.filter(object => object.name.endsWith("/"))
|
||||
let files = objects.filter(object => !object.name.endsWith("/"))
|
||||
files = files.sort(
|
||||
(a, b) =>
|
||||
new Date(a.lastModified).getTime() - new Date(b.lastModified).getTime()
|
||||
)
|
||||
if (order) files = files.reverse()
|
||||
return [...folders, ...files]
|
||||
}
|
||||
|
||||
export const pathSlice = (path) => {
|
||||
path = path.replace(minioBrowserPrefix, '')
|
||||
let prefix = ''
|
||||
let bucket = ''
|
||||
if (!path) return {
|
||||
export const pathSlice = path => {
|
||||
path = path.replace(minioBrowserPrefix, "")
|
||||
let prefix = ""
|
||||
let bucket = ""
|
||||
if (!path)
|
||||
return {
|
||||
bucket,
|
||||
prefix
|
||||
}
|
||||
let objectIndex = path.indexOf('/', 1)
|
||||
}
|
||||
let objectIndex = path.indexOf("/", 1)
|
||||
if (objectIndex == -1) {
|
||||
bucket = path.slice(1)
|
||||
return {
|
||||
@@ -79,7 +81,29 @@ export const pathSlice = (path) => {
|
||||
}
|
||||
|
||||
export const pathJoin = (bucket, prefix) => {
|
||||
if (!prefix)
|
||||
prefix = ''
|
||||
return minioBrowserPrefix + '/' + bucket + '/' + prefix
|
||||
if (!prefix) prefix = ""
|
||||
return minioBrowserPrefix + "/" + bucket + "/" + prefix
|
||||
}
|
||||
|
||||
export const getRandomAccessKey = () => {
|
||||
const alphaNumericTable = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
let arr = new Uint8Array(20)
|
||||
window.crypto.getRandomValues(arr)
|
||||
const random = Array.prototype.map.call(arr, v => {
|
||||
const i = v % alphaNumericTable.length
|
||||
return alphaNumericTable.charAt(i)
|
||||
})
|
||||
return random.join("")
|
||||
}
|
||||
|
||||
export const getRandomSecretKey = () => {
|
||||
let arr = new Uint8Array(40)
|
||||
window.crypto.getRandomValues(arr)
|
||||
const binStr = Array.prototype.map
|
||||
.call(arr, v => {
|
||||
return String.fromCharCode(v)
|
||||
})
|
||||
.join("")
|
||||
const base64Str = btoa(binStr)
|
||||
return base64Str.replace(/\//g, "+").substr(0, 40)
|
||||
}
|
||||
|
||||
@@ -72,6 +72,9 @@ class Web {
|
||||
Logout() {
|
||||
storage.removeItem('token')
|
||||
}
|
||||
GetToken() {
|
||||
return storage.getItem('token')
|
||||
}
|
||||
ServerInfo() {
|
||||
return this.makeCall('ServerInfo')
|
||||
}
|
||||
@@ -99,12 +102,6 @@ class Web {
|
||||
RemoveObject(args) {
|
||||
return this.makeCall('RemoveObject', args)
|
||||
}
|
||||
GetAuth() {
|
||||
return this.makeCall('GetAuth')
|
||||
}
|
||||
GenerateAuth() {
|
||||
return this.makeCall('GenerateAuth')
|
||||
}
|
||||
SetAuth(args) {
|
||||
return this.makeCall('SetAuth', args)
|
||||
.then(res => {
|
||||
|
||||
@@ -190,8 +190,8 @@
|
||||
----------------------------*/
|
||||
.toggle-password {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
right: 35px;
|
||||
bottom: 0 ;
|
||||
right: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid #eee;
|
||||
@@ -206,6 +206,10 @@
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.has-toggle-password {
|
||||
position: relative;
|
||||
}
|
||||
//--------------------------
|
||||
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"humanize": "0.0.9",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"local-storage-fallback": "^4.0.2",
|
||||
"material-design-iconic-font": "^2.2.0",
|
||||
"mime-db": "^1.25.0",
|
||||
|
||||
+31
-31
File diff suppressed because one or more lines are too long
+24
-16
@@ -3364,9 +3364,9 @@ fstream-ignore@^1.0.5:
|
||||
minimatch "^3.0.0"
|
||||
|
||||
fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
|
||||
integrity sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=
|
||||
version "1.0.12"
|
||||
resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"
|
||||
integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==
|
||||
dependencies:
|
||||
graceful-fs "^4.1.2"
|
||||
inherits "~2.0.0"
|
||||
@@ -3482,7 +3482,7 @@ glob@^7.0.0, glob@^7.1.1, glob@^7.1.2:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.0.3, glob@^7.0.5, glob@^7.1.3:
|
||||
glob@^7.0.3, glob@^7.0.5:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
|
||||
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
|
||||
@@ -3494,6 +3494,18 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.3:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.1.3:
|
||||
version "7.1.4"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
|
||||
integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
global-modules-path@^2.3.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/global-modules-path/-/global-modules-path-2.3.1.tgz#e541f4c800a1a8514a990477b267ac67525b9931"
|
||||
@@ -3546,11 +3558,7 @@ globby@^7.1.1:
|
||||
pify "^3.0.0"
|
||||
slash "^1.0.0"
|
||||
|
||||
graceful-fs@^4.1.11, graceful-fs@^4.1.4:
|
||||
version "4.1.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
|
||||
|
||||
graceful-fs@^4.1.15, graceful-fs@^4.1.2:
|
||||
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.4:
|
||||
version "4.1.15"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
|
||||
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
|
||||
@@ -4907,6 +4915,11 @@ jsprim@^1.2.2:
|
||||
json-schema "0.2.3"
|
||||
verror "1.10.0"
|
||||
|
||||
jwt-decode@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
|
||||
integrity sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=
|
||||
|
||||
keycode@^2.1.2:
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa"
|
||||
@@ -5965,6 +5978,7 @@ on-headers@~1.0.1:
|
||||
once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
@@ -7392,19 +7406,13 @@ right-align@^0.1.1:
|
||||
dependencies:
|
||||
align-text "^0.1.1"
|
||||
|
||||
rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1, rimraf@^2.6.2:
|
||||
rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
|
||||
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@^2.5.4:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
|
||||
dependencies:
|
||||
glob "^7.0.5"
|
||||
|
||||
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@@ -100,6 +99,8 @@ var (
|
||||
"enable": false,
|
||||
"brokers": null,
|
||||
"topic": "",
|
||||
"queueDir": "",
|
||||
"queueLimit": 0,
|
||||
"tls": {
|
||||
"enable": false,
|
||||
"skipVerify": false,
|
||||
@@ -298,60 +299,6 @@ func (atb *adminXLTestBed) TearDown() {
|
||||
resetTestGlobals()
|
||||
}
|
||||
|
||||
func (atb *adminXLTestBed) GenerateHealTestData(t *testing.T) {
|
||||
// Create an object myobject under bucket mybucket.
|
||||
bucketName := "mybucket"
|
||||
err := atb.objLayer.MakeBucketWithLocation(context.Background(), bucketName, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make bucket %s - %v", bucketName,
|
||||
err)
|
||||
}
|
||||
|
||||
// create some objects
|
||||
{
|
||||
objName := "myobject"
|
||||
for i := 0; i < 10; i++ {
|
||||
objectName := fmt.Sprintf("%s-%d", objName, i)
|
||||
_, err = atb.objLayer.PutObject(context.Background(), bucketName, objectName,
|
||||
mustGetPutObjReader(t, bytes.NewReader([]byte("hello")),
|
||||
int64(len("hello")), "", ""), ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create %s - %v", objectName,
|
||||
err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create a multipart upload (incomplete)
|
||||
{
|
||||
objName := "mpObject"
|
||||
uploadID, err := atb.objLayer.NewMultipartUpload(context.Background(), bucketName,
|
||||
objName, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("mp new error: %v", err)
|
||||
}
|
||||
|
||||
_, err = atb.objLayer.PutObjectPart(context.Background(), bucketName, objName,
|
||||
uploadID, 3, mustGetPutObjReader(t, bytes.NewReader(
|
||||
[]byte("hello")), int64(len("hello")), "", ""), ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("mp put error: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (atb *adminXLTestBed) CleanupHealTestData(t *testing.T) {
|
||||
bucketName := "mybucket"
|
||||
objName := "myobject"
|
||||
for i := 0; i < 10; i++ {
|
||||
atb.objLayer.DeleteObject(context.Background(), bucketName,
|
||||
fmt.Sprintf("%s-%d", objName, i))
|
||||
}
|
||||
|
||||
atb.objLayer.DeleteBucket(context.Background(), bucketName)
|
||||
}
|
||||
|
||||
// initTestObjLayer - Helper function to initialize an XL-based object
|
||||
// layer and set globalObjectAPI.
|
||||
func initTestXLObjLayer() (ObjectLayer, []string, error) {
|
||||
|
||||
+4
-14
@@ -27,7 +27,7 @@ import (
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
"google.golang.org/api/googleapi"
|
||||
|
||||
minio "github.com/minio/minio-go"
|
||||
minio "github.com/minio/minio-go/v6"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
@@ -879,16 +879,6 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Object name already exists as a directory.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrReadQuorum: {
|
||||
Code: "XMinioReadQuorum",
|
||||
Description: "Multiple disk failures, unable to reconstruct data.",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrWriteQuorum: {
|
||||
Code: "XMinioWriteQuorum",
|
||||
Description: "Multiple disks failures, unable to write data.",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrInvalidObjectName: {
|
||||
Code: "XMinioInvalidObjectName",
|
||||
Description: "Object name contains unsupported characters.",
|
||||
@@ -1534,7 +1524,7 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrKMSAuthFailure
|
||||
case errOperationTimedOut, context.Canceled, context.DeadlineExceeded:
|
||||
apiErr = ErrOperationTimedOut
|
||||
case errNetworkConnReset:
|
||||
case errDiskNotFound:
|
||||
apiErr = ErrSlowDown
|
||||
}
|
||||
|
||||
@@ -1593,9 +1583,9 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
case InvalidPart:
|
||||
apiErr = ErrInvalidPart
|
||||
case InsufficientWriteQuorum:
|
||||
apiErr = ErrWriteQuorum
|
||||
apiErr = ErrSlowDown
|
||||
case InsufficientReadQuorum:
|
||||
apiErr = ErrReadQuorum
|
||||
apiErr = ErrSlowDown
|
||||
case UnsupportedDelimiter:
|
||||
apiErr = ErrNotImplemented
|
||||
case InvalidMarkerPrefixCombination:
|
||||
|
||||
@@ -39,8 +39,8 @@ var toAPIErrorTests = []struct {
|
||||
{err: ObjectNameInvalid{}, errCode: ErrInvalidObjectName},
|
||||
{err: InvalidUploadID{}, errCode: ErrNoSuchUpload},
|
||||
{err: InvalidPart{}, errCode: ErrInvalidPart},
|
||||
{err: InsufficientReadQuorum{}, errCode: ErrReadQuorum},
|
||||
{err: InsufficientWriteQuorum{}, errCode: ErrWriteQuorum},
|
||||
{err: InsufficientReadQuorum{}, errCode: ErrSlowDown},
|
||||
{err: InsufficientWriteQuorum{}, errCode: ErrSlowDown},
|
||||
{err: UnsupportedDelimiter{}, errCode: ErrNotImplemented},
|
||||
{err: InvalidMarkerPrefixCombination{}, errCode: ErrNotImplemented},
|
||||
{err: InvalidUploadIDKeyCombination{}, errCode: ErrNotImplemented},
|
||||
|
||||
+2
-8
@@ -17,7 +17,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -46,13 +45,8 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled bool) {
|
||||
apiRouter := router.PathPrefix("/").Subrouter()
|
||||
var routers []*mux.Router
|
||||
for _, domainName := range globalDomainNames {
|
||||
var hostPort string
|
||||
if globalMinioPort != "443" && globalMinioPort != "80" {
|
||||
hostPort = net.JoinHostPort(domainName, globalMinioPort)
|
||||
} else {
|
||||
hostPort = domainName
|
||||
}
|
||||
routers = append(routers, apiRouter.Host("{bucket:.+}."+hostPort).Subrouter())
|
||||
routers = append(routers, apiRouter.Host("{bucket:.+}."+domainName).Subrouter())
|
||||
routers = append(routers, apiRouter.Host("{bucket:.+}."+domainName+":{port:.*}").Subrouter())
|
||||
}
|
||||
routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter())
|
||||
|
||||
|
||||
+1
-1
@@ -362,7 +362,7 @@ func isReqAuthenticated(ctx context.Context, r *http.Request, region string, sty
|
||||
|
||||
// Verify 'Content-Md5' and/or 'X-Amz-Content-Sha256' if present.
|
||||
// The verification happens implicit during reading.
|
||||
reader, err := hash.NewReader(r.Body, -1, hex.EncodeToString(contentMD5), hex.EncodeToString(contentSHA256), -1)
|
||||
reader, err := hash.NewReader(r.Body, -1, hex.EncodeToString(contentMD5), hex.EncodeToString(contentSHA256), -1, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
return toAPIErrorCode(ctx, err)
|
||||
}
|
||||
|
||||
+32
-8
@@ -30,7 +30,7 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
@@ -309,12 +309,19 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
deleteObject := objectAPI.DeleteObject
|
||||
deleteObjectsFn := objectAPI.DeleteObjects
|
||||
if api.CacheAPI() != nil {
|
||||
deleteObject = api.CacheAPI().DeleteObject
|
||||
deleteObjectsFn = api.CacheAPI().DeleteObjects
|
||||
}
|
||||
|
||||
type delObj struct {
|
||||
origIndex int
|
||||
name string
|
||||
}
|
||||
|
||||
var objectsToDelete []delObj
|
||||
var dErrs = make([]APIErrorCode, len(deleteObjects.Objects))
|
||||
|
||||
for index, object := range deleteObjects.Objects {
|
||||
if dErrs[index] = checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, object.ObjectName); dErrs[index] != ErrNone {
|
||||
if dErrs[index] == ErrSignatureDoesNotMatch || dErrs[index] == ErrInvalidAccessKeyID {
|
||||
@@ -323,10 +330,26 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
}
|
||||
continue
|
||||
}
|
||||
err := deleteObject(ctx, bucket, object.ObjectName)
|
||||
if err != nil {
|
||||
dErrs[index] = toAPIErrorCode(ctx, err)
|
||||
|
||||
objectsToDelete = append(objectsToDelete, delObj{index, object.ObjectName})
|
||||
}
|
||||
|
||||
toNames := func(input []delObj) (output []string) {
|
||||
output = make([]string, len(input))
|
||||
for i := range input {
|
||||
output[i] = input[i].name
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
errs, err := deleteObjectsFn(ctx, bucket, toNames(objectsToDelete))
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
for i, obj := range objectsToDelete {
|
||||
dErrs[obj.origIndex] = toAPIErrorCode(ctx, errs[i])
|
||||
}
|
||||
|
||||
// Collect deleted objects and errors if any.
|
||||
@@ -600,7 +623,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
hashReader, err := hash.NewReader(fileBody, fileSize, "", "", fileSize)
|
||||
hashReader, err := hash.NewReader(fileBody, fileSize, "", "", fileSize, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
@@ -638,7 +661,8 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
info := ObjectInfo{Size: fileSize}
|
||||
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", fileSize) // do not try to verify encrypted content
|
||||
// do not try to verify encrypted content
|
||||
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", fileSize, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
|
||||
@@ -762,7 +762,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
// Assert the response code with the expected status.
|
||||
if rec.Code != testCase.expectedRespStatus {
|
||||
t.Errorf("Case %d: MinIO %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
|
||||
t.Errorf("Test %d: MinIO %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
|
||||
}
|
||||
|
||||
// read the response body.
|
||||
|
||||
+4
-1
@@ -29,7 +29,7 @@ import (
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
dns2 "github.com/miekg/dns"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/cmd/logger/target/console"
|
||||
"github.com/minio/minio/cmd/logger/target/http"
|
||||
@@ -161,6 +161,9 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
||||
globalCertsCADir = &ConfigDir{path: filepath.Join(globalCertsDir.Get(), certsCADir)}
|
||||
|
||||
logger.FatalIf(mkdirAllIgnorePerm(globalCertsCADir.Get()), "Unable to create certs CA directory at %s", globalCertsCADir.Get())
|
||||
|
||||
// Check "compat" flag from command line argument.
|
||||
globalCLIContext.StrictS3Compat = ctx.IsSet("compat") || ctx.GlobalIsSet("compat")
|
||||
}
|
||||
|
||||
// Parses the given compression exclude list `extensions` or `content-types`.
|
||||
|
||||
@@ -74,7 +74,7 @@ func saveConfigEtcd(ctx context.Context, client *etcd.Client, configFile string,
|
||||
}
|
||||
|
||||
func saveConfig(ctx context.Context, objAPI ObjectLayer, configFile string, data []byte) error {
|
||||
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data), int64(len(data)))
|
||||
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data), int64(len(data)), globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -107,7 +107,7 @@ func readConfigEtcd(ctx context.Context, client *etcd.Client, configFile string)
|
||||
// watchConfigEtcd - watches for changes on `configFile` on etcd and loads them.
|
||||
func watchConfigEtcd(objAPI ObjectLayer, configFile string, loadCfgFn func(ObjectLayer) error) {
|
||||
for {
|
||||
watchCh := globalEtcdClient.Watch(context.Background(), iamConfigPrefix)
|
||||
watchCh := globalEtcdClient.Watch(context.Background(), configFile)
|
||||
select {
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
|
||||
@@ -325,7 +325,7 @@ func (s *serverConfig) TestNotificationTargets() error {
|
||||
if !v.Enable {
|
||||
continue
|
||||
}
|
||||
t, err := target.NewKafkaTarget(k, v)
|
||||
t, err := target.NewKafkaTarget(k, v, GlobalServiceDoneCh)
|
||||
if err != nil {
|
||||
return fmt.Errorf("kafka(%s): %s", k, err.Error())
|
||||
}
|
||||
@@ -667,7 +667,7 @@ func getNotificationTargets(config *serverConfig) *event.TargetList {
|
||||
|
||||
for id, args := range config.Notify.Kafka {
|
||||
if args.Enable {
|
||||
newTarget, err := target.NewKafkaTarget(id, args)
|
||||
newTarget, err := target.NewKafkaTarget(id, args, GlobalServiceDoneCh)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
|
||||
@@ -200,7 +200,7 @@ func TestValidateConfig(t *testing.T) {
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, false},
|
||||
|
||||
// Test 16 - Test Kafka
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "kafka": { "1": { "enable": true, "brokers": null, "topic": "" } }}}`, false},
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "kafka": { "1": { "enable": true, "brokers": null, "topic": "", "queueDir": "", "queueLimit": 0 } }}}`, false},
|
||||
|
||||
// Test 17 - Test Webhook
|
||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "webhook": { "1": { "enable": true, "endpoint": "" } }}}`, false},
|
||||
|
||||
@@ -884,7 +884,7 @@ type serverConfigV32 struct {
|
||||
} `json:"policy"`
|
||||
}
|
||||
|
||||
// serverConfigV33 is just like version '32', removes clientID from NATS and MQTT, and adds queueDir, queueLimit with MQTT.
|
||||
// serverConfigV33 is just like version '32', removes clientID from NATS and MQTT, and adds queueDir, queueLimit in MQTT and kafka.
|
||||
type serverConfigV33 struct {
|
||||
quick.Config `json:"-"` // ignore interfaces
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -399,7 +398,7 @@ func (cfs *cacheFSObjects) PutObject(ctx context.Context, bucket string, object
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
if fsMeta.Meta["etag"] == "" {
|
||||
fsMeta.Meta["etag"] = hex.EncodeToString(data.MD5Current())
|
||||
fsMeta.Meta["etag"] = r.MD5CurrentHexString()
|
||||
}
|
||||
// Should return IncompleteBody{} error when reader has fewer
|
||||
// bytes than specified in request header.
|
||||
|
||||
+24
-6
@@ -62,6 +62,7 @@ type cacheObjects struct {
|
||||
GetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
PutObjectFn func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
DeleteObjectFn func(ctx context.Context, bucket, object string) error
|
||||
DeleteObjectsFn func(ctx context.Context, bucket string, objects []string) ([]error, 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)
|
||||
@@ -94,6 +95,7 @@ type CacheObjectLayer interface {
|
||||
GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
PutObject(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
DeleteObject(ctx context.Context, bucket, object string) error
|
||||
DeleteObjects(ctx context.Context, bucket string, objects []string) ([]error, error)
|
||||
|
||||
// Multipart operations.
|
||||
NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (uploadID string, err error)
|
||||
@@ -243,7 +245,7 @@ func (c cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string,
|
||||
// Initialize pipe.
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
teeReader := io.TeeReader(bkReader, pipeWriter)
|
||||
hashReader, herr := hash.NewReader(pipeReader, bkReader.ObjInfo.Size, "", "", bkReader.ObjInfo.Size)
|
||||
hashReader, herr := hash.NewReader(pipeReader, bkReader.ObjInfo.Size, "", "", bkReader.ObjInfo.Size, globalCLIContext.StrictS3Compat)
|
||||
if herr != nil {
|
||||
bkReader.Close()
|
||||
return nil, herr
|
||||
@@ -314,7 +316,7 @@ func (c cacheObjects) GetObject(ctx context.Context, bucket, object string, star
|
||||
}
|
||||
// Initialize pipe.
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
hashReader, err := hash.NewReader(pipeReader, objInfo.Size, "", "", objInfo.Size)
|
||||
hashReader, err := hash.NewReader(pipeReader, objInfo.Size, "", "", objInfo.Size, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -629,6 +631,14 @@ func (c cacheObjects) DeleteObject(ctx context.Context, bucket, object string) (
|
||||
return
|
||||
}
|
||||
|
||||
func (c cacheObjects) DeleteObjects(ctx context.Context, bucket string, objects []string) ([]error, error) {
|
||||
errs := make([]error, len(objects))
|
||||
for idx, object := range objects {
|
||||
errs[idx] = c.DeleteObject(ctx, bucket, object)
|
||||
}
|
||||
return errs, nil
|
||||
}
|
||||
|
||||
// Returns true if object should be excluded from cache
|
||||
func (c cacheObjects) isCacheExclude(bucket, object string) bool {
|
||||
for _, pattern := range c.exclude {
|
||||
@@ -664,13 +674,13 @@ func (c cacheObjects) PutObject(ctx context.Context, bucket, object string, r *P
|
||||
objInfo = ObjectInfo{}
|
||||
// Initialize pipe to stream data to backend
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
hashReader, err := hash.NewReader(pipeReader, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize())
|
||||
hashReader, err := hash.NewReader(pipeReader, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize(), globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
// Initialize pipe to stream data to cache
|
||||
rPipe, wPipe := io.Pipe()
|
||||
cHashReader, err := hash.NewReader(rPipe, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize())
|
||||
cHashReader, err := hash.NewReader(rPipe, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize(), globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
@@ -758,13 +768,13 @@ func (c cacheObjects) PutObjectPart(ctx context.Context, bucket, object, uploadI
|
||||
info = PartInfo{}
|
||||
// Initialize pipe to stream data to backend
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
hashReader, err := hash.NewReader(pipeReader, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize())
|
||||
hashReader, err := hash.NewReader(pipeReader, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize(), globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Initialize pipe to stream data to cache
|
||||
rPipe, wPipe := io.Pipe()
|
||||
cHashReader, err := hash.NewReader(rPipe, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize())
|
||||
cHashReader, err := hash.NewReader(rPipe, size, data.MD5HexString(), data.SHA256HexString(), data.ActualSize(), globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -974,6 +984,14 @@ func newServerCacheObjects(config CacheConfig) (CacheObjectLayer, error) {
|
||||
DeleteObjectFn: func(ctx context.Context, bucket, object string) error {
|
||||
return newObjectLayerFn().DeleteObject(ctx, bucket, object)
|
||||
},
|
||||
DeleteObjectsFn: func(ctx context.Context, bucket string, objects []string) ([]error, error) {
|
||||
errs := make([]error, len(objects))
|
||||
for idx, object := range objects {
|
||||
errs[idx] = newObjectLayerFn().DeleteObject(ctx, bucket, object)
|
||||
}
|
||||
return errs, nil
|
||||
},
|
||||
|
||||
ListObjectsFn: func(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (result ListObjectsInfo, err error) {
|
||||
return newObjectLayerFn().ListObjects(ctx, bucket, prefix, marker, delimiter, maxKeys)
|
||||
},
|
||||
|
||||
@@ -194,7 +194,7 @@ func TestDiskCache(t *testing.T) {
|
||||
objInfo.UserDefined = httpMeta
|
||||
var opts ObjectOptions
|
||||
byteReader := bytes.NewReader([]byte(content))
|
||||
hashReader, err := hash.NewReader(byteReader, int64(size), "", "", int64(size))
|
||||
hashReader, err := hash.NewReader(byteReader, int64(size), "", "", int64(size), globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -269,7 +269,7 @@ func TestDiskCacheMaxUse(t *testing.T) {
|
||||
opts := ObjectOptions{}
|
||||
|
||||
byteReader := bytes.NewReader([]byte(content))
|
||||
hashReader, err := hash.NewReader(byteReader, int64(size), "", "", int64(size))
|
||||
hashReader, err := hash.NewReader(byteReader, int64(size), "", "", int64(size), globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
"github.com/minio/minio-go/v6/pkg/encrypt"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/ioutil"
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"testing"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
"github.com/minio/minio-go/v6/pkg/encrypt"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/sio"
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/pkg/ellipses"
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ import (
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/cpu"
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
|
||||
@@ -504,10 +504,7 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
}
|
||||
|
||||
// Calculate s3 compatible md5sum for complete multipart.
|
||||
s3MD5, err := getCompleteMultipartMD5(ctx, parts)
|
||||
if err != nil {
|
||||
return oi, err
|
||||
}
|
||||
s3MD5 := getCompleteMultipartMD5(parts)
|
||||
|
||||
partSize := int64(-1) // Used later to ensure that all parts sizes are same.
|
||||
|
||||
|
||||
+17
-5
@@ -30,7 +30,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/lock"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
@@ -450,6 +450,11 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
|
||||
// Return the new object info.
|
||||
return fsMeta.ToObjectInfo(srcBucket, srcObject, fi), nil
|
||||
}
|
||||
|
||||
if err := checkPutObjectArgs(ctx, dstBucket, dstObject, fs, srcInfo.PutObjReader.Size()); err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
|
||||
objInfo, err := fs.putObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, ObjectOptions{ServerSideEncryption: dstOpts.ServerSideEncryption, UserDefined: srcInfo.UserDefined})
|
||||
if err != nil {
|
||||
return oi, toObjectErr(err, dstBucket, dstObject)
|
||||
@@ -813,6 +818,7 @@ func (fs *FSObjects) PutObject(ctx context.Context, bucket string, object string
|
||||
return objInfo, err
|
||||
}
|
||||
defer objectLock.Unlock()
|
||||
|
||||
return fs.putObject(ctx, bucket, object, r, opts)
|
||||
}
|
||||
|
||||
@@ -854,10 +860,6 @@ func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string
|
||||
return fsMeta.ToObjectInfo(bucket, object, fi), nil
|
||||
}
|
||||
|
||||
if err = checkPutObjectArgs(ctx, bucket, object, fs, data.Size()); err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
|
||||
// Check if an object is present as one of the parent dir.
|
||||
if fs.parentDirIsObject(ctx, bucket, path.Dir(object)) {
|
||||
return ObjectInfo{}, toObjectErr(errFileParentIsFile, bucket, object)
|
||||
@@ -951,6 +953,16 @@ func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string
|
||||
return fsMeta.ToObjectInfo(bucket, object, fi), nil
|
||||
}
|
||||
|
||||
// DeleteObjects - deletes an object from a bucket, this operation is destructive
|
||||
// and there are no rollbacks supported.
|
||||
func (fs *FSObjects) DeleteObjects(ctx context.Context, bucket string, objects []string) ([]error, error) {
|
||||
errs := make([]error, len(objects))
|
||||
for idx, object := range objects {
|
||||
errs[idx] = fs.DeleteObject(ctx, bucket, object)
|
||||
}
|
||||
return errs, nil
|
||||
}
|
||||
|
||||
// DeleteObject - deletes an object from a bucket, this operation is destructive
|
||||
// and there are no rollbacks supported.
|
||||
func (fs *FSObjects) DeleteObject(ctx context.Context, bucket, object string) error {
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -25,7 +24,7 @@ import (
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
|
||||
minio "github.com/minio/minio-go"
|
||||
minio "github.com/minio/minio-go/v6"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -49,9 +48,6 @@ var (
|
||||
|
||||
// IsStringEqual is string equal.
|
||||
IsStringEqual = isStringEqual
|
||||
|
||||
// GetCompleteMultipartMD5 returns multipart MD5
|
||||
GetCompleteMultipartMD5 = getCompleteMultipartMD5
|
||||
)
|
||||
|
||||
// StatInfo - alias for statInfo
|
||||
@@ -351,8 +347,8 @@ func ErrorRespToObjectError(err error, params ...string) error {
|
||||
}
|
||||
|
||||
// ComputeCompleteMultipartMD5 calculates MD5 ETag for complete multipart responses
|
||||
func ComputeCompleteMultipartMD5(parts []CompletePart) (string, error) {
|
||||
return getCompleteMultipartMD5(context.Background(), parts)
|
||||
func ComputeCompleteMultipartMD5(parts []CompletePart) string {
|
||||
return getCompleteMultipartMD5(parts)
|
||||
}
|
||||
|
||||
// parse gateway sse env variable
|
||||
|
||||
@@ -221,6 +221,11 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
logger.FatalIf(err, "Unable to initialize gateway backend")
|
||||
}
|
||||
|
||||
// Populate existing buckets to the etcd backend
|
||||
if globalDNSConfig != nil {
|
||||
initFederatorBackend(newObject)
|
||||
}
|
||||
|
||||
if enableConfigOps {
|
||||
// Create a new config system.
|
||||
globalConfigSys = NewConfigSys()
|
||||
@@ -299,6 +304,9 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
printGatewayStartupMessage(getAPIEndpoints(), gatewayName)
|
||||
}
|
||||
|
||||
// Set when gateway is enabled
|
||||
globalIsGateway = true
|
||||
|
||||
// Set uptime time after object layer has initialized.
|
||||
globalBootTime = UTCNow()
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import (
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/cli"
|
||||
miniogopolicy "github.com/minio/minio-go/pkg/policy"
|
||||
miniogopolicy "github.com/minio/minio-go/v6/pkg/policy"
|
||||
"github.com/minio/minio/cmd"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
@@ -90,23 +90,23 @@ ENVIRONMENT VARIABLES:
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for Azure Blob Storage backend.
|
||||
$ export MINIO_ACCESS_KEY=azureaccountname
|
||||
$ export MINIO_SECRET_KEY=azureaccountkey
|
||||
$ {{.HelpName}}
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}azureaccountname
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}azureaccountkey
|
||||
{{.Prompt}} {{.HelpName}}
|
||||
|
||||
2. Start minio gateway server for Azure Blob Storage backend on custom endpoint.
|
||||
$ export MINIO_ACCESS_KEY=azureaccountname
|
||||
$ export MINIO_SECRET_KEY=azureaccountkey
|
||||
$ {{.HelpName}} https://azureaccountname.blob.custom.azure.endpoint
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}azureaccountname
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}azureaccountkey
|
||||
{{.Prompt}} {{.HelpName}} https://azureaccountname.blob.custom.azure.endpoint
|
||||
|
||||
3. Start minio gateway server for Azure Blob Storage backend with edge caching enabled.
|
||||
$ export MINIO_ACCESS_KEY=azureaccountname
|
||||
$ export MINIO_SECRET_KEY=azureaccountkey
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}}
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}azureaccountname
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}azureaccountkey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_DRIVES{{.AssignmentOperator}}"/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXCLUDE{{.AssignmentOperator}}"bucket1/*;*.png"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXPIRY{{.AssignmentOperator}}40
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_MAXUSE{{.AssignmentOperator}}80
|
||||
{{.Prompt}} {{.HelpName}}
|
||||
`
|
||||
|
||||
minio.RegisterGatewayCommand(cli.Command{
|
||||
@@ -835,7 +835,7 @@ func (a *azureObjects) PutObject(ctx context.Context, bucket, object string, r *
|
||||
}
|
||||
|
||||
// Save md5sum for future processing on the object.
|
||||
opts.UserDefined["x-amz-meta-md5sum"] = hex.EncodeToString(data.MD5Current())
|
||||
opts.UserDefined["x-amz-meta-md5sum"] = r.MD5CurrentHexString()
|
||||
objBlob.Metadata, objBlob.Properties, err = s3MetaToAzureProperties(ctx, opts.UserDefined)
|
||||
if err != nil {
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
@@ -896,6 +896,14 @@ func (a *azureObjects) DeleteObject(ctx context.Context, bucket, object string)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *azureObjects) DeleteObjects(ctx context.Context, bucket string, objects []string) ([]error, error) {
|
||||
errs := make([]error, len(objects))
|
||||
for idx, object := range objects {
|
||||
errs[idx] = a.DeleteObject(ctx, bucket, object)
|
||||
}
|
||||
return errs, nil
|
||||
}
|
||||
|
||||
// ListMultipartUploads - It's decided not to support List Multipart Uploads, hence returning empty result.
|
||||
func (a *azureObjects) ListMultipartUploads(ctx context.Context, bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (result minio.ListMultipartsInfo, err error) {
|
||||
// It's decided not to support List Multipart Uploads, hence returning empty result.
|
||||
@@ -1207,10 +1215,7 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
|
||||
if err != nil {
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
objBlob.Metadata["md5sum"], err = cmd.ComputeCompleteMultipartMD5(uploadedParts)
|
||||
if err != nil {
|
||||
return objInfo, err
|
||||
}
|
||||
objBlob.Metadata["md5sum"] = cmd.ComputeCompleteMultipartMD5(uploadedParts)
|
||||
err = objBlob.SetProperties(nil)
|
||||
if err != nil {
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
|
||||
b2 "github.com/minio/blazer/base"
|
||||
"github.com/minio/cli"
|
||||
miniogopolicy "github.com/minio/minio-go/pkg/policy"
|
||||
miniogopolicy "github.com/minio/minio-go/v6/pkg/policy"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
h2 "github.com/minio/minio/pkg/hash"
|
||||
@@ -77,18 +77,18 @@ ENVIRONMENT VARIABLES:
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for B2 backend.
|
||||
$ export MINIO_ACCESS_KEY=accountID
|
||||
$ export MINIO_SECRET_KEY=applicationKey
|
||||
$ {{.HelpName}}
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}accountID
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}applicationKey
|
||||
{{.Prompt}} {{.HelpName}}
|
||||
|
||||
2. Start minio gateway server for B2 backend with edge caching enabled.
|
||||
$ export MINIO_ACCESS_KEY=accountID
|
||||
$ export MINIO_SECRET_KEY=applicationKey
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}}
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}accountID
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}applicationKey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_DRIVES{{.AssignmentOperator}}"/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXCLUDE{{.AssignmentOperator}}"bucket1/*;*.png"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXPIRY{{.AssignmentOperator}}40
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_MAXUSE{{.AssignmentOperator}}80
|
||||
{{.Prompt}} {{.HelpName}}
|
||||
`
|
||||
minio.RegisterGatewayCommand(cli.Command{
|
||||
Name: b2Backend,
|
||||
@@ -595,6 +595,14 @@ func (l *b2Objects) DeleteObject(ctx context.Context, bucket string, object stri
|
||||
return b2ToObjectError(err, bucket, object)
|
||||
}
|
||||
|
||||
func (l *b2Objects) DeleteObjects(ctx context.Context, bucket string, objects []string) ([]error, error) {
|
||||
errs := make([]error, len(objects))
|
||||
for idx, object := range objects {
|
||||
errs[idx] = l.DeleteObject(ctx, bucket, object)
|
||||
}
|
||||
return errs, nil
|
||||
}
|
||||
|
||||
// ListMultipartUploads lists all multipart uploads.
|
||||
func (l *b2Objects) ListMultipartUploads(ctx context.Context, bucket string, prefix string, keyMarker string, uploadIDMarker string,
|
||||
delimiter string, maxUploads int) (lmi minio.ListMultipartsInfo, err error) {
|
||||
|
||||
@@ -19,6 +19,7 @@ package gcs
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -37,7 +38,7 @@ import (
|
||||
"cloud.google.com/go/storage"
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/cli"
|
||||
miniogopolicy "github.com/minio/minio-go/pkg/policy"
|
||||
miniogopolicy "github.com/minio/minio-go/v6/pkg/policy"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
@@ -128,21 +129,21 @@ ENVIRONMENT VARIABLES:
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for GCS backend.
|
||||
$ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} GOOGLE_APPLICATION_CREDENTIALS{{.AssignmentOperator}}/path/to/credentials.json
|
||||
(Instructions to generate credentials : https://developers.google.com/identity/protocols/application-default-credentials)
|
||||
$ export MINIO_ACCESS_KEY=accesskey
|
||||
$ export MINIO_SECRET_KEY=secretkey
|
||||
$ {{.HelpName}} mygcsprojectid
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}accesskey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}secretkey
|
||||
{{.Prompt}} {{.HelpName}} mygcsprojectid
|
||||
|
||||
2. Start minio gateway server for GCS backend with edge caching enabled.
|
||||
$ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
|
||||
$ export MINIO_ACCESS_KEY=accesskey
|
||||
$ export MINIO_SECRET_KEY=secretkey
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}} mygcsprojectid
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} GOOGLE_APPLICATION_CREDENTIALS{{.AssignmentOperator}}/path/to/credentials.json
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}accesskey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}secretkey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_DRIVES{{.AssignmentOperator}}"/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXCLUDE{{.AssignmentOperator}}"bucket1/*;*.png"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXPIRY{{.AssignmentOperator}}40
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_MAXUSE{{.AssignmentOperator}}80
|
||||
{{.Prompt}} {{.HelpName}} mygcsprojectid
|
||||
`
|
||||
|
||||
minio.RegisterGatewayCommand(cli.Command{
|
||||
@@ -830,12 +831,17 @@ func fromGCSAttrsToObjectInfo(attrs *storage.ObjectAttrs) minio.ObjectInfo {
|
||||
if attrs.ContentLanguage != "" {
|
||||
metadata["Content-Language"] = attrs.ContentLanguage
|
||||
}
|
||||
|
||||
etag := hex.EncodeToString(attrs.MD5)
|
||||
if etag == "" {
|
||||
etag = minio.ToS3ETag(fmt.Sprintf("%d", attrs.CRC32C))
|
||||
}
|
||||
return minio.ObjectInfo{
|
||||
Name: attrs.Name,
|
||||
Bucket: attrs.Bucket,
|
||||
ModTime: attrs.Updated,
|
||||
Size: attrs.Size,
|
||||
ETag: minio.ToS3ETag(fmt.Sprintf("%d", attrs.CRC32C)),
|
||||
ETag: etag,
|
||||
UserDefined: metadata,
|
||||
ContentType: attrs.ContentType,
|
||||
ContentEncoding: attrs.ContentEncoding,
|
||||
@@ -955,6 +961,14 @@ func (l *gcsGateway) DeleteObject(ctx context.Context, bucket string, object str
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *gcsGateway) DeleteObjects(ctx context.Context, bucket string, objects []string) ([]error, error) {
|
||||
errs := make([]error, len(objects))
|
||||
for idx, object := range objects {
|
||||
errs[idx] = l.DeleteObject(ctx, bucket, object)
|
||||
}
|
||||
return errs, nil
|
||||
}
|
||||
|
||||
// NewMultipartUpload - upload object in multiple parts
|
||||
func (l *gcsGateway) NewMultipartUpload(ctx context.Context, bucket string, key string, o minio.ObjectOptions) (uploadID string, err error) {
|
||||
// generate new uploadid
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"cloud.google.com/go/storage"
|
||||
"google.golang.org/api/googleapi"
|
||||
|
||||
miniogo "github.com/minio/minio-go"
|
||||
miniogo "github.com/minio/minio-go/v6"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ package hdfs
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
)
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/hdfs/v3"
|
||||
"github.com/minio/minio-go/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
@@ -76,18 +76,18 @@ ENVIRONMENT VARIABLES:
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for HDFS backend.
|
||||
$ export MINIO_ACCESS_KEY=accesskey
|
||||
$ export MINIO_SECRET_KEY=secretkey
|
||||
$ {{.HelpName}} hdfs://namenode:8200
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}accesskey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}secretkey
|
||||
{{.Prompt}} {{.HelpName}} hdfs://namenode:8200
|
||||
|
||||
2. Start minio gateway server for HDFS with edge caching enabled.
|
||||
$ export MINIO_ACCESS_KEY=accesskey
|
||||
$ export MINIO_SECRET_KEY=secretkey
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}} hdfs://namenode:8200
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}accesskey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}secretkey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_DRIVES{{.AssignmentOperator}}"/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXCLUDE{{.AssignmentOperator}}"bucket1/*;*.png"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXPIRY{{.AssignmentOperator}}40
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_MAXUSE{{.AssignmentOperator}}80
|
||||
{{.Prompt}} {{.HelpName}} hdfs://namenode:8200
|
||||
`
|
||||
|
||||
minio.RegisterGatewayCommand(cli.Command{
|
||||
@@ -400,6 +400,14 @@ func (n *hdfsObjects) DeleteObject(ctx context.Context, bucket, object string) e
|
||||
return hdfsToObjectErr(ctx, n.deleteObject(minio.PathJoin(hdfsSeparator, bucket), minio.PathJoin(hdfsSeparator, bucket, object)), bucket, object)
|
||||
}
|
||||
|
||||
func (n *hdfsObjects) DeleteObjects(ctx context.Context, bucket string, objects []string) ([]error, error) {
|
||||
errs := make([]error, len(objects))
|
||||
for idx, object := range objects {
|
||||
errs[idx] = n.DeleteObject(ctx, bucket, object)
|
||||
}
|
||||
return errs, nil
|
||||
}
|
||||
|
||||
func (n *hdfsObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, opts minio.ObjectOptions) (gr *minio.GetObjectReader, err error) {
|
||||
objInfo, err := n.GetObjectInfo(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
@@ -647,10 +655,7 @@ func (n *hdfsObjects) CompleteMultipartUpload(ctx context.Context, bucket, objec
|
||||
}
|
||||
|
||||
// Calculate s3 compatible md5sum for complete multipart.
|
||||
s3MD5, err := minio.GetCompleteMultipartMD5(ctx, parts)
|
||||
if err != nil {
|
||||
return objInfo, err
|
||||
}
|
||||
s3MD5 := minio.ComputeCompleteMultipartMD5(parts)
|
||||
|
||||
return minio.ObjectInfo{
|
||||
Bucket: bucket,
|
||||
|
||||
@@ -60,18 +60,18 @@ ENVIRONMENT VARIABLES:
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for NAS backend.
|
||||
$ export MINIO_ACCESS_KEY=accesskey
|
||||
$ export MINIO_SECRET_KEY=secretkey
|
||||
$ {{.HelpName}} /shared/nasvol
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}accesskey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}secretkey
|
||||
{{.Prompt}} {{.HelpName}} /shared/nasvol
|
||||
|
||||
2. Start minio gateway server for NAS with edge caching enabled.
|
||||
$ export MINIO_ACCESS_KEY=accesskey
|
||||
$ export MINIO_SECRET_KEY=secretkey
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}} /shared/nasvol
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}accesskey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}secretkey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_DRIVES{{.AssignmentOperator}}"/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXCLUDE{{.AssignmentOperator}}"bucket1/*;*.png"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXPIRY{{.AssignmentOperator}}40
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_MAXUSE{{.AssignmentOperator}}80
|
||||
{{.Prompt}} {{.HelpName}} /shared/nasvol
|
||||
`
|
||||
|
||||
minio.RegisterGatewayCommand(cli.Command{
|
||||
|
||||
@@ -29,8 +29,8 @@ import (
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
|
||||
"github.com/minio/cli"
|
||||
miniogopolicy "github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio-go/pkg/s3utils"
|
||||
miniogopolicy "github.com/minio/minio-go/v6/pkg/policy"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
@@ -79,23 +79,23 @@ ENVIRONMENT VARIABLES:
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for Aliyun OSS backend.
|
||||
$ export MINIO_ACCESS_KEY=accesskey
|
||||
$ export MINIO_SECRET_KEY=secretkey
|
||||
$ {{.HelpName}}
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}accesskey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}secretkey
|
||||
{{.Prompt}} {{.HelpName}}
|
||||
|
||||
2. Start minio gateway server for Aliyun OSS backend on custom endpoint.
|
||||
$ export MINIO_ACCESS_KEY=Q3AM3UQ867SPQQA43P2F
|
||||
$ export MINIO_SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
||||
$ {{.HelpName}} https://oss.example.com
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}Q3AM3UQ867SPQQA43P2F
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
||||
{{.Prompt}} {{.HelpName}} https://oss.example.com
|
||||
|
||||
3. Start minio gateway server for Aliyun OSS backend with edge caching enabled.
|
||||
$ export MINIO_ACCESS_KEY=accesskey
|
||||
$ export MINIO_SECRET_KEY=secretkey
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}}
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}accesskey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}secretkey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_DRIVES{{.AssignmentOperator}}"/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXCLUDE{{.AssignmentOperator}}"bucket1/*;*.png"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXPIRY{{.AssignmentOperator}}40
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_MAXUSE{{.AssignmentOperator}}80
|
||||
{{.Prompt}} {{.HelpName}}
|
||||
`
|
||||
|
||||
minio.RegisterGatewayCommand(cli.Command{
|
||||
@@ -710,6 +710,14 @@ func (l *ossObjects) DeleteObject(ctx context.Context, bucket, object string) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *ossObjects) DeleteObjects(ctx context.Context, bucket string, objects []string) ([]error, error) {
|
||||
errs := make([]error, len(objects))
|
||||
for idx, object := range objects {
|
||||
errs[idx] = l.DeleteObject(ctx, bucket, object)
|
||||
}
|
||||
return errs, nil
|
||||
}
|
||||
|
||||
// fromOSSClientListMultipartsInfo converts oss ListMultipartUploadResult to ListMultipartsInfo
|
||||
func fromOSSClientListMultipartsInfo(lmur oss.ListMultipartUploadResult) minio.ListMultipartsInfo {
|
||||
uploads := make([]minio.MultipartInfo, len(lmur.Uploads))
|
||||
|
||||
@@ -241,7 +241,7 @@ func getGWMetadata(ctx context.Context, bucket, prefix string, gwMeta gwMetaV1)
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, err
|
||||
}
|
||||
hashReader, err := hash.NewReader(bytes.NewReader(metadataBytes), int64(len(metadataBytes)), "", "", int64(len(metadataBytes)))
|
||||
hashReader, err := hash.NewReader(bytes.NewReader(metadataBytes), int64(len(metadataBytes)), "", "", int64(len(metadataBytes)), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
"github.com/minio/minio-go/v6/pkg/encrypt"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
|
||||
@@ -27,12 +27,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/cli"
|
||||
miniogo "github.com/minio/minio-go"
|
||||
"github.com/minio/minio-go/pkg/credentials"
|
||||
miniogo "github.com/minio/minio-go/v6"
|
||||
"github.com/minio/minio-go/v6/pkg/credentials"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
"github.com/minio/minio-go/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v6/pkg/encrypt"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
@@ -77,38 +77,38 @@ ENVIRONMENT VARIABLES:
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for AWS S3 backend.
|
||||
$ export MINIO_ACCESS_KEY=accesskey
|
||||
$ export MINIO_SECRET_KEY=secretkey
|
||||
$ {{.HelpName}}
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}accesskey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}secretkey
|
||||
{{.Prompt}} {{.HelpName}}
|
||||
|
||||
2. Start minio gateway server for S3 backend on custom endpoint.
|
||||
$ export MINIO_ACCESS_KEY=Q3AM3UQ867SPQQA43P2F
|
||||
$ export MINIO_SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
||||
$ {{.HelpName}} https://play.min.io:9000
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}Q3AM3UQ867SPQQA43P2F
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
||||
{{.Prompt}} {{.HelpName}} https://play.min.io:9000
|
||||
|
||||
3. Start minio gateway server for AWS S3 backend logging all requests to http endpoint.
|
||||
$ export MINIO_ACCESS_KEY=Q3AM3UQ867SPQQA43P2F
|
||||
$ export MINIO_SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
||||
$ export MINIO_LOGGER_HTTP_ENDPOINT="http://localhost:8000/"
|
||||
$ {{.HelpName}} https://play.min.io:9000
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}Q3AM3UQ867SPQQA43P2F
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}"http://localhost:8000/"
|
||||
{{.Prompt}} {{.HelpName}} https://play.min.io:9000
|
||||
|
||||
4. Start minio gateway server for AWS S3 backend with edge caching enabled.
|
||||
$ export MINIO_ACCESS_KEY=accesskey
|
||||
$ export MINIO_SECRET_KEY=secretkey
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}}
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}accesskey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}secretkey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}"/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}"bucket1/*;*.png"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}40
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}80
|
||||
{{.Prompt}} {{.HelpName}}
|
||||
|
||||
4. Start minio gateway server for AWS S3 backend using AWS environment variables.
|
||||
NOTE: The access and secret key in this case will authenticate with MinIO instead
|
||||
of AWS and AWS envs will be used to authenticate to AWS S3.
|
||||
$ export AWS_ACCESS_KEY_ID=aws_access_key
|
||||
$ export AWS_SECRET_ACCESS_KEY=aws_secret_key
|
||||
$ export MINIO_ACCESS_KEY=accesskey
|
||||
$ export MINIO_SECRET_KEY=secretkey
|
||||
$ {{.HelpName}}
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}aws_access_key
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}aws_secret_key
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}accesskey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}}(.*){{.AssignmentOperator}}secretkey
|
||||
{{.Prompt}} {{.HelpName}}
|
||||
`
|
||||
|
||||
minio.RegisterGatewayCommand(cli.Command{
|
||||
@@ -500,6 +500,14 @@ func (l *s3Objects) DeleteObject(ctx context.Context, bucket string, object stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *s3Objects) DeleteObjects(ctx context.Context, bucket string, objects []string) ([]error, error) {
|
||||
errs := make([]error, len(objects))
|
||||
for idx, object := range objects {
|
||||
errs[idx] = l.DeleteObject(ctx, bucket, object)
|
||||
}
|
||||
return errs, nil
|
||||
}
|
||||
|
||||
// ListMultipartUploads lists all multipart uploads.
|
||||
func (l *s3Objects) ListMultipartUploads(ctx context.Context, bucket string, prefix string, keyMarker string, uploadIDMarker string, delimiter string, maxUploads int) (lmi minio.ListMultipartsInfo, e error) {
|
||||
result, err := l.Client.ListMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
miniogo "github.com/minio/minio-go"
|
||||
miniogo "github.com/minio/minio-go/v6"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
|
||||
minio "github.com/minio/minio/cmd"
|
||||
|
||||
+17
-3
@@ -24,7 +24,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
@@ -379,6 +379,20 @@ type resourceHandler struct {
|
||||
|
||||
// setCorsHandler handler for CORS (Cross Origin Resource Sharing)
|
||||
func setCorsHandler(h http.Handler) http.Handler {
|
||||
commonS3Headers := []string{
|
||||
"Date",
|
||||
"ETag",
|
||||
"Server",
|
||||
"Connection",
|
||||
"Accept-Ranges",
|
||||
"Content-Range",
|
||||
"Content-Encoding",
|
||||
"Content-Length",
|
||||
"Content-Type",
|
||||
"X-Amz*",
|
||||
"x-amz*",
|
||||
"*",
|
||||
}
|
||||
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
@@ -391,8 +405,8 @@ func setCorsHandler(h http.Handler) http.Handler {
|
||||
http.MethodOptions,
|
||||
http.MethodPatch,
|
||||
},
|
||||
AllowedHeaders: []string{"*"},
|
||||
ExposedHeaders: []string{"*"},
|
||||
AllowedHeaders: commonS3Headers,
|
||||
ExposedHeaders: commonS3Headers,
|
||||
AllowCredentials: true,
|
||||
})
|
||||
|
||||
|
||||
+8
-6
@@ -23,7 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
isatty "github.com/mattn/go-isatty"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
@@ -82,8 +82,6 @@ const (
|
||||
// GlobalMultipartCleanupInterval - Cleanup interval when the stale multipart cleanup is initiated.
|
||||
GlobalMultipartCleanupInterval = time.Hour * 24 // 24 hrs.
|
||||
|
||||
// Refresh interval to update in-memory bucket policy cache.
|
||||
globalRefreshBucketPolicyInterval = 5 * time.Minute
|
||||
// Refresh interval to update in-memory iam config cache.
|
||||
globalRefreshIAMInterval = 5 * time.Minute
|
||||
|
||||
@@ -92,9 +90,10 @@ const (
|
||||
)
|
||||
|
||||
var globalCLIContext = struct {
|
||||
JSON, Quiet bool
|
||||
Anonymous bool
|
||||
Addr string
|
||||
JSON, Quiet bool
|
||||
Anonymous bool
|
||||
Addr string
|
||||
StrictS3Compat bool
|
||||
}{}
|
||||
|
||||
var (
|
||||
@@ -110,6 +109,9 @@ var (
|
||||
// Indicates if the running minio server is an erasure-code backend.
|
||||
globalIsXL = false
|
||||
|
||||
// Indicates if the running minio is in gateway mode.
|
||||
globalIsGateway = false
|
||||
|
||||
// This flag is set to 'true' by default
|
||||
globalIsBrowserEnabled = true
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
)
|
||||
|
||||
var serverPort uint32 = 60000
|
||||
|
||||
+9
-11
@@ -26,9 +26,8 @@ import (
|
||||
"time"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/pkg/certs"
|
||||
)
|
||||
|
||||
@@ -56,9 +55,6 @@ const (
|
||||
|
||||
// DefaultMaxHeaderBytes - default maximum HTTP header size in bytes.
|
||||
DefaultMaxHeaderBytes = 1 * humanize.MiByte
|
||||
|
||||
// DefaultHTTP2MaxConcurrentStreams - default value for HTTP 2.0 maximum concurrent streams allowed.
|
||||
DefaultHTTP2MaxConcurrentStreams = 1024
|
||||
)
|
||||
|
||||
// Server - extended http.Server supports multiple addresses to serve and enhanced connection handling.
|
||||
@@ -133,11 +129,6 @@ func (srv *Server) Start() (err error) {
|
||||
|
||||
// Start servicing with listener.
|
||||
if tlsConfig != nil {
|
||||
if err = http2.ConfigureServer(&srv.Server, &http2.Server{
|
||||
MaxConcurrentStreams: DefaultHTTP2MaxConcurrentStreams,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.Server.Serve(tls.NewListener(listener, tlsConfig))
|
||||
}
|
||||
return srv.Server.Serve(listener)
|
||||
@@ -211,7 +202,14 @@ func NewServer(addrs []string, handler http.Handler, getCert certs.GetCertificat
|
||||
CipherSuites: defaultCipherSuites,
|
||||
CurvePreferences: secureCurves,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
// Do not edit the next line, protos priority is kept
|
||||
// on purpose in this manner for HTTP 2.0, we would
|
||||
// still like HTTP 2.0 clients to negotiate connection
|
||||
// to server if needed but by default HTTP 1.1 is
|
||||
// expected. We need to change this in future
|
||||
// when we wish to go back to HTTP 2.0 as default
|
||||
// priority for HTTP protocol negotiation.
|
||||
NextProtos: []string{"http/1.1", "h2"},
|
||||
}
|
||||
tlsConfig.GetCertificate = getCert
|
||||
}
|
||||
|
||||
+48
-2
@@ -25,7 +25,7 @@ import (
|
||||
"time"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
@@ -79,7 +79,8 @@ func (sys *IAMSys) Init(objAPI ObjectLayer) error {
|
||||
go func() {
|
||||
// Refresh IAMSys with etcd watch.
|
||||
for {
|
||||
watchCh := globalEtcdClient.Watch(context.Background(), iamConfigPrefix)
|
||||
watchCh := globalEtcdClient.Watch(context.Background(),
|
||||
iamConfigPrefix, etcd.WithPrefix())
|
||||
select {
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
@@ -490,6 +491,51 @@ func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetUserSecretKey - sets user secret key
|
||||
func (sys *IAMSys) SetUserSecretKey(accessKey string, secretKey string) error {
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
cred, ok := sys.iamUsersMap[accessKey]
|
||||
if !ok {
|
||||
return errNoSuchUser
|
||||
}
|
||||
|
||||
uinfo := madmin.UserInfo{
|
||||
SecretKey: secretKey,
|
||||
Status: madmin.AccountStatus(cred.Status),
|
||||
}
|
||||
|
||||
configFile := pathJoin(iamConfigUsersPrefix, accessKey, iamIdentityFile)
|
||||
data, err := json.Marshal(uinfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if globalEtcdClient != nil {
|
||||
err = saveConfigEtcd(context.Background(), globalEtcdClient, configFile, data)
|
||||
} else {
|
||||
err = saveConfig(context.Background(), objectAPI, configFile, data)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sys.iamUsersMap[accessKey] = auth.Credentials{
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
Status: string(uinfo.Status),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUser - get user credentials
|
||||
func (sys *IAMSys) GetUser(accessKey string) (cred auth.Credentials, ok bool) {
|
||||
sys.RLock()
|
||||
|
||||
@@ -47,6 +47,7 @@ var (
|
||||
errChangeCredNotAllowed = errors.New("Changing access key and secret key not allowed")
|
||||
errAuthentication = errors.New("Authentication failed, check your access credentials")
|
||||
errNoAuthToken = errors.New("JWT token missing")
|
||||
errIncorrectCreds = errors.New("Current access key or secret key is incorrect")
|
||||
)
|
||||
|
||||
func authenticateJWTUsers(accessKey, secretKey string, expiry time.Duration) (string, error) {
|
||||
|
||||
@@ -17,12 +17,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
const lockRESTVersion = "v1"
|
||||
@@ -53,28 +49,22 @@ type lockResponse struct {
|
||||
func (l *localLocker) removeEntryIfExists(nlrip nameLockRequesterInfoPair) {
|
||||
// Check if entry is still in map (could have been removed altogether by 'concurrent' (R)Unlock of last entry)
|
||||
if lri, ok := l.lockMap[nlrip.name]; ok {
|
||||
if !l.removeEntry(nlrip.name, nlrip.lri.UID, &lri) {
|
||||
// Remove failed, in case it is a:
|
||||
if nlrip.lri.Writer {
|
||||
// Writer: this should never happen as the whole (mapped) entry should have been deleted
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("name", nlrip.name)
|
||||
reqInfo.AppendTags("uid", nlrip.lri.UID)
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, errors.New("Lock maintenance failed to remove entry for write lock (should never happen)"))
|
||||
} // Reader: this can happen if multiple read locks were active and
|
||||
// the one we are looking for has been released concurrently (so it is fine).
|
||||
} // Removal went okay, all is fine.
|
||||
// Even if the entry exists, it may not be the same entry which was
|
||||
// considered as expired, so we simply an attempt to remove it if its
|
||||
// not possible there is nothing we need to do.
|
||||
l.removeEntry(nlrip.name, nlrip.lri.UID, &lri)
|
||||
}
|
||||
}
|
||||
|
||||
// removeEntry either, based on the uid of the lock message, removes a single entry from the
|
||||
// lockRequesterInfo array or the whole array from the map (in case of a write lock or last read lock)
|
||||
// removeEntry based on the uid of the lock message, removes a single entry from the
|
||||
// lockRequesterInfo array or the whole array from the map (in case of a write lock
|
||||
// or last read lock)
|
||||
func (l *localLocker) removeEntry(name, uid string, lri *[]lockRequesterInfo) bool {
|
||||
// Find correct entry to remove based on uid.
|
||||
for index, entry := range *lri {
|
||||
if entry.UID == uid {
|
||||
if len(*lri) == 1 {
|
||||
// Remove the (last) lock.
|
||||
// Remove the write lock.
|
||||
delete(l.lockMap, name)
|
||||
} else {
|
||||
// Remove the appropriate read lock.
|
||||
@@ -84,6 +74,7 @@ func (l *localLocker) removeEntry(name, uid string, lri *[]lockRequesterInfo) bo
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// None found return false, perhaps entry removed in previous run.
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -286,17 +286,17 @@ func (l *lockRESTServer) lockMaintenance(interval time.Duration) {
|
||||
Resource: nlrip.name,
|
||||
})
|
||||
|
||||
// Close the connection regardless of the call response.
|
||||
c.Close()
|
||||
|
||||
// For successful response, verify if lock is indeed active or stale.
|
||||
// For successful response, verify if lock was indeed active or stale.
|
||||
if expired {
|
||||
// The lock is no longer active at server that originated the lock
|
||||
// So remove the lock from the map.
|
||||
// The lock is no longer active at server that originated
|
||||
// the lock, attempt to remove the lock.
|
||||
l.ll.mutex.Lock()
|
||||
l.ll.removeEntryIfExists(nlrip) // Purge the stale entry if it exists.
|
||||
l.ll.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Close the connection regardless of the call response.
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/highwayhash"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger/message/log"
|
||||
)
|
||||
|
||||
|
||||
@@ -74,6 +74,11 @@ func (c *Target) Send(e interface{}) error {
|
||||
apiString += ")"
|
||||
timeString := "Time: " + time.Now().Format(logger.TimeFormat)
|
||||
|
||||
var deploymentID string
|
||||
if entry.DeploymentID != "" {
|
||||
deploymentID = "\nDeploymentID: " + entry.DeploymentID
|
||||
}
|
||||
|
||||
var requestID string
|
||||
if entry.RequestID != "" {
|
||||
requestID = "\nRequestID: " + entry.RequestID
|
||||
@@ -94,8 +99,8 @@ func (c *Target) Send(e interface{}) error {
|
||||
}
|
||||
|
||||
var msg = logger.ColorFgRed(logger.ColorBold(entry.Trace.Message))
|
||||
var output = fmt.Sprintf("\n%s\n%s%s%s%s\nError: %s%s\n%s",
|
||||
apiString, timeString, requestID, remoteHost, userAgent,
|
||||
var output = fmt.Sprintf("\n%s\n%s%s%s%s%s\nError: %s%s\n%s",
|
||||
apiString, timeString, deploymentID, requestID, remoteHost, userAgent,
|
||||
msg, tagString, strings.Join(trace, "\n"))
|
||||
|
||||
fmt.Println(output)
|
||||
|
||||
+10
-6
@@ -32,24 +32,28 @@ var globalFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "config-dir, C",
|
||||
Value: defaultConfigDir.Get(),
|
||||
Usage: "[DEPRECATED] Path to legacy configuration directory.",
|
||||
Usage: "[DEPRECATED] path to legacy configuration directory",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "certs-dir, S",
|
||||
Value: defaultCertsDir.Get(),
|
||||
Usage: "Path to certs directory.",
|
||||
Usage: "path to certs directory",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Usage: "Disable startup information.",
|
||||
Usage: "disable startup information",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "anonymous",
|
||||
Usage: "Hide sensitive information from logging.",
|
||||
Usage: "hide sensitive information from logging",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "json",
|
||||
Usage: "Output server logs and startup information in json format.",
|
||||
Usage: "output server logs and startup information in json format",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "compat",
|
||||
Usage: "trade off performance for S3 compatibility",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -118,7 +122,7 @@ func newApp(name string) *cli.App {
|
||||
// Set up app.
|
||||
cli.HelpFlag = cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Show help.",
|
||||
Usage: "show help",
|
||||
}
|
||||
|
||||
app := cli.NewApp()
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 (
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
globalMergeLookupTimeout = time.Minute * 1 // 1 minutes.
|
||||
)
|
||||
|
||||
// mergeWalk - represents the go routine that does the merge walk.
|
||||
type mergeWalk struct {
|
||||
entryChs []FileInfoCh
|
||||
endWalkCh chan struct{} // To signal when mergeWalk go-routine should end.
|
||||
endTimerCh chan<- struct{} // To signal when timer go-routine should end.
|
||||
}
|
||||
|
||||
// MergeWalkPool - pool of mergeWalk go routines.
|
||||
// A mergeWalk is added to the pool by Set() and removed either by
|
||||
// doing a Release() or if the concerned timer goes off.
|
||||
// mergeWalkPool's purpose is to maintain active mergeWalk go-routines in a map so that
|
||||
// it can be looked up across related list calls.
|
||||
type MergeWalkPool struct {
|
||||
pool map[listParams][]mergeWalk
|
||||
timeOut time.Duration
|
||||
lock *sync.Mutex
|
||||
}
|
||||
|
||||
// NewMergeWalkPool - initialize new tree walk pool.
|
||||
func NewMergeWalkPool(timeout time.Duration) *MergeWalkPool {
|
||||
tPool := &MergeWalkPool{
|
||||
pool: make(map[listParams][]mergeWalk),
|
||||
timeOut: timeout,
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
return tPool
|
||||
}
|
||||
|
||||
// Release - selects a mergeWalk from the pool based on the input
|
||||
// listParams, removes it from the pool, and returns the MergeWalkResult
|
||||
// channel.
|
||||
// Returns nil if listParams does not have an asccociated mergeWalk.
|
||||
func (t MergeWalkPool) Release(params listParams) ([]FileInfoCh, chan struct{}) {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
walks, ok := t.pool[params] // Pick the valid walks.
|
||||
if ok {
|
||||
if len(walks) > 0 {
|
||||
// Pop out the first valid walk entry.
|
||||
walk := walks[0]
|
||||
walks = walks[1:]
|
||||
if len(walks) > 0 {
|
||||
t.pool[params] = walks
|
||||
} else {
|
||||
delete(t.pool, params)
|
||||
}
|
||||
walk.endTimerCh <- struct{}{}
|
||||
return walk.entryChs, walk.endWalkCh
|
||||
}
|
||||
}
|
||||
// Release return nil if params not found.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Set - adds a mergeWalk to the mergeWalkPool.
|
||||
// Also starts a timer go-routine that ends when:
|
||||
// 1) time.After() expires after t.timeOut seconds.
|
||||
// The expiration is needed so that the mergeWalk go-routine resources are freed after a timeout
|
||||
// if the S3 client does only partial listing of objects.
|
||||
// 2) Relase() signals the timer go-routine to end on endTimerCh.
|
||||
// During listing the timer should not timeout and end the mergeWalk go-routine, hence the
|
||||
// timer go-routine should be ended.
|
||||
func (t MergeWalkPool) Set(params listParams, resultChs []FileInfoCh, endWalkCh chan struct{}) {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
// Should be a buffered channel so that Release() never blocks.
|
||||
endTimerCh := make(chan struct{}, 1)
|
||||
|
||||
walkInfo := mergeWalk{
|
||||
entryChs: resultChs,
|
||||
endWalkCh: endWalkCh,
|
||||
endTimerCh: endTimerCh,
|
||||
}
|
||||
|
||||
// Append new walk info.
|
||||
t.pool[params] = append(t.pool[params], walkInfo)
|
||||
|
||||
// Timer go-routine which times out after t.timeOut seconds.
|
||||
go func(endTimerCh <-chan struct{}, walkInfo mergeWalk) {
|
||||
select {
|
||||
// Wait until timeOut
|
||||
case <-time.After(t.timeOut):
|
||||
// Timeout has expired. Remove the mergeWalk from mergeWalkPool and
|
||||
// end the mergeWalk go-routine.
|
||||
t.lock.Lock()
|
||||
walks, ok := t.pool[params]
|
||||
if ok {
|
||||
// Trick of filtering without allocating
|
||||
// https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
|
||||
nwalks := walks[:0]
|
||||
// Look for walkInfo, remove it from the walks list.
|
||||
for _, walk := range walks {
|
||||
if !reflect.DeepEqual(walk, walkInfo) {
|
||||
nwalks = append(nwalks, walk)
|
||||
}
|
||||
}
|
||||
if len(nwalks) == 0 {
|
||||
// No more mergeWalk go-routines associated with listParams
|
||||
// hence remove map entry.
|
||||
delete(t.pool, params)
|
||||
} else {
|
||||
// There are more mergeWalk go-routines associated with listParams
|
||||
// hence save the list in the map.
|
||||
t.pool[params] = nwalks
|
||||
}
|
||||
}
|
||||
// Signal the mergeWalk go-routine to die.
|
||||
close(endWalkCh)
|
||||
t.lock.Unlock()
|
||||
case <-endTimerCh:
|
||||
return
|
||||
}
|
||||
}(endTimerCh, walkInfo)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 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 (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Test if tree walker go-routine is removed from the pool after timeout
|
||||
// and that is available in the pool before the timeout.
|
||||
func TestMergeWalkPoolBasic(t *testing.T) {
|
||||
// Create a treeWalkPool
|
||||
tw := NewMergeWalkPool(1 * time.Second)
|
||||
|
||||
// Create sample params
|
||||
params := listParams{
|
||||
bucket: "test-bucket",
|
||||
}
|
||||
|
||||
endWalkCh := make(chan struct{})
|
||||
// Add a treeWalk to the pool
|
||||
tw.Set(params, []FileInfoCh{}, endWalkCh)
|
||||
|
||||
// Wait for treeWalkPool timeout to happen
|
||||
<-time.After(2 * time.Second)
|
||||
if c1, _ := tw.Release(params); c1 != nil {
|
||||
t.Error("treeWalk go-routine must have been freed")
|
||||
}
|
||||
|
||||
// Add the treeWalk back to the pool
|
||||
endWalkCh = make(chan struct{})
|
||||
tw.Set(params, []FileInfoCh{}, endWalkCh)
|
||||
|
||||
// Release the treeWalk before timeout
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
break
|
||||
default:
|
||||
if c1, _ := tw.Release(params); c1 == nil {
|
||||
t.Error("treeWalk go-routine got freed before timeout")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test if multiple merge walkers for the same listParams are managed as expected by the pool.
|
||||
func TestManyMergeWalksSameParam(t *testing.T) {
|
||||
// Create a treeWalkPool.
|
||||
tw := NewMergeWalkPool(5 * time.Second)
|
||||
|
||||
// Create sample params.
|
||||
params := listParams{
|
||||
bucket: "test-bucket",
|
||||
}
|
||||
|
||||
select {
|
||||
// This timeout is an upper-bound. This is started
|
||||
// before the first treeWalk go-routine's timeout period starts.
|
||||
case <-time.After(5 * time.Second):
|
||||
break
|
||||
default:
|
||||
// Create many treeWalk go-routines for the same params.
|
||||
for i := 0; i < 10; i++ {
|
||||
endWalkCh := make(chan struct{})
|
||||
walkChs := make([]FileInfoCh, 0)
|
||||
tw.Set(params, walkChs, endWalkCh)
|
||||
}
|
||||
|
||||
tw.lock.Lock()
|
||||
if walks, ok := tw.pool[params]; ok {
|
||||
if len(walks) != 10 {
|
||||
t.Error("There aren't as many walks as were Set")
|
||||
}
|
||||
}
|
||||
tw.lock.Unlock()
|
||||
for i := 0; i < 10; i++ {
|
||||
tw.lock.Lock()
|
||||
if walks, ok := tw.pool[params]; ok {
|
||||
// Before ith Release we should have 10-i treeWalk go-routines.
|
||||
if 10-i != len(walks) {
|
||||
t.Error("There aren't as many walks as were Set")
|
||||
}
|
||||
}
|
||||
tw.lock.Unlock()
|
||||
tw.Release(params)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
|
||||
"github.com/minio/dsync"
|
||||
"github.com/minio/lsync"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
@@ -111,6 +111,13 @@ func (d *naughtyDisk) DeleteVol(volume string) (err error) {
|
||||
return d.disk.DeleteVol(volume)
|
||||
}
|
||||
|
||||
func (d *naughtyDisk) Walk(volume, path, marker string, recursive bool, leafFile string, readMetadataFn readMetadataFunc, endWalkCh chan struct{}) (chan FileInfo, error) {
|
||||
if err := d.calcError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.disk.Walk(volume, path, marker, recursive, leafFile, readMetadataFn, endWalkCh)
|
||||
}
|
||||
|
||||
func (d *naughtyDisk) ListDir(volume, path string, count int, leafFile string) (entries []string, err error) {
|
||||
if err := d.calcError(); err != nil {
|
||||
return []string{}, err
|
||||
@@ -167,11 +174,19 @@ func (d *naughtyDisk) DeleteFile(volume string, path string) (err error) {
|
||||
return d.disk.DeleteFile(volume, path)
|
||||
}
|
||||
|
||||
func (d *naughtyDisk) WriteAll(volume string, path string, buf []byte) (err error) {
|
||||
func (d *naughtyDisk) DeleteFileBulk(volume string, paths []string) ([]error, error) {
|
||||
errs := make([]error, len(paths))
|
||||
for idx, path := range paths {
|
||||
errs[idx] = d.disk.DeleteFile(volume, path)
|
||||
}
|
||||
return errs, nil
|
||||
}
|
||||
|
||||
func (d *naughtyDisk) WriteAll(volume string, path string, reader io.Reader) (err error) {
|
||||
if err := d.calcError(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.disk.WriteAll(volume, path, buf)
|
||||
return d.disk.WriteAll(volume, path, reader)
|
||||
}
|
||||
|
||||
func (d *naughtyDisk) ReadAll(volume string, path string) (buf []byte, err error) {
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
)
|
||||
|
||||
func TestMustSplitHostPort(t *testing.T) {
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
@@ -910,6 +911,11 @@ func (args eventArgs) ToEvent() event.Event {
|
||||
}
|
||||
|
||||
func sendEvent(args eventArgs) {
|
||||
|
||||
// remove sensitive encryption entries in metadata.
|
||||
crypto.RemoveSensitiveEntries(args.Object.UserDefined)
|
||||
crypto.RemoveInternalEntries(args.Object.UserDefined)
|
||||
|
||||
// globalNotificationSys is not initialized in gateway mode.
|
||||
if globalNotificationSys == nil {
|
||||
return
|
||||
|
||||
@@ -144,6 +144,78 @@ func cleanupDir(ctx context.Context, storage StorageAPI, volume, dirPath string)
|
||||
return err
|
||||
}
|
||||
|
||||
// Cleanup objects in bulk and recursively: each object will have a list of sub-files to delete in the backend
|
||||
func cleanupObjectsBulk(ctx context.Context, storage StorageAPI, volume string, objsPaths []string, errs []error) ([]error, error) {
|
||||
// The list of files in disk to delete
|
||||
var filesToDelete []string
|
||||
// Map files to delete to the passed objsPaths
|
||||
var filesToDeleteObjsIndexes []int
|
||||
|
||||
// Traverse and return the list of sub entries
|
||||
var traverse func(string) ([]string, error)
|
||||
traverse = func(entryPath string) ([]string, error) {
|
||||
var output = make([]string, 0)
|
||||
if !hasSuffix(entryPath, slashSeparator) {
|
||||
output = append(output, entryPath)
|
||||
return output, nil
|
||||
}
|
||||
entries, err := storage.ListDir(volume, entryPath, -1, "")
|
||||
if err != nil {
|
||||
if err == errFileNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
subEntries, err := traverse(pathJoin(entryPath, entry))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = append(output, subEntries...)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// Find and collect the list of files to remove associated
|
||||
// to the passed objects paths
|
||||
for idx, objPath := range objsPaths {
|
||||
if errs[idx] != nil {
|
||||
continue
|
||||
}
|
||||
output, err := traverse(objPath)
|
||||
if err != nil {
|
||||
errs[idx] = err
|
||||
continue
|
||||
} else {
|
||||
errs[idx] = nil
|
||||
}
|
||||
filesToDelete = append(filesToDelete, output...)
|
||||
for i := 0; i < len(output); i++ {
|
||||
filesToDeleteObjsIndexes = append(filesToDeleteObjsIndexes, idx)
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse the list so remove can succeed
|
||||
reverseStringSlice(filesToDelete)
|
||||
|
||||
dErrs, err := storage.DeleteFileBulk(volume, filesToDelete)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Map files deletion errors to the correspondent objects
|
||||
for i := range dErrs {
|
||||
if dErrs[i] != nil {
|
||||
if errs[filesToDeleteObjsIndexes[i]] != nil {
|
||||
errs[filesToDeleteObjsIndexes[i]] = dErrs[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errs, nil
|
||||
}
|
||||
|
||||
// Removes notification.xml for a given bucket, only used during DeleteBucket.
|
||||
func removeNotificationConfig(ctx context.Context, objAPI ObjectLayer, bucket string) error {
|
||||
// Verify bucket is valid.
|
||||
|
||||
@@ -27,22 +27,6 @@ import (
|
||||
// underlying storage layer.
|
||||
func toObjectErr(err error, params ...string) error {
|
||||
switch err {
|
||||
case errDiskNotFound:
|
||||
switch len(params) {
|
||||
case 1:
|
||||
err = BucketNotFound{Bucket: params[0]}
|
||||
case 2:
|
||||
err = ObjectNotFound{
|
||||
Bucket: params[0],
|
||||
Object: params[1],
|
||||
}
|
||||
case 3:
|
||||
err = InvalidUploadID{
|
||||
Bucket: params[0],
|
||||
Object: params[1],
|
||||
UploadID: params[2],
|
||||
}
|
||||
}
|
||||
case errVolumeNotFound:
|
||||
if len(params) >= 1 {
|
||||
err = BucketNotFound{Bucket: params[0]}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
"github.com/minio/minio-go/v6/pkg/encrypt"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
@@ -73,6 +73,7 @@ type ObjectLayer interface {
|
||||
PutObject(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
CopyObject(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
DeleteObject(ctx context.Context, bucket, object string) error
|
||||
DeleteObjects(ctx context.Context, bucket string, objects []string) ([]error, error)
|
||||
|
||||
// Multipart operations.
|
||||
ListMultipartUploads(ctx context.Context, bucket, prefix, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (result ListMultipartsInfo, err error)
|
||||
|
||||
@@ -40,6 +40,8 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
||||
testBuckets := []string{
|
||||
// This bucket is used for testing ListObject operations.
|
||||
"test-bucket-list-object",
|
||||
// This bucket will be tested with empty directories
|
||||
"test-bucket-empty-dir",
|
||||
// Will not store any objects in this bucket,
|
||||
// Its to test ListObjects on an empty bucket.
|
||||
"empty-bucket",
|
||||
@@ -53,23 +55,27 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
||||
|
||||
var err error
|
||||
testObjects := []struct {
|
||||
name string
|
||||
content string
|
||||
meta map[string]string
|
||||
parentBucket string
|
||||
name string
|
||||
content string
|
||||
meta map[string]string
|
||||
}{
|
||||
{"Asia-maps.png", "asis-maps", map[string]string{"content-type": "image/png"}},
|
||||
{"Asia/India/India-summer-photos-1", "contentstring", nil},
|
||||
{"Asia/India/Karnataka/Bangalore/Koramangala/pics", "contentstring", nil},
|
||||
{"newPrefix0", "newPrefix0", nil},
|
||||
{"newPrefix1", "newPrefix1", nil},
|
||||
{"newzen/zen/recurse/again/again/again/pics", "recurse", nil},
|
||||
{"obj0", "obj0", nil},
|
||||
{"obj1", "obj1", nil},
|
||||
{"obj2", "obj2", nil},
|
||||
{testBuckets[0], "Asia-maps.png", "asis-maps", map[string]string{"content-type": "image/png"}},
|
||||
{testBuckets[0], "Asia/India/India-summer-photos-1", "contentstring", nil},
|
||||
{testBuckets[0], "Asia/India/Karnataka/Bangalore/Koramangala/pics", "contentstring", nil},
|
||||
{testBuckets[0], "newPrefix0", "newPrefix0", nil},
|
||||
{testBuckets[0], "newPrefix1", "newPrefix1", nil},
|
||||
{testBuckets[0], "newzen/zen/recurse/again/again/again/pics", "recurse", nil},
|
||||
{testBuckets[0], "obj0", "obj0", nil},
|
||||
{testBuckets[0], "obj1", "obj1", nil},
|
||||
{testBuckets[0], "obj2", "obj2", nil},
|
||||
{testBuckets[1], "obj1", "obj1", nil},
|
||||
{testBuckets[1], "obj2", "obj2", nil},
|
||||
{testBuckets[1], "temporary/0/", "", nil},
|
||||
}
|
||||
for _, object := range testObjects {
|
||||
md5Bytes := md5.Sum([]byte(object.content))
|
||||
_, err = obj.PutObject(context.Background(), testBuckets[0], object.name, mustGetPutObjReader(t, bytes.NewBufferString(object.content),
|
||||
_, err = obj.PutObject(context.Background(), object.parentBucket, object.name, mustGetPutObjReader(t, bytes.NewBufferString(object.content),
|
||||
int64(len(object.content)), hex.EncodeToString(md5Bytes[:]), ""), ObjectOptions{UserDefined: object.meta})
|
||||
if err != nil {
|
||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||
@@ -361,6 +367,7 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "Asia-maps.png"},
|
||||
},
|
||||
Prefixes: []string{"Asia/"},
|
||||
},
|
||||
// ListObjectsResult-26.
|
||||
// prefix = "new" and delimiter is set in the testCase.(testCase 58).
|
||||
@@ -370,6 +377,7 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
||||
{Name: "newPrefix0"},
|
||||
{Name: "newPrefix1"},
|
||||
},
|
||||
Prefixes: []string{"newzen/"},
|
||||
},
|
||||
// ListObjectsResult-27.
|
||||
// Prefix is set to "Asia/India/" in the testCase, and delimiter is set to forward slash '/' (testCase 59).
|
||||
@@ -378,6 +386,7 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "Asia/India/India-summer-photos-1"},
|
||||
},
|
||||
Prefixes: []string{"Asia/India/Karnataka/"},
|
||||
},
|
||||
// ListObjectsResult-28.
|
||||
// Marker is set to "Asia/India/India-summer-photos-1" and delimiter set in the testCase, (testCase 60).
|
||||
@@ -390,6 +399,7 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
Prefixes: []string{"newzen/"},
|
||||
},
|
||||
// ListObjectsResult-29.
|
||||
// Marker is set to "Asia/India/Karnataka/Bangalore/Koramangala/pics" in the testCase and delimeter set, (testCase 61).
|
||||
@@ -402,6 +412,7 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
Prefixes: []string{"newzen/"},
|
||||
},
|
||||
// ListObjectsResult-30.
|
||||
// Prefix and Delimiter is set to '/', (testCase 62).
|
||||
@@ -409,6 +420,31 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{},
|
||||
},
|
||||
// ListObjectsResult-31 Empty directory, recursive listing
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
{Name: "temporary/0/"},
|
||||
},
|
||||
},
|
||||
// ListObjectsResult-32 Empty directory, non recursive listing
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "obj1"},
|
||||
{Name: "obj2"},
|
||||
},
|
||||
Prefixes: []string{"temporary/"},
|
||||
},
|
||||
// ListObjectsResult-33 Listing empty directory only
|
||||
{
|
||||
IsTruncated: false,
|
||||
Objects: []ObjectInfo{
|
||||
{Name: "temporary/0/"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
@@ -519,9 +555,14 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
||||
{"test-bucket-list-object", "", "Asia/India/Karnataka/Bangalore/Koramangala/pics", "/", 10, resultCases[29], nil, true},
|
||||
// Test with prefix and delimiter set to '/'. (60)
|
||||
{"test-bucket-list-object", "/", "", "/", 10, resultCases[30], nil, true},
|
||||
|
||||
// Test with invalid prefix (61)
|
||||
{"test-bucket-list-object", "\\", "", "/", 10, resultCases[30], ObjectNameInvalid{Bucket: "test-bucket-list-object", Object: "\\"}, false},
|
||||
{"test-bucket-list-object", "\\", "", "/", 10, ListObjectsInfo{}, ObjectNameInvalid{Bucket: "test-bucket-list-object", Object: "\\"}, false},
|
||||
// Test listing an empty directory in recursive mode (62)
|
||||
{"test-bucket-empty-dir", "", "", "", 10, resultCases[31], nil, true},
|
||||
// Test listing an empty directory in a non recursive mode (63)
|
||||
{"test-bucket-empty-dir", "", "", "/", 10, resultCases[32], nil, true},
|
||||
// Test listing a directory which contains an empty directory (64)
|
||||
{"test-bucket-empty-dir", "", "temporary/", "", 10, resultCases[33], nil, true},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
@@ -563,6 +604,16 @@ func testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(testCase.result.Prefixes) != len(result.Prefixes) {
|
||||
t.Fatalf("Test %d: %s: Expected number of prefixes in the result to be '%d', but found '%d' prefixes instead", i+1, instanceType, len(testCase.result.Prefixes), len(result.Prefixes))
|
||||
}
|
||||
for j := 0; j < len(testCase.result.Prefixes); j++ {
|
||||
if testCase.result.Prefixes[j] != result.Prefixes[j] {
|
||||
t.Errorf("Test %d: %s: Expected prefix name to be \"%s\", but found \"%s\" instead", i+1, instanceType, testCase.result.Prefixes[j], result.Prefixes[j])
|
||||
}
|
||||
}
|
||||
|
||||
if testCase.result.IsTruncated != result.IsTruncated {
|
||||
t.Errorf("Test %d: %s: Expected IsTruncated flag to be %v, but instead found it to be %v", i+1, instanceType, testCase.result.IsTruncated, result.IsTruncated)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -256,8 +255,8 @@ func testPutObjectPartDiskNotFound(obj ObjectLayer, instanceType string, disks [
|
||||
}
|
||||
|
||||
// As all disks at not available, bucket not found.
|
||||
expectedErr2 := BucketNotFound{Bucket: testCase.bucketName}
|
||||
if err.Error() != expectedErr2.Error() {
|
||||
expectedErr2 := errDiskNotFound
|
||||
if err != errDiskNotFound {
|
||||
t.Fatalf("Test %s: expected error %s, got %s instead.", instanceType, expectedErr2, err)
|
||||
}
|
||||
}
|
||||
@@ -1866,10 +1865,7 @@ func testObjectCompleteMultipartUpload(obj ObjectLayer, instanceType string, t T
|
||||
},
|
||||
},
|
||||
}
|
||||
s3MD5, err := getCompleteMultipartMD5(context.Background(), inputParts[3].parts)
|
||||
if err != nil {
|
||||
t.Fatalf("Obtaining S3MD5 failed")
|
||||
}
|
||||
s3MD5 := getCompleteMultipartMD5(inputParts[3].parts)
|
||||
|
||||
// Test cases with sample input values for CompleteMultipartUpload.
|
||||
testCases := []struct {
|
||||
@@ -1898,8 +1894,8 @@ func testObjectCompleteMultipartUpload(obj ObjectLayer, instanceType string, t T
|
||||
// Asserting for Invalid UploadID (Test number 9).
|
||||
{bucketNames[0], objectNames[0], "abc", []CompletePart{}, "", InvalidUploadID{UploadID: "abc"}, false},
|
||||
// Test case with invalid Part Etag (Test number 10-11).
|
||||
{bucketNames[0], objectNames[0], uploadIDs[0], []CompletePart{{ETag: "abc"}}, "", hex.ErrLength, false},
|
||||
{bucketNames[0], objectNames[0], uploadIDs[0], []CompletePart{{ETag: "abcz"}}, "", hex.InvalidByteError(00), false},
|
||||
{bucketNames[0], objectNames[0], uploadIDs[0], []CompletePart{{ETag: "abc"}}, "", InvalidPart{}, false},
|
||||
{bucketNames[0], objectNames[0], uploadIDs[0], []CompletePart{{ETag: "abcz"}}, "", InvalidPart{}, false},
|
||||
// Part number 0 doesn't exist, expecting InvalidPart error (Test number 12).
|
||||
{bucketNames[0], objectNames[0], uploadIDs[0], []CompletePart{{ETag: "abcd", PartNumber: 0}}, "", InvalidPart{}, false},
|
||||
// // Upload and PartNumber exists, But a deliberate ETag mismatch is introduced (Test number 13).
|
||||
|
||||
+24
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2015, 2016 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2015-2019 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
snappy "github.com/golang/snappy"
|
||||
"github.com/minio/minio-go/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
@@ -190,18 +190,18 @@ func mustGetUUID() string {
|
||||
}
|
||||
|
||||
// Create an s3 compatible MD5sum for complete multipart transaction.
|
||||
func getCompleteMultipartMD5(ctx context.Context, parts []CompletePart) (string, error) {
|
||||
func getCompleteMultipartMD5(parts []CompletePart) string {
|
||||
var finalMD5Bytes []byte
|
||||
for _, part := range parts {
|
||||
md5Bytes, err := hex.DecodeString(canonicalizeETag(part.ETag))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return "", err
|
||||
finalMD5Bytes = append(finalMD5Bytes, []byte(part.ETag)...)
|
||||
} else {
|
||||
finalMD5Bytes = append(finalMD5Bytes, md5Bytes...)
|
||||
}
|
||||
finalMD5Bytes = append(finalMD5Bytes, md5Bytes...)
|
||||
}
|
||||
s3MD5 := fmt.Sprintf("%s-%d", getMD5Hash(finalMD5Bytes), len(parts))
|
||||
return s3MD5, nil
|
||||
return s3MD5
|
||||
}
|
||||
|
||||
// Clean unwanted fields from metadata
|
||||
@@ -675,9 +675,26 @@ func (p *PutObjReader) Size() int64 {
|
||||
// as a hex encoded string
|
||||
func (p *PutObjReader) MD5CurrentHexString() string {
|
||||
md5sumCurr := p.rawReader.MD5Current()
|
||||
var appendHyphen bool
|
||||
// md5sumcurr is not empty in two scenarios
|
||||
// - server is running in strict compatibility mode
|
||||
// - client set Content-Md5 during PUT operation
|
||||
if len(md5sumCurr) == 0 {
|
||||
// md5sumCurr is only empty when we are running
|
||||
// in non-compatibility mode.
|
||||
md5sumCurr = make([]byte, 16)
|
||||
rand.Read(md5sumCurr)
|
||||
appendHyphen = true
|
||||
}
|
||||
if p.sealMD5Fn != nil {
|
||||
md5sumCurr = p.sealMD5Fn(md5sumCurr)
|
||||
}
|
||||
if appendHyphen {
|
||||
// Make sure to return etag string upto 32 length, for SSE
|
||||
// requests ETag might be longer and the code decrypting the
|
||||
// ETag ignores ETag in multipart ETag form i.e <hex>-N
|
||||
return hex.EncodeToString(md5sumCurr)[:32] + "-1"
|
||||
}
|
||||
return hex.EncodeToString(md5sumCurr)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
@@ -143,8 +142,8 @@ func TestGetCompleteMultipartMD5(t *testing.T) {
|
||||
expectedResult string
|
||||
expectedErr string
|
||||
}{
|
||||
// Wrong MD5 hash string
|
||||
{[]CompletePart{{ETag: "wrong-md5-hash-string"}}, "", "encoding/hex: invalid byte: U+0077 'w'"},
|
||||
// Wrong MD5 hash string, returns md5um of hash
|
||||
{[]CompletePart{{ETag: "wrong-md5-hash-string"}}, "0deb8cb07527b4b2669c861cb9653607-1", ""},
|
||||
|
||||
// Single CompletePart with valid MD5 hash string.
|
||||
{[]CompletePart{{ETag: "cf1f738a5924e645913c984e0fe3d708"}}, "10dc1617fbcf0bd0858048cb96e6bd77-1", ""},
|
||||
@@ -154,17 +153,10 @@ func TestGetCompleteMultipartMD5(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
result, err := getCompleteMultipartMD5(context.Background(), test.parts)
|
||||
result := getCompleteMultipartMD5(test.parts)
|
||||
if result != test.expectedResult {
|
||||
t.Fatalf("test %d failed: expected: result=%v, got=%v", i+1, test.expectedResult, result)
|
||||
}
|
||||
errString := ""
|
||||
if err != nil {
|
||||
errString = err.Error()
|
||||
}
|
||||
if errString != test.expectedErr {
|
||||
t.Fatalf("test %d failed: expected: err=%v, got=%v", i+1, test.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-23
@@ -33,8 +33,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
miniogo "github.com/minio/minio-go"
|
||||
"github.com/minio/minio-go/pkg/encrypt"
|
||||
miniogo "github.com/minio/minio-go/v6"
|
||||
"github.com/minio/minio-go/v6/pkg/encrypt"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
@@ -827,7 +827,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
reader = gr
|
||||
}
|
||||
|
||||
srcInfo.Reader, err = hash.NewReader(reader, length, "", "", actualSize)
|
||||
srcInfo.Reader, err = hash.NewReader(reader, length, "", "", actualSize, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -934,7 +934,8 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
crypto.RemoveInternalEntries(srcInfo.UserDefined)
|
||||
}
|
||||
|
||||
srcInfo.Reader, err = hash.NewReader(reader, targetSize, "", "", targetSize) // do not try to verify encrypted content
|
||||
// do not try to verify encrypted content
|
||||
srcInfo.Reader, err = hash.NewReader(reader, targetSize, "", "", targetSize, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1188,7 +1189,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
metadata[ReservedMetadataPrefix+"compression"] = compressionAlgorithmV1
|
||||
metadata[ReservedMetadataPrefix+"actual-size"] = strconv.FormatInt(size, 10)
|
||||
|
||||
actualReader, err := hash.NewReader(reader, size, md5hex, sha256hex, actualSize)
|
||||
actualReader, err := hash.NewReader(reader, size, md5hex, sha256hex, actualSize, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1201,7 +1202,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
sha256hex = ""
|
||||
}
|
||||
|
||||
hashReader, err := hash.NewReader(reader, size, md5hex, sha256hex, actualSize)
|
||||
hashReader, err := hash.NewReader(reader, size, md5hex, sha256hex, actualSize, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1235,7 +1236,8 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
info := ObjectInfo{Size: size}
|
||||
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", size) // do not try to verify encrypted content
|
||||
// do not try to verify encrypted content
|
||||
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", size, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1260,8 +1262,9 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
|
||||
etag := objInfo.ETag
|
||||
if objInfo.IsCompressed() {
|
||||
// Ignore compressed ETag.
|
||||
etag = objInfo.ETag + "-1"
|
||||
if !strings.HasSuffix(objInfo.ETag, "-1") {
|
||||
etag = objInfo.ETag + "-1"
|
||||
}
|
||||
} else if hasServerSideEncryptionHeader(r.Header) {
|
||||
etag = getDecryptedETag(r.Header, objInfo, false)
|
||||
}
|
||||
@@ -1613,7 +1616,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
reader = gr
|
||||
}
|
||||
|
||||
srcInfo.Reader, err = hash.NewReader(reader, length, "", "", actualPartSize)
|
||||
srcInfo.Reader, err = hash.NewReader(reader, length, "", "", actualPartSize, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1673,7 +1676,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
}
|
||||
|
||||
info := ObjectInfo{Size: length}
|
||||
srcInfo.Reader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", length)
|
||||
srcInfo.Reader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", length, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1840,7 +1843,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
|
||||
isCompressed := false
|
||||
if objectAPI.IsCompressionSupported() && compressPart {
|
||||
actualReader, err := hash.NewReader(reader, size, md5hex, sha256hex, actualSize)
|
||||
actualReader, err := hash.NewReader(reader, size, md5hex, sha256hex, actualSize, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1854,7 +1857,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
isCompressed = true
|
||||
}
|
||||
|
||||
hashReader, err := hash.NewReader(reader, size, md5hex, sha256hex, actualSize)
|
||||
hashReader, err := hash.NewReader(reader, size, md5hex, sha256hex, actualSize, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1921,7 +1924,8 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
info := ObjectInfo{Size: size}
|
||||
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", size) // do not try to verify encrypted content
|
||||
// do not try to verify encrypted content
|
||||
hashReader, err = hash.NewReader(reader, info.EncryptedSize(), "", "", size, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1942,10 +1946,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
etag := partInfo.ETag
|
||||
if isCompressed {
|
||||
// Suppress compressed ETag.
|
||||
etag = partInfo.ETag + "-1"
|
||||
} else if isEncrypted {
|
||||
if isEncrypted {
|
||||
etag = tryDecryptETag(objectEncryptionKey, partInfo.ETag, crypto.SSEC.IsRequested(r.Header))
|
||||
}
|
||||
w.Header()["ETag"] = []string{"\"" + etag + "\""}
|
||||
@@ -2235,11 +2236,6 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
// Complete parts.
|
||||
var completeParts []CompletePart
|
||||
for _, part := range complMultipartUpload.Parts {
|
||||
// Avoiding for gateway parts.
|
||||
// `strings.TrimPrefix` does not work here as intended. So `Replace` is used instead.
|
||||
if objectAPI.IsCompressionSupported() {
|
||||
part.ETag = strings.Replace(part.ETag, "-1", "", -1) // For compressed multiparts, We append '-1' for part.ETag.
|
||||
}
|
||||
part.ETag = canonicalizeETag(part.ETag)
|
||||
if isEncrypted {
|
||||
// ETag is stored in the backend in encrypted form. Validate client sent ETag with
|
||||
|
||||
@@ -1916,7 +1916,7 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
||||
for i, input := range putObjectInputs {
|
||||
// uploading the object.
|
||||
var objInfo ObjectInfo
|
||||
objInfo, err = obj.PutObject(context.Background(), input.bucketName, input.objectName, mustGetPutObjReader(t, bytes.NewBuffer(input.textData), input.contentLength, input.metaData[""], ""), ObjectOptions{UserDefined: input.metaData})
|
||||
objInfo, err = obj.PutObject(context.Background(), input.bucketName, input.objectName, mustGetPutObjReader(t, bytes.NewBuffer(input.textData), input.contentLength, input.md5sum, ""), ObjectOptions{UserDefined: input.metaData})
|
||||
// if object upload fails stop the test.
|
||||
if err != nil {
|
||||
t.Fatalf("Put Object case %d: Error uploading object: <ERROR> %v", i+1, err)
|
||||
@@ -2233,11 +2233,6 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
||||
t.Fatalf("Test %d: %s: Failed to parse the CopyObjectResult response: <ERROR> %s", i+1, instanceType, err)
|
||||
}
|
||||
|
||||
retag := canonicalizeETag(cpObjResp.ETag)
|
||||
if retag != bytesData[0].md5sum {
|
||||
t.Fatalf("Test %d: %s: Failed to CopyObject: <ERROR> got %s, expected %s", i+1, instanceType, retag, bytesData[0].md5sum)
|
||||
}
|
||||
|
||||
// See if the new object is formed.
|
||||
// testing whether the copy was successful.
|
||||
err = obj.GetObject(context.Background(), testCase.bucketName, testCase.newObjectName, 0, int64(len(bytesData[0].byteData)), buffers[0], "", opts)
|
||||
@@ -2660,10 +2655,7 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
|
||||
}
|
||||
|
||||
// on successful complete multipart operation the s3MD5 for the parts uploaded will be returned.
|
||||
s3MD5, err := getCompleteMultipartMD5(context.Background(), inputParts[3].parts)
|
||||
if err != nil {
|
||||
t.Fatalf("Obtaining S3MD5 failed")
|
||||
}
|
||||
s3MD5 := getCompleteMultipartMD5(inputParts[3].parts)
|
||||
|
||||
// generating the response body content for the success case.
|
||||
successResponse := generateCompleteMultpartUploadResponse(bucketName, objectName, getGetObjectURL("", bucketName, objectName), s3MD5)
|
||||
|
||||
+27
-21
@@ -25,10 +25,9 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
miniogopolicy "github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
miniogopolicy "github.com/minio/minio-go/v6/pkg/policy"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
@@ -61,6 +60,11 @@ func (sys *PolicySys) removeDeletedBuckets(bucketInfos []BucketInfo) {
|
||||
|
||||
// Set - sets policy to given bucket name. If policy is empty, existing policy is removed.
|
||||
func (sys *PolicySys) Set(bucketName string, policy policy.Policy) {
|
||||
if globalIsGateway {
|
||||
// Set policy is a non-op under gateway mode.
|
||||
return
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
@@ -84,9 +88,21 @@ func (sys *PolicySys) IsAllowed(args policy.Args) bool {
|
||||
sys.RLock()
|
||||
defer sys.RUnlock()
|
||||
|
||||
// If policy is available for given bucket, check the policy.
|
||||
if p, found := sys.bucketPolicyMap[args.BucketName]; found {
|
||||
return p.IsAllowed(args)
|
||||
if globalIsGateway {
|
||||
// When gateway is enabled, no cached value
|
||||
// is used to validate bucket policies.
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI != nil {
|
||||
config, err := objAPI.GetBucketPolicy(context.Background(), args.BucketName)
|
||||
if err == nil {
|
||||
return config.IsAllowed(args)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If policy is available for given bucket, check the policy.
|
||||
if p, found := sys.bucketPolicyMap[args.BucketName]; found {
|
||||
return p.IsAllowed(args)
|
||||
}
|
||||
}
|
||||
|
||||
// As policy is not available for given bucket name, returns IsOwner i.e.
|
||||
@@ -135,21 +151,11 @@ func (sys *PolicySys) Init(objAPI ObjectLayer) error {
|
||||
return errInvalidArgument
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// Refresh PolicySys in background.
|
||||
go func() {
|
||||
ticker := time.NewTicker(globalRefreshBucketPolicyInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-GlobalServiceDoneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
sys.refresh(objAPI)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}()
|
||||
// In gateway mode, we don't need to load the policies
|
||||
// from the backend.
|
||||
if globalIsGateway {
|
||||
return nil
|
||||
}
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
defer close(doneCh)
|
||||
|
||||
+2
-2
@@ -20,8 +20,8 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
miniogopolicy "github.com/minio/minio-go/pkg/policy"
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
miniogopolicy "github.com/minio/minio-go/v6/pkg/policy"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/policy/condition"
|
||||
)
|
||||
|
||||
+120
-78
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2016, 2017, 2018 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2016-2019 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
slashpath "path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -34,18 +35,19 @@ import (
|
||||
"bytes"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/klauspost/readahead"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
xioutil "github.com/minio/minio/pkg/ioutil"
|
||||
"github.com/minio/minio/pkg/mountinfo"
|
||||
"github.com/ncw/directio"
|
||||
)
|
||||
|
||||
const (
|
||||
diskMinFreeSpace = 900 * humanize.MiByte // Min 900MiB free space.
|
||||
diskMinTotalSpace = diskMinFreeSpace // Min 900MiB total space.
|
||||
maxAllowedIOError = 5
|
||||
posixWriteBlockSize = 4 * humanize.MiByte
|
||||
directioAlignSize = 4096 // DirectIO alignment needs to be 4K. Defined here as directio.AlignSize is defined as 0 in MacOS causing divide by 0 error.
|
||||
diskMinFreeSpace = 900 * humanize.MiByte // Min 900MiB free space.
|
||||
diskMinTotalSpace = diskMinFreeSpace // Min 900MiB total space.
|
||||
maxAllowedIOError = 5
|
||||
readBlockSize = humanize.KiByte * 32 // Default read block size 32KiB.
|
||||
)
|
||||
|
||||
// isValidVolname verifies a volname name in accordance with object
|
||||
@@ -187,10 +189,9 @@ func newPosix(path string) (*posix, error) {
|
||||
p := &posix{
|
||||
connected: true,
|
||||
diskPath: path,
|
||||
// 4MiB buffer pool for posix internal operations.
|
||||
pool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
b := directio.AlignedBlock(posixWriteBlockSize)
|
||||
b := directio.AlignedBlock(readBlockSize)
|
||||
return &b
|
||||
},
|
||||
},
|
||||
@@ -642,6 +643,85 @@ func (s *posix) DeleteVol(volume string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Walk - is a sorted walker which returns file entries in lexically
|
||||
// sorted order, additionally along with metadata about each of those entries.
|
||||
func (s *posix) Walk(volume, dirPath, marker string, recursive bool, leafFile string,
|
||||
readMetadataFn readMetadataFunc, endWalkCh chan struct{}) (ch chan FileInfo, err error) {
|
||||
|
||||
defer func() {
|
||||
if err == errFaultyDisk {
|
||||
atomic.AddInt32(&s.ioErrCount, 1)
|
||||
}
|
||||
}()
|
||||
|
||||
if atomic.LoadInt32(&s.ioErrCount) > maxAllowedIOError {
|
||||
return nil, errFaultyDisk
|
||||
}
|
||||
|
||||
if err = s.checkDiskFound(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify if volume is valid and it exists.
|
||||
volumeDir, err := s.getVolDir(volume)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Stat a volume entry.
|
||||
_, err = os.Stat(volumeDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, errVolumeNotFound
|
||||
} else if isSysErrIO(err) {
|
||||
return nil, errFaultyDisk
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch = make(chan FileInfo)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
listDir := func(volume, dirPath, dirEntry string) (entries []string) {
|
||||
entries, err := s.ListDir(volume, dirPath, -1, leafFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sort.Strings(entries)
|
||||
return filterMatchingPrefix(entries, dirEntry)
|
||||
}
|
||||
|
||||
walkResultCh := startTreeWalk(context.Background(), volume, dirPath, marker, recursive, listDir, endWalkCh)
|
||||
for {
|
||||
walkResult, ok := <-walkResultCh
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var fi FileInfo
|
||||
if hasSuffix(walkResult.entry, slashSeparator) {
|
||||
fi = FileInfo{
|
||||
Volume: volume,
|
||||
Name: walkResult.entry,
|
||||
Mode: os.ModeDir,
|
||||
}
|
||||
} else {
|
||||
buf, err := s.ReadAll(volume, pathJoin(walkResult.entry, leafFile))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
fi = readMetadataFn(buf, volume, walkResult.entry)
|
||||
}
|
||||
select {
|
||||
case ch <- fi:
|
||||
case <-endWalkCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// ListDir - return all the entries at the given directory path.
|
||||
// If an entry is a directory it will be returned with a trailing "/".
|
||||
func (s *posix) ListDir(volume, dirPath string, count int, leafFile string) (entries []string, err error) {
|
||||
@@ -1028,10 +1108,13 @@ func (s *posix) ReadFileStream(volume, path string, offset, length int64) (io.Re
|
||||
if _, err = file.Seek(offset, io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return struct {
|
||||
|
||||
r := struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{Reader: io.LimitReader(file, length), Closer: file}, nil
|
||||
}{Reader: io.LimitReader(file, length), Closer: file}
|
||||
|
||||
return readahead.NewReadCloser(r), nil
|
||||
}
|
||||
|
||||
// CreateFile - creates the file.
|
||||
@@ -1087,7 +1170,7 @@ func (s *posix) CreateFile(volume, path string, fileSize int64, r io.Reader) (er
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := disk.OpenFileDirectIO(filePath, os.O_CREATE|os.O_WRONLY|os.O_EXCL|os.O_SYNC, 0666)
|
||||
w, err := disk.OpenFileDirectIO(filePath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0666)
|
||||
if err != nil {
|
||||
switch {
|
||||
case os.IsPermission(err):
|
||||
@@ -1100,7 +1183,6 @@ func (s *posix) CreateFile(volume, path string, fileSize int64, r io.Reader) (er
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
var e error
|
||||
if fileSize > 0 {
|
||||
@@ -1123,76 +1205,26 @@ func (s *posix) CreateFile(volume, path string, fileSize int64, r io.Reader) (er
|
||||
return err
|
||||
}
|
||||
|
||||
defer w.Close()
|
||||
|
||||
bufp := s.pool.Get().(*[]byte)
|
||||
defer s.pool.Put(bufp)
|
||||
|
||||
buf := *bufp
|
||||
|
||||
// Writes remaining bytes in the buffer.
|
||||
writeRemaining := func(w *os.File, buf []byte) (remainingWritten int, err error) {
|
||||
var n int
|
||||
remaining := len(buf)
|
||||
// The following logic writes the remainging data such that it writes whatever best is possible (aligned buffer)
|
||||
// in O_DIRECT mode and remaining (unaligned buffer) in non-O_DIRECT mode.
|
||||
remainingAligned := (remaining / directioAlignSize) * directioAlignSize
|
||||
remainingAlignedBuf := buf[:remainingAligned]
|
||||
remainingUnalignedBuf := buf[remainingAligned:]
|
||||
if len(remainingAlignedBuf) > 0 {
|
||||
n, err = w.Write(remainingAlignedBuf)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
remainingWritten += n
|
||||
}
|
||||
if len(remainingUnalignedBuf) > 0 {
|
||||
// Write on O_DIRECT fds fail if buffer is not 4K aligned, hence disable O_DIRECT.
|
||||
if err = disk.DisableDirectIO(w); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err = w.Write(remainingUnalignedBuf)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
remainingWritten += n
|
||||
}
|
||||
return remainingWritten, nil
|
||||
written, err := xioutil.CopyAligned(w, r, *bufp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var written int
|
||||
for {
|
||||
var n int
|
||||
n, err = io.ReadFull(r, buf)
|
||||
switch err {
|
||||
case nil:
|
||||
n, err = w.Write(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
written += n
|
||||
case io.ErrUnexpectedEOF:
|
||||
n, err = writeRemaining(w, buf[:n])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
written += n
|
||||
fallthrough
|
||||
case io.EOF:
|
||||
if fileSize != -1 {
|
||||
if written < int(fileSize) {
|
||||
return errLessData
|
||||
}
|
||||
if written > int(fileSize) {
|
||||
return errMoreData
|
||||
}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
if written < fileSize {
|
||||
return errLessData
|
||||
} else if written > fileSize {
|
||||
return errMoreData
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *posix) WriteAll(volume, path string, buf []byte) (err error) {
|
||||
func (s *posix) WriteAll(volume, path string, reader io.Reader) (err error) {
|
||||
defer func() {
|
||||
if err == errFaultyDisk {
|
||||
atomic.AddInt32(&s.ioErrCount, 1)
|
||||
@@ -1210,11 +1242,13 @@ func (s *posix) WriteAll(volume, path string, buf []byte) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = w.Write(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
return w.Close()
|
||||
bufp := s.pool.Get().(*[]byte)
|
||||
defer s.pool.Put(bufp)
|
||||
|
||||
_, err = io.CopyBuffer(w, reader, *bufp)
|
||||
return err
|
||||
}
|
||||
|
||||
// AppendFile - append a byte array at path, if file doesn't exist at
|
||||
@@ -1387,6 +1421,14 @@ func (s *posix) DeleteFile(volume, path string) (err error) {
|
||||
return deleteFile(volumeDir, filePath)
|
||||
}
|
||||
|
||||
func (s *posix) DeleteFileBulk(volume string, paths []string) (errs []error, err error) {
|
||||
errs = make([]error, len(paths))
|
||||
for idx, path := range paths {
|
||||
errs[idx] = s.DeleteFile(volume, path)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RenameFile - rename source path to destination path atomically.
|
||||
func (s *posix) RenameFile(srcVolume, srcPath, dstVolume, dstPath string) (err error) {
|
||||
defer func() {
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
minio "github.com/minio/minio-go"
|
||||
minio "github.com/minio/minio-go/v6"
|
||||
)
|
||||
|
||||
// Test Post Policy parsing and checking conditions
|
||||
|
||||
+15
-12
@@ -28,12 +28,22 @@ import (
|
||||
"time"
|
||||
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
// DefaultRESTTimeout - default RPC timeout is one minute.
|
||||
const DefaultRESTTimeout = 1 * time.Minute
|
||||
|
||||
// NetworkError - error type in case of errors related to http/transport
|
||||
// for ex. connection refused, connection reset, dns resolution failure etc.
|
||||
// All errors returned by storage-rest-server (ex errFileNotFound, errDiskNotFound) are not considered to be network errors.
|
||||
type NetworkError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (n *NetworkError) Error() string {
|
||||
return n.Err.Error()
|
||||
}
|
||||
|
||||
// Client - http based RPC client.
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
@@ -46,7 +56,7 @@ type Client struct {
|
||||
func (c *Client) Call(method string, values url.Values, body io.Reader, length int64) (reply io.ReadCloser, err error) {
|
||||
req, err := http.NewRequest(http.MethodPost, c.url.String()+"/"+method+"?"+values.Encode(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, &NetworkError{err}
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.newAuthToken())
|
||||
@@ -56,7 +66,7 @@ func (c *Client) Call(method string, values url.Values, body io.Reader, length i
|
||||
}
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, &NetworkError{err}
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -100,20 +110,13 @@ func NewClient(url *url.URL, tlsConfig *tls.Config, timeout time.Duration, newAu
|
||||
tr := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: newCustomDialContext(timeout),
|
||||
MaxIdleConnsPerHost: 4096,
|
||||
MaxIdleConns: 4096,
|
||||
IdleConnTimeout: 120 * time.Second,
|
||||
MaxIdleConnsPerHost: 256,
|
||||
IdleConnTimeout: 60 * time.Second,
|
||||
TLSHandshakeTimeout: 30 * time.Second,
|
||||
ExpectContinueTimeout: 10 * time.Second,
|
||||
TLSClientConfig: tlsConfig,
|
||||
DisableCompression: true,
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
// If TLS is enabled configure http2
|
||||
if err := http2.ConfigureTransport(tr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &Client{
|
||||
httpClient: &http.Client{Transport: tr},
|
||||
httpIdleConnsCloser: tr.CloseIdleConnections,
|
||||
|
||||
+18
-18
@@ -101,36 +101,36 @@ ENVIRONMENT VARIABLES:
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio server on "/home/shared" directory.
|
||||
$ {{.HelpName}} /home/shared
|
||||
{{.Prompt}} {{.HelpName}} /home/shared
|
||||
|
||||
2. Start minio server bound to a specific ADDRESS:PORT.
|
||||
$ {{.HelpName}} --address 192.168.1.101:9000 /home/shared
|
||||
{{.Prompt}} {{.HelpName}} --address 192.168.1.101:9000 /home/shared
|
||||
|
||||
3. Start minio server and enable virtual-host-style requests.
|
||||
$ export MINIO_DOMAIN=mydomain.com
|
||||
$ {{.HelpName}} --address mydomain.com:9000 /mnt/export
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_DOMAIN{{.AssignmentOperator}}mydomain.com
|
||||
{{.Prompt}} {{.HelpName}} --address mydomain.com:9000 /mnt/export
|
||||
|
||||
4. Start erasure coded minio server on a node with 64 drives.
|
||||
$ {{.HelpName}} /mnt/export{1...64}
|
||||
{{.Prompt}} {{.HelpName}} /mnt/export{1...64}
|
||||
|
||||
5. Start distributed minio server on an 32 node setup with 32 drives each. Run following command on all the 32 nodes.
|
||||
$ export MINIO_ACCESS_KEY=minio
|
||||
$ export MINIO_SECRET_KEY=miniostorage
|
||||
$ {{.HelpName}} http://node{1...32}.example.com/mnt/export/{1...32}
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}minio
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}miniostorage
|
||||
{{.Prompt}} {{.HelpName}} http://node{1...32}.example.com/mnt/export/{1...32}
|
||||
|
||||
6. Start minio server with edge caching enabled.
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}} /home/shared
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_DRIVES{{.AssignmentOperator}}"/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXCLUDE{{.AssignmentOperator}}"bucket1/*;*.png"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXPIRY{{.AssignmentOperator}}40
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_MAXUSE{{.AssignmentOperator}}80
|
||||
{{.Prompt}} {{.HelpName}} /home/shared
|
||||
|
||||
7. Start minio server with KMS enabled.
|
||||
$ export MINIO_SSE_VAULT_APPROLE_ID=9b56cc08-8258-45d5-24a3-679876769126
|
||||
$ export MINIO_SSE_VAULT_APPROLE_SECRET=4e30c52f-13e4-a6f5-0763-d50e8cb4321f
|
||||
$ export MINIO_SSE_VAULT_ENDPOINT=https://vault-endpoint-ip:8200
|
||||
$ export MINIO_SSE_VAULT_KEY_NAME=my-minio-key
|
||||
$ {{.HelpName}} /home/shared
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SSE_VAULT_APPROLE_ID{{.AssignmentOperator}}9b56cc08-8258-45d5-24a3-679876769126
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SSE_VAULT_APPROLE_SECRET{{.AssignmentOperator}}4e30c52f-13e4-a6f5-0763-d50e8cb4321f
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SSE_VAULT_ENDPOINT{{.AssignmentOperator}}https://vault-endpoint-ip:8200
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SSE_VAULT_KEY_NAME{{.AssignmentOperator}}my-minio-key
|
||||
{{.Prompt}} {{.HelpName}} /home/shared
|
||||
`,
|
||||
}
|
||||
|
||||
|
||||
+1
-5
@@ -18,7 +18,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/xml"
|
||||
@@ -2725,12 +2724,9 @@ func (s *TestSuiteCommon) TestObjectMultipart(c *check) {
|
||||
c.Assert(response.StatusCode, http.StatusOK)
|
||||
var parts []CompletePart
|
||||
for _, part := range completeUploads.Parts {
|
||||
// For compressed objects, we dont treat E-Tag as checksum.
|
||||
part.ETag = strings.Replace(part.ETag, "-1", "", -1)
|
||||
part.ETag = canonicalizeETag(part.ETag)
|
||||
parts = append(parts, part)
|
||||
}
|
||||
etag, err := getCompleteMultipartMD5(context.Background(), parts)
|
||||
c.Assert(err, nil)
|
||||
etag := getCompleteMultipartMD5(parts)
|
||||
c.Assert(canonicalizeETag(response.Header.Get("Etag")), etag)
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ func sumHMAC(key []byte, data []byte) []byte {
|
||||
// extractSignedHeaders extract signed headers from Authorization header
|
||||
func extractSignedHeaders(signedHeaders []string, r *http.Request) (http.Header, APIErrorCode) {
|
||||
reqHeaders := r.Header
|
||||
reqQueries := r.URL.Query()
|
||||
// find whether "host" is part of list of signed headers.
|
||||
// if not return ErrUnsignedHeaders. "host" is mandatory.
|
||||
if !contains(signedHeaders, "host") {
|
||||
@@ -156,6 +157,10 @@ func extractSignedHeaders(signedHeaders []string, r *http.Request) (http.Header,
|
||||
// `host` will not be found in the headers, can be found in r.Host.
|
||||
// but its alway necessary that the list of signed headers containing host in it.
|
||||
val, ok := reqHeaders[http.CanonicalHeaderKey(header)]
|
||||
if !ok {
|
||||
// try to set headers from Query String
|
||||
val, ok = reqQueries[header]
|
||||
}
|
||||
if ok {
|
||||
for _, enc := range val {
|
||||
extractedSignedHeaders.Add(header, enc)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
@@ -149,6 +150,22 @@ func TestExtractSignedHeaders(t *testing.T) {
|
||||
t.Fatalf("Expected the APIErrorCode to be %d, but got %d", ErrNone, errCode)
|
||||
}
|
||||
|
||||
inputQuery := r.URL.Query()
|
||||
// case where some headers need to get from request query
|
||||
signedHeaders = append(signedHeaders, "x-amz-server-side-encryption")
|
||||
// expect to fail with `ErrUnsignedHeaders` because couldn't find some header
|
||||
_, errCode = extractSignedHeaders(signedHeaders, r)
|
||||
if errCode != ErrUnsignedHeaders {
|
||||
t.Fatalf("Expected the APIErrorCode to %d, but got %d", ErrUnsignedHeaders, errCode)
|
||||
}
|
||||
// set headers value through Get parameter
|
||||
inputQuery.Add("x-amz-server-side-encryption", crypto.SSEAlgorithmAES256)
|
||||
r.URL.RawQuery = inputQuery.Encode()
|
||||
_, errCode = extractSignedHeaders(signedHeaders, r)
|
||||
if errCode != ErrNone {
|
||||
t.Fatalf("Expected the APIErrorCode to be %d, but got %d", ErrNone, errCode)
|
||||
}
|
||||
|
||||
// "x-amz-content-sha256" header value from the extracted result.
|
||||
extractedContentSha256 := extractedSignedHeaders.Get("x-amz-content-sha256")
|
||||
// "host" header value from the extracted result.
|
||||
|
||||
+9
-3
@@ -35,7 +35,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
sha256 "github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
@@ -251,13 +251,19 @@ func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region s
|
||||
|
||||
// Save other headers available in the request parameters.
|
||||
for k, v := range req.URL.Query() {
|
||||
key := strings.ToLower(k)
|
||||
|
||||
// Handle the metadata in presigned put query string
|
||||
if strings.Contains(strings.ToLower(k), "x-amz-meta-") {
|
||||
if strings.Contains(key, "x-amz-meta-") {
|
||||
query.Set(k, v[0])
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(key, "x-amz-server-side-") {
|
||||
query.Set(k, v[0])
|
||||
}
|
||||
|
||||
if strings.HasPrefix(strings.ToLower(k), "x-amz") {
|
||||
if strings.HasPrefix(key, "x-amz") {
|
||||
continue
|
||||
}
|
||||
query[k] = v
|
||||
|
||||
@@ -30,6 +30,13 @@ type VolInfo struct {
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
// FilesInfo represent a list of files, additionally
|
||||
// indicates if the list is last.
|
||||
type FilesInfo struct {
|
||||
Files []FileInfo
|
||||
IsTruncated bool
|
||||
}
|
||||
|
||||
// FileInfo - represents file stat information.
|
||||
type FileInfo struct {
|
||||
// Name of the volume.
|
||||
@@ -46,4 +53,12 @@ type FileInfo struct {
|
||||
|
||||
// File mode bits.
|
||||
Mode os.FileMode
|
||||
|
||||
// File metadata
|
||||
Metadata map[string]string
|
||||
|
||||
// All the parts per object.
|
||||
Parts []ObjectPartInfo
|
||||
|
||||
Quorum int
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ type StorageAPI interface {
|
||||
StatVol(volume string) (vol VolInfo, err error)
|
||||
DeleteVol(volume string) (err error)
|
||||
|
||||
// Walk in sorted order directly on disk.
|
||||
Walk(volume, dirPath string, marker string, recursive bool, leafFile string,
|
||||
readMetadataFn readMetadataFunc, endWalkCh chan struct{}) (chan FileInfo, error)
|
||||
|
||||
// File operations.
|
||||
ListDir(volume, dirPath string, count int, leafFile string) ([]string, error)
|
||||
ReadFile(volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error)
|
||||
@@ -47,9 +51,10 @@ type StorageAPI interface {
|
||||
RenameFile(srcVolume, srcPath, dstVolume, dstPath string) error
|
||||
StatFile(volume string, path string) (file FileInfo, err error)
|
||||
DeleteFile(volume string, path string) (err error)
|
||||
DeleteFileBulk(volume string, paths []string) (errs []error, err error)
|
||||
|
||||
// Write all data, syncs the data to disk.
|
||||
WriteAll(volume string, path string, buf []byte) (err error)
|
||||
WriteAll(volume string, path string, reader io.Reader) (err error)
|
||||
|
||||
// Read all.
|
||||
ReadAll(volume string, path string) (buf []byte, err error)
|
||||
|
||||
+69
-28
@@ -21,7 +21,6 @@ import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
@@ -44,32 +43,10 @@ func isNetworkError(err error) bool {
|
||||
if err.Error() == errConnectionStale.Error() {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(err.Error(), "connection reset by peer") {
|
||||
if _, ok := err.(*rest.NetworkError); ok {
|
||||
return true
|
||||
}
|
||||
if uerr, isURLError := err.(*url.Error); isURLError {
|
||||
if uerr.Timeout() {
|
||||
return true
|
||||
}
|
||||
|
||||
err = uerr.Err
|
||||
}
|
||||
|
||||
_, isNetOpError := err.(*net.OpError)
|
||||
return isNetOpError
|
||||
}
|
||||
|
||||
// Attempt to approximate network error with a
|
||||
// typed network error, otherwise default to
|
||||
// errDiskNotFound
|
||||
func toNetworkError(err error) error {
|
||||
if err == nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(err.Error(), "connection reset by peer") {
|
||||
return errNetworkConnReset
|
||||
}
|
||||
return errDiskNotFound
|
||||
return false
|
||||
}
|
||||
|
||||
// Converts rpc.ServerError to underlying error. This function is
|
||||
@@ -81,7 +58,7 @@ func toStorageErr(err error) error {
|
||||
}
|
||||
|
||||
if isNetworkError(err) {
|
||||
return toNetworkError(err)
|
||||
return errDiskNotFound
|
||||
}
|
||||
|
||||
switch err.Error() {
|
||||
@@ -256,11 +233,10 @@ func (client *storageRESTClient) CreateFile(volume, path string, length int64, r
|
||||
}
|
||||
|
||||
// WriteAll - write all data to a file.
|
||||
func (client *storageRESTClient) WriteAll(volume, path string, buffer []byte) error {
|
||||
func (client *storageRESTClient) WriteAll(volume, path string, reader io.Reader) error {
|
||||
values := make(url.Values)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
values.Set(storageRESTFilePath, path)
|
||||
reader := bytes.NewBuffer(buffer)
|
||||
respBody, err := client.call(storageRESTMethodWriteAll, values, reader, -1)
|
||||
defer http.DrainBody(respBody)
|
||||
return err
|
||||
@@ -330,6 +306,43 @@ func (client *storageRESTClient) ReadFile(volume, path string, offset int64, buf
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
func (client *storageRESTClient) Walk(volume, dirPath, marker string, recursive bool, leafFile string,
|
||||
readMetadataFn readMetadataFunc, endWalkCh chan struct{}) (chan FileInfo, error) {
|
||||
values := make(url.Values)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
values.Set(storageRESTDirPath, dirPath)
|
||||
values.Set(storageRESTMarkerPath, marker)
|
||||
values.Set(storageRESTRecursive, strconv.FormatBool(recursive))
|
||||
values.Set(storageRESTLeafFile, leafFile)
|
||||
respBody, err := client.call(storageRESTMethodWalk, values, nil, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := make(chan FileInfo)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
defer http.DrainBody(respBody)
|
||||
|
||||
decoder := gob.NewDecoder(respBody)
|
||||
for {
|
||||
var fi FileInfo
|
||||
if gerr := decoder.Decode(&fi); gerr != nil {
|
||||
// Upon error return
|
||||
return
|
||||
}
|
||||
select {
|
||||
case ch <- fi:
|
||||
case <-endWalkCh:
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// ListDir - lists a directory.
|
||||
func (client *storageRESTClient) ListDir(volume, dirPath string, count int, leafFile string) (entries []string, err error) {
|
||||
values := make(url.Values)
|
||||
@@ -356,6 +369,34 @@ func (client *storageRESTClient) DeleteFile(volume, path string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteFileBulk - deletes files in bulk.
|
||||
func (client *storageRESTClient) DeleteFileBulk(volume string, paths []string) (errs []error, err error) {
|
||||
errs = make([]error, len(paths))
|
||||
values := make(url.Values)
|
||||
values.Set(storageRESTVolume, volume)
|
||||
for _, path := range paths {
|
||||
values.Add(storageRESTFilePath, path)
|
||||
}
|
||||
respBody, err := client.call(storageRESTMethodDeleteFileBulk, values, nil, -1)
|
||||
defer http.DrainBody(respBody)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bulkErrs := bulkErrorsResponse{}
|
||||
gob.NewDecoder(respBody).Decode(&bulkErrs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, dErr := range bulkErrs.Errs {
|
||||
errs[i] = toStorageErr(dErr)
|
||||
}
|
||||
|
||||
return errs, nil
|
||||
}
|
||||
|
||||
// RenameFile - renames a file.
|
||||
func (client *storageRESTClient) RenameFile(srcVolume, srcPath, dstVolume, dstPath string) (err error) {
|
||||
values := make(url.Values)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package cmd
|
||||
|
||||
const storageRESTVersion = "v5"
|
||||
const storageRESTVersion = "v6"
|
||||
const storageRESTPath = minioReservedBucketPath + "/storage/" + storageRESTVersion + "/"
|
||||
|
||||
const (
|
||||
@@ -34,7 +34,9 @@ const (
|
||||
storageRESTMethodReadFile = "readfile"
|
||||
storageRESTMethodReadFileStream = "readfilestream"
|
||||
storageRESTMethodListDir = "listdir"
|
||||
storageRESTMethodWalk = "walk"
|
||||
storageRESTMethodDeleteFile = "deletefile"
|
||||
storageRESTMethodDeleteFileBulk = "deletefilebulk"
|
||||
storageRESTMethodRenameFile = "renamefile"
|
||||
storageRESTMethodGetInstanceID = "getinstanceid"
|
||||
)
|
||||
@@ -50,7 +52,9 @@ const (
|
||||
storageRESTOffset = "offset"
|
||||
storageRESTLength = "length"
|
||||
storageRESTCount = "count"
|
||||
storageRESTMarkerPath = "marker"
|
||||
storageRESTLeafFile = "leaf-file"
|
||||
storageRESTRecursive = "recursive"
|
||||
storageRESTBitrotAlgo = "bitrot-algo"
|
||||
storageRESTBitrotHash = "bitrot-hash"
|
||||
storageRESTInstanceID = "instance-id"
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
@@ -47,6 +48,22 @@ func (s *storageRESTServer) writeErrorResponse(w http.ResponseWriter, err error)
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
type bulkErrorsResponse struct {
|
||||
Errs []error `json:"errors"`
|
||||
}
|
||||
|
||||
func (s *storageRESTServer) writeErrorsResponse(w http.ResponseWriter, errs []error) {
|
||||
resp := bulkErrorsResponse{Errs: make([]error, len(errs))}
|
||||
for idx, err := range errs {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
resp.Errs[idx] = err
|
||||
}
|
||||
gob.NewEncoder(w).Encode(resp)
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// DefaultSkewTime - skew time is 15 minutes between minio peers.
|
||||
const DefaultSkewTime = 15 * time.Minute
|
||||
|
||||
@@ -228,14 +245,7 @@ func (s *storageRESTServer) WriteAllHandler(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, r.ContentLength)
|
||||
_, err := io.ReadFull(r.Body, buf)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = s.storage.WriteAll(volume, filePath, buf)
|
||||
err := s.storage.WriteAll(volume, filePath, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
}
|
||||
@@ -353,6 +363,57 @@ func (s *storageRESTServer) ReadFileStreamHandler(w http.ResponseWriter, r *http
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// readMetadata func provides the function types for reading leaf metadata.
|
||||
type readMetadataFunc func(buf []byte, volume, entry string) FileInfo
|
||||
|
||||
func readMetadata(buf []byte, volume, entry string) FileInfo {
|
||||
m, err := xlMetaV1UnmarshalJSON(context.Background(), buf)
|
||||
if err != nil {
|
||||
return FileInfo{}
|
||||
}
|
||||
return FileInfo{
|
||||
Volume: volume,
|
||||
Name: entry,
|
||||
ModTime: m.Stat.ModTime,
|
||||
Size: m.Stat.Size,
|
||||
Metadata: m.Meta,
|
||||
Parts: m.Parts,
|
||||
Quorum: m.Erasure.DataBlocks,
|
||||
}
|
||||
}
|
||||
|
||||
// WalkHandler - remote caller to start walking at a requested directory path.
|
||||
func (s *storageRESTServer) WalkHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
volume := vars[storageRESTVolume]
|
||||
dirPath := vars[storageRESTDirPath]
|
||||
markerPath := vars[storageRESTMarkerPath]
|
||||
recursive, err := strconv.ParseBool(vars[storageRESTRecursive])
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
leafFile := vars[storageRESTLeafFile]
|
||||
|
||||
endWalkCh := make(chan struct{})
|
||||
defer close(endWalkCh)
|
||||
|
||||
fch, err := s.storage.Walk(volume, dirPath, markerPath, recursive, leafFile, readMetadata, endWalkCh)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
defer w.(http.Flusher).Flush()
|
||||
|
||||
encoder := gob.NewEncoder(w)
|
||||
for fi := range fch {
|
||||
encoder.Encode(&fi)
|
||||
}
|
||||
}
|
||||
|
||||
// ListDirHandler - list a directory.
|
||||
func (s *storageRESTServer) ListDirHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
@@ -391,6 +452,24 @@ func (s *storageRESTServer) DeleteFileHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteFileBulkHandler - delete a file.
|
||||
func (s *storageRESTServer) DeleteFileBulkHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
return
|
||||
}
|
||||
vars := r.URL.Query()
|
||||
volume := vars.Get(storageRESTVolume)
|
||||
filePaths := vars[storageRESTFilePath]
|
||||
|
||||
errs, err := s.storage.DeleteFileBulk(volume, filePaths)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
s.writeErrorsResponse(w, errs)
|
||||
}
|
||||
|
||||
// RenameFileHandler - rename a file.
|
||||
func (s *storageRESTServer) RenameFileHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
@@ -445,8 +524,13 @@ func registerStorageRESTHandlers(router *mux.Router, endpoints EndpointList) {
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath, storageRESTOffset, storageRESTLength)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodListDir).HandlerFunc(httpTraceHdrs(server.ListDirHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTDirPath, storageRESTCount, storageRESTLeafFile)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodWalk).HandlerFunc(httpTraceHdrs(server.WalkHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTDirPath, storageRESTMarkerPath, storageRESTRecursive, storageRESTLeafFile)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodDeleteFile).HandlerFunc(httpTraceHdrs(server.DeleteFileHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodDeleteFileBulk).HandlerFunc(httpTraceHdrs(server.DeleteFileBulkHandler)).
|
||||
Queries(restQueries(storageRESTVolume, storageRESTFilePath)...)
|
||||
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodRenameFile).HandlerFunc(httpTraceHdrs(server.RenameFileHandler)).
|
||||
Queries(restQueries(storageRESTSrcVolume, storageRESTSrcPath, storageRESTDstVolume, storageRESTDstPath)...)
|
||||
subrouter.Methods(http.MethodPost).Path("/" + storageRESTMethodGetInstanceID).HandlerFunc(httpTraceAll(server.GetInstanceID))
|
||||
|
||||
+10
-6
@@ -54,8 +54,8 @@ import (
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio-go/pkg/s3signer"
|
||||
"github.com/minio/minio-go/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v6/pkg/s3signer"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/bpool"
|
||||
@@ -138,7 +138,7 @@ func calculateSignedChunkLength(chunkDataSize int64) int64 {
|
||||
}
|
||||
|
||||
func mustGetPutObjReader(t TestErrHandler, data io.Reader, size int64, md5hex, sha256hex string) *PutObjReader {
|
||||
hr, err := hash.NewReader(data, size, md5hex, sha256hex, size)
|
||||
hr, err := hash.NewReader(data, size, md5hex, sha256hex, size, globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -174,7 +174,7 @@ func prepareFS() (ObjectLayer, string, error) {
|
||||
return obj, fsDirs[0], nil
|
||||
}
|
||||
|
||||
func prepareXL32() (ObjectLayer, []string, error) {
|
||||
func prepareXLSets32() (ObjectLayer, []string, error) {
|
||||
fsDirs1, err := getRandomDisks(16)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -1704,7 +1704,7 @@ func prepareTestBackend(instanceType string) (ObjectLayer, []string, error) {
|
||||
switch instanceType {
|
||||
// Total number of disks for XL sets backend is set to 32.
|
||||
case XLSetsTestStr:
|
||||
return prepareXL32()
|
||||
return prepareXLSets32()
|
||||
// Total number of disks for XL backend is set to 16.
|
||||
case XLTestStr:
|
||||
return prepareXL16()
|
||||
@@ -1927,6 +1927,10 @@ func ExecObjectLayerAPITest(t *testing.T, objAPITest objAPITestType, endpoints [
|
||||
|
||||
globalIAMSys = NewIAMSys()
|
||||
globalIAMSys.Init(objLayer)
|
||||
|
||||
globalPolicySys = NewPolicySys()
|
||||
globalPolicySys.Init(objLayer)
|
||||
|
||||
// initialize the server and obtain the credentials and root.
|
||||
// credentials are necessary to sign the HTTP request.
|
||||
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||
@@ -1982,7 +1986,7 @@ func ExecObjectLayerTest(t TestErrHandler, objTest objTestType) {
|
||||
// Executing the object layer tests for single node setup.
|
||||
objTest(objLayer, FSTestStr, t)
|
||||
|
||||
objLayer, fsDirs, err := prepareXL16()
|
||||
objLayer, fsDirs, err := prepareXLSets32()
|
||||
if err != nil {
|
||||
t.Fatalf("Initialization of object layer failed for XL setup: %s", err)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -127,20 +128,23 @@ func (t TreeWalkPool) Set(params listParams, resultCh chan TreeWalkResult, endWa
|
||||
t.lock.Lock()
|
||||
walks, ok := t.pool[params]
|
||||
if ok {
|
||||
// Trick of filtering without allocating
|
||||
// https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
|
||||
nwalks := walks[:0]
|
||||
// Look for walkInfo, remove it from the walks list.
|
||||
for i, walk := range walks {
|
||||
if walk == walkInfo {
|
||||
walks = append(walks[:i], walks[i+1:]...)
|
||||
for _, walk := range walks {
|
||||
if !reflect.DeepEqual(walk, walkInfo) {
|
||||
nwalks = append(nwalks, walk)
|
||||
}
|
||||
}
|
||||
if len(walks) == 0 {
|
||||
if len(nwalks) == 0 {
|
||||
// No more treeWalk go-routines associated with listParams
|
||||
// hence remove map entry.
|
||||
delete(t.pool, params)
|
||||
} else {
|
||||
// There are more treeWalk go-routines associated with listParams
|
||||
// hence save the list in the map.
|
||||
t.pool[params] = walks
|
||||
t.pool[params] = nwalks
|
||||
}
|
||||
}
|
||||
// Signal the treeWalk go-routine to die.
|
||||
|
||||
+17
-16
@@ -59,7 +59,7 @@ func filterMatchingPrefix(entries []string, prefixEntry string) []string {
|
||||
type ListDirFunc func(bucket, prefixDir, prefixEntry string) (entries []string)
|
||||
|
||||
// treeWalk walks directory tree recursively pushing TreeWalkResult into the channel as and when it encounters files.
|
||||
func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker string, recursive bool, listDir ListDirFunc, resultCh chan TreeWalkResult, endWalkCh chan struct{}, isEnd bool) error {
|
||||
func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker string, recursive bool, listDir ListDirFunc, resultCh chan TreeWalkResult, endWalkCh chan struct{}, isEnd bool) (totalNum int, treeErr error) {
|
||||
// Example:
|
||||
// if prefixDir="one/two/three/" and marker="four/five.txt" treeWalk is recursively
|
||||
// called with prefixDir="one/two/three/four/" and marker="five.txt"
|
||||
@@ -78,7 +78,7 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
|
||||
entries := listDir(bucket, prefixDir, entryPrefixMatch)
|
||||
// For an empty list return right here.
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// example:
|
||||
@@ -90,19 +90,13 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
|
||||
entries = entries[idx:]
|
||||
// For an empty list after search through the entries, return right here.
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
for i, entry := range entries {
|
||||
pentry := pathJoin(prefixDir, entry)
|
||||
isDir := hasSuffix(pentry, slashSeparator)
|
||||
|
||||
leaf := !hasSuffix(pentry, slashSeparator)
|
||||
var leafDir bool
|
||||
if !leaf {
|
||||
leafDir = false
|
||||
}
|
||||
|
||||
isDir := !leafDir && !leaf
|
||||
if i == 0 && markerDir == entry {
|
||||
if !recursive {
|
||||
// Skip as the marker would already be listed in the previous listing.
|
||||
@@ -129,24 +123,31 @@ func doTreeWalk(ctx context.Context, bucket, prefixDir, entryPrefixMatch, marker
|
||||
// markIsEnd is passed to this entry's treeWalk() so that treeWalker.end can be marked
|
||||
// true at the end of the treeWalk stream.
|
||||
markIsEnd := i == len(entries)-1 && isEnd
|
||||
if err := doTreeWalk(ctx, bucket, pentry, prefixMatch, markerArg, recursive,
|
||||
listDir, resultCh, endWalkCh, markIsEnd); err != nil {
|
||||
return err
|
||||
totalFound, err := doTreeWalk(ctx, bucket, pentry, prefixMatch, markerArg, recursive,
|
||||
listDir, resultCh, endWalkCh, markIsEnd)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// A nil totalFound means this is an empty directory that
|
||||
// needs to be sent to the result channel, otherwise continue
|
||||
// to the next entry.
|
||||
if totalFound > 0 {
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// EOF is set if we are at last entry and the caller indicated we at the end.
|
||||
isEOF := ((i == len(entries)-1) && isEnd)
|
||||
select {
|
||||
case <-endWalkCh:
|
||||
return errWalkAbort
|
||||
return 0, errWalkAbort
|
||||
case resultCh <- TreeWalkResult{entry: pentry, end: isEOF}:
|
||||
}
|
||||
}
|
||||
|
||||
// Everything is listed.
|
||||
return nil
|
||||
return len(entries), nil
|
||||
}
|
||||
|
||||
// Initiate a new treeWalk in a goroutine.
|
||||
|
||||
@@ -85,6 +85,3 @@ var errNoSuchPolicy = errors.New("Specified canned policy does not exist")
|
||||
|
||||
// error returned when access is denied.
|
||||
var errAccessDenied = errors.New("Do not have enough permissions to access this resource")
|
||||
|
||||
// errNetworkConnReset - connection reset by peer
|
||||
var errNetworkConnReset = errors.New("connection reset by peer")
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ EXIT STATUS:
|
||||
|
||||
EXAMPLES:
|
||||
1. Check and update minio:
|
||||
$ {{.HelpName}}
|
||||
{{.Prompt}} {{.HelpName}}
|
||||
`,
|
||||
}
|
||||
|
||||
|
||||
@@ -474,3 +474,10 @@ func restQueries(keys ...string) []string {
|
||||
}
|
||||
return accumulator
|
||||
}
|
||||
|
||||
// Reverse the input order of a slice of string
|
||||
func reverseStringSlice(input []string) {
|
||||
for left, right := 0, len(input)-1; left < right; left, right = left+1, right-1 {
|
||||
input[left], input[right] = input[right], input[left]
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user