From f7c91eff54c0b477432950e4ff5a07aa9027310a Mon Sep 17 00:00:00 2001 From: Egor Rudinsky Date: Sat, 2 May 2020 09:55:53 +0300 Subject: [PATCH] Share button for public objects (#9162) --- browser/app/js/browser/selectors.js | 24 +++++++++ browser/app/js/objects/ShareObjectModal.js | 11 ++-- .../__tests__/ShareObjectModal.test.js | 17 ++++-- .../app/js/objects/__tests__/actions.test.js | 53 +++++++++++++++++-- browser/app/js/objects/actions.js | 34 +++++++++--- browser/app/js/objects/reducer.js | 3 +- cmd/globals.go | 1 + cmd/web-handlers_test.go | 1 + 8 files changed, 122 insertions(+), 22 deletions(-) create mode 100644 browser/app/js/browser/selectors.js diff --git a/browser/app/js/browser/selectors.js b/browser/app/js/browser/selectors.js new file mode 100644 index 000000000..1742c7e00 --- /dev/null +++ b/browser/app/js/browser/selectors.js @@ -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), +) diff --git a/browser/app/js/objects/ShareObjectModal.js b/browser/app/js/objects/ShareObjectModal.js index ed13bc9e9..e48a0a76f 100644 --- a/browser/app/js/objects/ShareObjectModal.js +++ b/browser/app/js/objects/ShareObjectModal.js @@ -77,7 +77,8 @@ export class ShareObjectModal extends React.Component { hideShareObject() } render() { - const { shareObjectDetails, shareObject, hideShareObject } = this.props + const { shareObjectDetails, hideShareObject } = this.props + const url = `${window.location.protocol}//${shareObjectDetails.url}` return ( (this.copyTextInput = node)} readOnly="readOnly" - value={window.location.protocol + "//" + shareObjectDetails.url} + value={url} onClick={() => this.copyTextInput.select()} /> -
@@ -174,10 +176,11 @@ export class ShareObjectModal extends React.Component {
+ )}
diff --git a/browser/app/js/objects/__tests__/ShareObjectModal.test.js b/browser/app/js/objects/__tests__/ShareObjectModal.test.js index 58edd1aaa..8242ba7ca 100644 --- a/browser/app/js/objects/__tests__/ShareObjectModal.test.js +++ b/browser/app/js/objects/__tests__/ShareObjectModal.test.js @@ -34,7 +34,7 @@ describe("ShareObjectModal", () => { shallow( ) }) @@ -44,7 +44,7 @@ describe("ShareObjectModal", () => { const wrapper = shallow( ) @@ -59,7 +59,7 @@ describe("ShareObjectModal", () => { const wrapper = shallow( ) expect( @@ -76,7 +76,7 @@ describe("ShareObjectModal", () => { const wrapper = shallow( @@ -89,8 +89,15 @@ describe("ShareObjectModal", () => { describe("Update expiry values", () => { const props = { 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() + expect(wrapper.find('.set-expire').exists()).toEqual(false) + }) + it("should have default expiry values", () => { const wrapper = shallow() expect(wrapper.state("expiry")).toEqual({ diff --git a/browser/app/js/objects/__tests__/actions.test.js b/browser/app/js/objects/__tests__/actions.test.js index a769f5fc2..421ebc6f4 100644 --- a/browser/app/js/objects/__tests__/actions.test.js +++ b/browser/app/js/objects/__tests__/actions.test.js @@ -34,6 +34,7 @@ jest.mock("../../web", () => ({ .mockReturnValueOnce(false) .mockReturnValueOnce(true) .mockReturnValueOnce(true) + .mockReturnValueOnce(true) .mockReturnValueOnce(false), ListObjects: jest.fn(({ bucketName }) => { if (bucketName === "test-deny") { @@ -69,7 +70,14 @@ jest.mock("../../web", () => ({ }) .mockImplementationOnce(() => { 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({}) + }) })) const middlewares = [thunk] @@ -295,7 +303,8 @@ describe("Objects actions", () => { type: "objects/SET_SHARE_OBJECT", show: true, object: "b.txt", - url: "test" + url: "test", + showExpiryDate: true } ] store.dispatch(actionsObjects.showShareObject("b.txt", "test")) @@ -321,14 +330,16 @@ describe("Objects actions", () => { it("creates objects/SET_SHARE_OBJECT when object is shared", () => { const store = mockStore({ buckets: { currentBucket: "bk1" }, - objects: { currentPrefix: "pre1/" } + objects: { currentPrefix: "pre1/" }, + browser: { serverInfo: {} }, }) const expectedActions = [ { type: "objects/SET_SHARE_OBJECT", show: true, object: "a.txt", - url: "https://test.com/bk1/pre1/b.txt" + url: "https://test.com/bk1/pre1/b.txt", + showExpiryDate: true }, { 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", () => { const store = mockStore({ buckets: { currentBucket: "" }, - objects: { currentPrefix: "pre1/" } + objects: { currentPrefix: "pre1/" }, + browser: { serverInfo: {} }, }) const expectedActions = [ { diff --git a/browser/app/js/objects/actions.js b/browser/app/js/objects/actions.js index 371beac27..8e71d2d1f 100644 --- a/browser/app/js/objects/actions.js +++ b/browser/app/js/objects/actions.js @@ -24,7 +24,6 @@ import { import { getCurrentBucket } from "../buckets/selectors" import { getCurrentPrefix, getCheckedList } from "./selectors" import * as alertActions from "../alert/actions" -import * as bucketActions from "../buckets/actions" import { minioBrowserPrefix, SORT_BY_NAME, @@ -33,6 +32,7 @@ import { SORT_ORDER_ASC, SORT_ORDER_DESC, } from "../constants" +import { getServerInfo, hasServerPublicDomain } from '../browser/selectors' export const SET_LIST = "objects/SET_LIST" export const RESET_LIST = "objects/RESET_LIST" @@ -222,19 +222,38 @@ export const deleteCheckedObjects = () => { export const shareObject = (object, days, hours, minutes) => { return function (dispatch, getState) { + const hasServerDomain = hasServerPublicDomain(getState()) const currentBucket = getCurrentBucket(getState()) const currentPrefix = getCurrentPrefix(getState()) const objectName = `${currentPrefix}${object}` const expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 if (web.LoggedIn()) { return web - .PresignedGet({ - host: location.host, - bucket: currentBucket, - object: objectName, - expiry: expiry, + .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 + .PresignedGet({ + host: location.host, + bucket: currentBucket, + object: objectName, + expiry: expiry + }) + } }) .then((obj) => { + if (!obj) return dispatch(showShareObject(object, obj.url)) dispatch( 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, show: true, object, url, + showExpiryDate, }) export const hideShareObject = (object, url) => ({ diff --git a/browser/app/js/objects/reducer.js b/browser/app/js/objects/reducer.js index 801039c4e..91f5fc58d 100644 --- a/browser/app/js/objects/reducer.js +++ b/browser/app/js/objects/reducer.js @@ -89,7 +89,8 @@ export default ( shareObject: { show: action.show, object: action.object, - url: action.url + url: action.url, + showExpiryDate: action.showExpiryDate } } case actionsObjects.CHECKED_LIST_ADD: diff --git a/cmd/globals.go b/cmd/globals.go index fd97224d9..0bbf7164d 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -291,6 +291,7 @@ var ( func getGlobalInfo() (globalInfo map[string]interface{}) { globalInfo = map[string]interface{}{ "serverRegion": globalServerRegion, + "domains": globalDomainNames, // Add more relevant global settings here. } diff --git a/cmd/web-handlers_test.go b/cmd/web-handlers_test.go index 17c6baf42..a0a167349 100644 --- a/cmd/web-handlers_test.go +++ b/cmd/web-handlers_test.go @@ -242,6 +242,7 @@ func testServerInfoWebHandler(obj ObjectLayer, instanceType string, t TestErrHan if serverInfoReply.MinioVersion != Version { t.Fatalf("Cannot get minio version from server info handler") } + serverInfoReply.MinioGlobalInfo["domains"] = []string(nil) globalInfo := getGlobalInfo() if !reflect.DeepEqual(serverInfoReply.MinioGlobalInfo, globalInfo) { t.Fatalf("Global info did not match got %#v, expected %#v", serverInfoReply.MinioGlobalInfo, globalInfo)