Share button for public objects (#9162)

master
Egor Rudinsky 5 years ago committed by GitHub
parent a6bdc086a2
commit f7c91eff54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 24
      browser/app/js/browser/selectors.js
  2. 9
      browser/app/js/objects/ShareObjectModal.js
  3. 17
      browser/app/js/objects/__tests__/ShareObjectModal.test.js
  4. 51
      browser/app/js/objects/__tests__/actions.test.js
  5. 26
      browser/app/js/objects/actions.js
  6. 3
      browser/app/js/objects/reducer.js
  7. 1
      cmd/globals.go
  8. 1
      cmd/web-handlers_test.go

@ -0,0 +1,24 @@
/*
* MinIO Cloud Storage (C) 2020 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.
*/
import { createSelector } from "reselect"
export const getServerInfo = state => state.browser.serverInfo
export const hasServerPublicDomain = createSelector(
getServerInfo,
serverInfo => Boolean(serverInfo.info && serverInfo.info.domains && serverInfo.info.domains.length),
)

@ -77,7 +77,8 @@ export class ShareObjectModal extends React.Component {
hideShareObject() hideShareObject()
} }
render() { render() {
const { shareObjectDetails, shareObject, hideShareObject } = this.props const { shareObjectDetails, hideShareObject } = this.props
const url = `${window.location.protocol}//${shareObjectDetails.url}`
return ( return (
<Modal <Modal
show={true} show={true}
@ -93,10 +94,11 @@ export class ShareObjectModal extends React.Component {
type="text" type="text"
ref={node => (this.copyTextInput = node)} ref={node => (this.copyTextInput = node)}
readOnly="readOnly" readOnly="readOnly"
value={window.location.protocol + "//" + shareObjectDetails.url} value={url}
onClick={() => this.copyTextInput.select()} onClick={() => this.copyTextInput.select()}
/> />
</div> </div>
{shareObjectDetails.showExpiryDate && (
<div <div
className="input-group" className="input-group"
style={{ display: web.LoggedIn() ? "block" : "none" }} style={{ display: web.LoggedIn() ? "block" : "none" }}
@ -174,10 +176,11 @@ export class ShareObjectModal extends React.Component {
</div> </div>
</div> </div>
</div> </div>
)}
</ModalBody> </ModalBody>
<div className="modal-footer"> <div className="modal-footer">
<CopyToClipboard <CopyToClipboard
text={window.location.protocol + "//" + shareObjectDetails.url} text={url}
onCopy={this.onUrlCopied.bind(this)} onCopy={this.onUrlCopied.bind(this)}
> >
<button className="btn btn-success">Copy Link</button> <button className="btn btn-success">Copy Link</button>

@ -34,7 +34,7 @@ describe("ShareObjectModal", () => {
shallow( shallow(
<ShareObjectModal <ShareObjectModal
object={{ name: "obj1" }} object={{ name: "obj1" }}
shareObjectDetails={{ show: true, object: "obj1", url: "test" }} shareObjectDetails={{ show: true, object: "obj1", url: "test", showExpiryDate: true }}
/> />
) )
}) })
@ -44,7 +44,7 @@ describe("ShareObjectModal", () => {
const wrapper = shallow( const wrapper = shallow(
<ShareObjectModal <ShareObjectModal
object={{ name: "obj1" }} object={{ name: "obj1" }}
shareObjectDetails={{ show: true, object: "obj1", url: "test" }} shareObjectDetails={{ show: true, object: "obj1", url: "test", showExpiryDate: true }}
hideShareObject={hideShareObject} hideShareObject={hideShareObject}
/> />
) )
@ -59,7 +59,7 @@ describe("ShareObjectModal", () => {
const wrapper = shallow( const wrapper = shallow(
<ShareObjectModal <ShareObjectModal
object={{ name: "obj1" }} object={{ name: "obj1" }}
shareObjectDetails={{ show: true, object: "obj1", url: "test" }} shareObjectDetails={{ show: true, object: "obj1", url: "test", showExpiryDate: true }}
/> />
) )
expect( expect(
@ -76,7 +76,7 @@ describe("ShareObjectModal", () => {
const wrapper = shallow( const wrapper = shallow(
<ShareObjectModal <ShareObjectModal
object={{ name: "obj1" }} object={{ name: "obj1" }}
shareObjectDetails={{ show: true, object: "obj1", url: "test" }} shareObjectDetails={{ show: true, object: "obj1", url: "test", showExpiryDate: true }}
hideShareObject={hideShareObject} hideShareObject={hideShareObject}
showCopyAlert={showCopyAlert} showCopyAlert={showCopyAlert}
/> />
@ -89,8 +89,15 @@ describe("ShareObjectModal", () => {
describe("Update expiry values", () => { describe("Update expiry values", () => {
const props = { const props = {
object: { name: "obj1" }, object: { name: "obj1" },
shareObjectDetails: { show: true, object: "obj1", url: "test" } shareObjectDetails: { show: true, object: "obj1", url: "test", showExpiryDate: true }
} }
it("should not show expiry values if shared with public link", () => {
const shareObjectDetails = { show: true, object: "obj1", url: "test", showExpiryDate: false }
const wrapper = shallow(<ShareObjectModal {...props} shareObjectDetails={shareObjectDetails} />)
expect(wrapper.find('.set-expire').exists()).toEqual(false)
})
it("should have default expiry values", () => { it("should have default expiry values", () => {
const wrapper = shallow(<ShareObjectModal {...props} />) const wrapper = shallow(<ShareObjectModal {...props} />)
expect(wrapper.state("expiry")).toEqual({ expect(wrapper.state("expiry")).toEqual({

@ -34,6 +34,7 @@ jest.mock("../../web", () => ({
.mockReturnValueOnce(false) .mockReturnValueOnce(false)
.mockReturnValueOnce(true) .mockReturnValueOnce(true)
.mockReturnValueOnce(true) .mockReturnValueOnce(true)
.mockReturnValueOnce(true)
.mockReturnValueOnce(false), .mockReturnValueOnce(false),
ListObjects: jest.fn(({ bucketName }) => { ListObjects: jest.fn(({ bucketName }) => {
if (bucketName === "test-deny") { if (bucketName === "test-deny") {
@ -69,6 +70,13 @@ jest.mock("../../web", () => ({
}) })
.mockImplementationOnce(() => { .mockImplementationOnce(() => {
return Promise.resolve({ token: "test" }) return Promise.resolve({ token: "test" })
}),
GetBucketPolicy: jest.fn(({ bucketName, prefix }) => {
if (!bucketName) {
return Promise.reject({ message: "Invalid bucket" })
}
if (bucketName === 'test-public') return Promise.resolve({ policy: 'readonly' })
return Promise.resolve({})
}) })
})) }))
@ -295,7 +303,8 @@ describe("Objects actions", () => {
type: "objects/SET_SHARE_OBJECT", type: "objects/SET_SHARE_OBJECT",
show: true, show: true,
object: "b.txt", object: "b.txt",
url: "test" url: "test",
showExpiryDate: true
} }
] ]
store.dispatch(actionsObjects.showShareObject("b.txt", "test")) store.dispatch(actionsObjects.showShareObject("b.txt", "test"))
@ -321,14 +330,16 @@ describe("Objects actions", () => {
it("creates objects/SET_SHARE_OBJECT when object is shared", () => { it("creates objects/SET_SHARE_OBJECT when object is shared", () => {
const store = mockStore({ const store = mockStore({
buckets: { currentBucket: "bk1" }, buckets: { currentBucket: "bk1" },
objects: { currentPrefix: "pre1/" } objects: { currentPrefix: "pre1/" },
browser: { serverInfo: {} },
}) })
const expectedActions = [ const expectedActions = [
{ {
type: "objects/SET_SHARE_OBJECT", type: "objects/SET_SHARE_OBJECT",
show: true, show: true,
object: "a.txt", object: "a.txt",
url: "https://test.com/bk1/pre1/b.txt" url: "https://test.com/bk1/pre1/b.txt",
showExpiryDate: true
}, },
{ {
type: "alert/SET", type: "alert/SET",
@ -347,10 +358,42 @@ describe("Objects actions", () => {
}) })
}) })
it("creates objects/SET_SHARE_OBJECT when object is shared with public link", () => {
const store = mockStore({
buckets: { currentBucket: "test-public" },
objects: { currentPrefix: "pre1/" },
browser: { serverInfo: { info: { domains: ['public.com'] }} },
})
const expectedActions = [
{
type: "objects/SET_SHARE_OBJECT",
show: true,
object: "a.txt",
url: "public.com/test-public/pre1/a.txt",
showExpiryDate: false
},
{
type: "alert/SET",
alert: {
type: "success",
message: "Object shared.",
id: alertActions.alertId
}
}
]
return store
.dispatch(actionsObjects.shareObject("a.txt", 1, 0, 0))
.then(() => {
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
})
it("creates alert/SET when shareObject is failed", () => { it("creates alert/SET when shareObject is failed", () => {
const store = mockStore({ const store = mockStore({
buckets: { currentBucket: "" }, buckets: { currentBucket: "" },
objects: { currentPrefix: "pre1/" } objects: { currentPrefix: "pre1/" },
browser: { serverInfo: {} },
}) })
const expectedActions = [ const expectedActions = [
{ {

@ -24,7 +24,6 @@ import {
import { getCurrentBucket } from "../buckets/selectors" import { getCurrentBucket } from "../buckets/selectors"
import { getCurrentPrefix, getCheckedList } from "./selectors" import { getCurrentPrefix, getCheckedList } from "./selectors"
import * as alertActions from "../alert/actions" import * as alertActions from "../alert/actions"
import * as bucketActions from "../buckets/actions"
import { import {
minioBrowserPrefix, minioBrowserPrefix,
SORT_BY_NAME, SORT_BY_NAME,
@ -33,6 +32,7 @@ import {
SORT_ORDER_ASC, SORT_ORDER_ASC,
SORT_ORDER_DESC, SORT_ORDER_DESC,
} from "../constants" } from "../constants"
import { getServerInfo, hasServerPublicDomain } from '../browser/selectors'
export const SET_LIST = "objects/SET_LIST" export const SET_LIST = "objects/SET_LIST"
export const RESET_LIST = "objects/RESET_LIST" export const RESET_LIST = "objects/RESET_LIST"
@ -222,19 +222,38 @@ export const deleteCheckedObjects = () => {
export const shareObject = (object, days, hours, minutes) => { export const shareObject = (object, days, hours, minutes) => {
return function (dispatch, getState) { return function (dispatch, getState) {
const hasServerDomain = hasServerPublicDomain(getState())
const currentBucket = getCurrentBucket(getState()) const currentBucket = getCurrentBucket(getState())
const currentPrefix = getCurrentPrefix(getState()) const currentPrefix = getCurrentPrefix(getState())
const objectName = `${currentPrefix}${object}` const objectName = `${currentPrefix}${object}`
const expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 const expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60
if (web.LoggedIn()) { if (web.LoggedIn()) {
return web
.GetBucketPolicy({ bucketName: currentBucket, prefix: currentPrefix })
.catch(() => ({ policy: null }))
.then(({ policy }) => {
if (hasServerDomain && ['readonly', 'readwrite'].includes(policy)) {
const domain = getServerInfo(getState()).info.domains[0]
const url = `${domain}/${currentBucket}/${encodeURI(objectName)}`
dispatch(showShareObject(object, url, false))
dispatch(
alertActions.set({
type: "success",
message: "Object shared."
})
)
} else {
return web return web
.PresignedGet({ .PresignedGet({
host: location.host, host: location.host,
bucket: currentBucket, bucket: currentBucket,
object: objectName, object: objectName,
expiry: expiry, expiry: expiry
})
}
}) })
.then((obj) => { .then((obj) => {
if (!obj) return
dispatch(showShareObject(object, obj.url)) dispatch(showShareObject(object, obj.url))
dispatch( dispatch(
alertActions.set({ alertActions.set({
@ -272,11 +291,12 @@ export const shareObject = (object, days, hours, minutes) => {
} }
} }
export const showShareObject = (object, url) => ({ export const showShareObject = (object, url, showExpiryDate = true) => ({
type: SET_SHARE_OBJECT, type: SET_SHARE_OBJECT,
show: true, show: true,
object, object,
url, url,
showExpiryDate,
}) })
export const hideShareObject = (object, url) => ({ export const hideShareObject = (object, url) => ({

@ -89,7 +89,8 @@ export default (
shareObject: { shareObject: {
show: action.show, show: action.show,
object: action.object, object: action.object,
url: action.url url: action.url,
showExpiryDate: action.showExpiryDate
} }
} }
case actionsObjects.CHECKED_LIST_ADD: case actionsObjects.CHECKED_LIST_ADD:

@ -291,6 +291,7 @@ var (
func getGlobalInfo() (globalInfo map[string]interface{}) { func getGlobalInfo() (globalInfo map[string]interface{}) {
globalInfo = map[string]interface{}{ globalInfo = map[string]interface{}{
"serverRegion": globalServerRegion, "serverRegion": globalServerRegion,
"domains": globalDomainNames,
// Add more relevant global settings here. // Add more relevant global settings here.
} }

@ -242,6 +242,7 @@ func testServerInfoWebHandler(obj ObjectLayer, instanceType string, t TestErrHan
if serverInfoReply.MinioVersion != Version { if serverInfoReply.MinioVersion != Version {
t.Fatalf("Cannot get minio version from server info handler") t.Fatalf("Cannot get minio version from server info handler")
} }
serverInfoReply.MinioGlobalInfo["domains"] = []string(nil)
globalInfo := getGlobalInfo() globalInfo := getGlobalInfo()
if !reflect.DeepEqual(serverInfoReply.MinioGlobalInfo, globalInfo) { if !reflect.DeepEqual(serverInfoReply.MinioGlobalInfo, globalInfo) {
t.Fatalf("Global info did not match got %#v, expected %#v", serverInfoReply.MinioGlobalInfo, globalInfo) t.Fatalf("Global info did not match got %#v, expected %#v", serverInfoReply.MinioGlobalInfo, globalInfo)

Loading…
Cancel
Save