From 52d6678bf0d1442d0ab9b7bc63b47ceda69d0e4a Mon Sep 17 00:00:00 2001 From: Rushan Date: Thu, 23 Feb 2017 04:21:38 +0530 Subject: [PATCH] Browser: Implement multi select user interface for object listings (#3730) --- browser/app/js/actions.js | 57 +++- browser/app/js/components/Browse.js | 43 ++- browser/app/js/components/Dropzone.js | 3 +- browser/app/js/components/ObjectsList.js | 25 +- browser/app/js/components/Policy.js | 2 +- browser/app/js/components/PolicyInput.js | 2 +- browser/app/js/reducers.js | 17 +- browser/app/less/inc/buttons.less | 4 + browser/app/less/inc/header.less | 8 +- browser/app/less/inc/list.less | 335 ++++++++++++++++------- browser/app/less/inc/misc.less | 14 + browser/app/less/inc/mixin.less | 2 +- browser/app/less/inc/variables.less | 11 +- browser/webpack.config.js | 4 + cmd/web-handlers_test.go | 2 +- cmd/web-router.go | 2 +- 16 files changed, 391 insertions(+), 140 deletions(-) diff --git a/browser/app/js/actions.js b/browser/app/js/actions.js index c9b2f72bf..83b6e9f61 100644 --- a/browser/app/js/actions.js +++ b/browser/app/js/actions.js @@ -28,7 +28,6 @@ export const SET_CURRENT_BUCKET = 'SET_CURRENT_BUCKET' export const SET_CURRENT_PATH = 'SET_CURRENT_PATH' export const SET_BUCKETS = 'SET_BUCKETS' export const ADD_BUCKET = 'ADD_BUCKET' -export const ADD_OBJECT = 'ADD_OBJECT' export const SET_VISIBLE_BUCKETS = 'SET_VISIBLE_BUCKETS' export const SET_OBJECTS = 'SET_OBJECTS' export const SET_STORAGE_INFO = 'SET_STORAGE_INFO' @@ -57,6 +56,9 @@ export const SET_SHARE_OBJECT = 'SET_SHARE_OBJECT' export const DELETE_CONFIRMATION = 'DELETE_CONFIRMATION' export const SET_PREFIX_WRITABLE = 'SET_PREFIX_WRITABLE' export const REMOVE_OBJECT = 'REMOVE_OBJECT' +export const CHECKED_OBJECTS_ADD = 'CHECKED_OBJECTS_ADD' +export const CHECKED_OBJECTS_REMOVE = 'CHECKED_OBJECTS_REMOVE' +export const CHECKED_OBJECTS_RESET = 'CHECKED_OBJECTS_RESET' export const showDeleteConfirmation = (object) => { return { @@ -304,13 +306,13 @@ export const listObjects = () => { marker: marker }) .then(res => { - let objects = res.objects + let objects = res.objects if (!objects) objects = [] objects = objects.map(object => { - object.name = object.name.replace(`${currentPath}`, ''); - return object - }) + object.name = object.name.replace(`${currentPath}`, ''); + return object + }) dispatch(setObjects(objects, res.nextmarker, res.istruncated)) dispatch(setPrefixWritable(res.writable)) dispatch(setLoadBucket('')) @@ -344,9 +346,9 @@ export const selectPrefix = prefix => { if (!objects) objects = [] objects = objects.map(object => { - object.name = object.name.replace(`${prefix}`, ''); - return object - }) + object.name = object.name.replace(`${prefix}`, ''); + return object + }) dispatch(setObjects( objects, res.nextmarker, @@ -410,6 +412,24 @@ export const setLoginError = () => { } } +export const downloadAllasZip = (url, req, xhr) => { + return (dispatch) => { + xhr.open('POST', url, true) + xhr.responseType = 'blob' + + xhr.onload = function(e) { + if (this.status == 200) { + var blob = new Blob([this.response], { + type: 'application/zip' + }) + var blobUrl = window.URL.createObjectURL(blob); + window.location = blobUrl + } + }; + xhr.send(JSON.stringify(req)); + } +} + export const uploadFile = (file, xhr) => { return (dispatch, getState) => { const {currentBucket, currentPath} = getState() @@ -563,3 +583,24 @@ export const setPolicies = (policies) => { policies } } + +export const checkedObjectsAdd = (objectName) => { + return { + type: CHECKED_OBJECTS_ADD, + objectName + } +} + +export const checkedObjectsRemove = (objectName) => { + return { + type: CHECKED_OBJECTS_REMOVE, + objectName + } +} + +export const checkedObjectsReset = (objectName) => { + return { + type: CHECKED_OBJECTS_RESET, + objectName + } +} \ No newline at end of file diff --git a/browser/app/js/components/Browse.js b/browser/app/js/components/Browse.js index 6458ad22b..ab2c9e0bd 100644 --- a/browser/app/js/components/Browse.js +++ b/browser/app/js/components/Browse.js @@ -27,7 +27,6 @@ import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger' import Tooltip from 'react-bootstrap/lib/Tooltip' import Dropdown from 'react-bootstrap/lib/Dropdown' import MenuItem from 'react-bootstrap/lib/MenuItem' - import InputGroup from '../components/InputGroup' import Dropzone from '../components/Dropzone' import ObjectsList from '../components/ObjectsList' @@ -363,9 +362,27 @@ export default class Browse extends React.Component { } } + checkObject(e, objectName) { + const {dispatch} = this.props + e.target.checked ? dispatch(actions.checkedObjectsAdd(objectName)) : dispatch(actions.checkedObjectsRemove(objectName)) + } + + downloadAll() { + const {dispatch} = this.props + let req = { + bucketName: this.props.currentBucket, + objects: this.props.checkedObjects, + prefix: this.props.currentPath + } + let requestUrl = location.origin + "/minio/zip?token=" + localStorage.token + + this.xhr = new XMLHttpRequest() + dispatch(actions.downloadAllasZip(requestUrl, req, this.xhr)) + } + render() { const {total, free} = this.props.storageInfo - const {showMakeBucketModal, alert, sortNameOrder, sortSizeOrder, sortDateOrder, showAbout, showBucketPolicy} = this.props + const {showMakeBucketModal, alert, sortNameOrder, sortSizeOrder, sortDateOrder, showAbout, showBucketPolicy, checkedObjects} = this.props const {version, memory, platform, runtime} = this.props.serverInfo const {sidebarStatus} = this.props const {showSettings} = this.props @@ -435,7 +452,6 @@ export default class Browse extends React.Component { - } let createButton = '' @@ -490,6 +506,12 @@ export default class Browse extends React.Component { clickOutside={ this.hideSidebar.bind(this) } showPolicy={ this.showBucketPolicy.bind(this) } />
+
0 + })) }> + { checkedObjects.length } Objects selected + +
{ alertBox }
@@ -515,7 +537,8 @@ export default class Browse extends React.Component {
-
+
+
Name
-
+
Size
-
+
Last Modified
-
+
@@ -553,7 +576,9 @@ export default class Browse extends React.Component { + shareObject={ this.shareObject.bind(this) } + checkObject={ this.checkObject.bind(this) } + checkedObjectsArray={ checkedObjects } />
Loading... @@ -734,4 +759,4 @@ export default class Browse extends React.Component {
) } -} +} \ No newline at end of file diff --git a/browser/app/js/components/Dropzone.js b/browser/app/js/components/Dropzone.js index 0ddab2661..f1ff9cc6c 100644 --- a/browser/app/js/components/Dropzone.js +++ b/browser/app/js/components/Dropzone.js @@ -39,11 +39,12 @@ export default class Dropzone extends React.Component { // won't handle child elements correctly. const style = { height: '100%', - borderWidth: '2px', + borderWidth: '0', borderStyle: 'dashed', borderColor: '#fff' } const activeStyle = { + borderWidth: '2px', borderColor: '#777' } const rejectStyle = { diff --git a/browser/app/js/components/ObjectsList.js b/browser/app/js/components/ObjectsList.js index 623594218..a19b7be94 100644 --- a/browser/app/js/components/ObjectsList.js +++ b/browser/app/js/components/ObjectsList.js @@ -20,8 +20,7 @@ import humanize from 'humanize' import connect from 'react-redux/lib/components/connect' import Dropdown from 'react-bootstrap/lib/Dropdown' - -let ObjectsList = ({objects, currentPath, selectPrefix, dataType, showDeleteConfirmation, shareObject, loadPath}) => { +let ObjectsList = ({objects, currentPath, selectPrefix, dataType, showDeleteConfirmation, shareObject, loadPath, checkObject, checkedObjectsArray}) => { const list = objects.map((object, i) => { let size = object.name.endsWith('/') ? '-' : humanize.filesize(object.size) let lastModified = object.name.endsWith('/') ? '-' : Moment(object.lastModified).format('lll') @@ -39,20 +38,30 @@ let ObjectsList = ({objects, currentPath, selectPrefix, dataType, showDeleteConf } + + let activeClass = checkedObjectsArray.indexOf(object.name) > -1 ? ' fesl-row-selected' : '' + return ( -
-
+
+
+
+ checkObject(e, object.name) } /> + + +
+
+ -
+
{ size }
-
+
{ lastModified }
-
+
{ actionButtons }
@@ -72,4 +81,4 @@ export default connect(state => { currentPath: state.currentPath, loadPath: state.loadPath } -})(ObjectsList) +})(ObjectsList) \ No newline at end of file diff --git a/browser/app/js/components/Policy.js b/browser/app/js/components/Policy.js index 65930ad64..cdf95eedf 100644 --- a/browser/app/js/components/Policy.js +++ b/browser/app/js/components/Policy.js @@ -77,4 +77,4 @@ class Policy extends Component { } } -export default connect(state => state)(Policy) +export default connect(state => state)(Policy) \ No newline at end of file diff --git a/browser/app/js/components/PolicyInput.js b/browser/app/js/components/PolicyInput.js index 75809df96..9353bde41 100644 --- a/browser/app/js/components/PolicyInput.js +++ b/browser/app/js/components/PolicyInput.js @@ -80,4 +80,4 @@ class PolicyInput extends Component { } } -export default connect(state => state)(PolicyInput) +export default connect(state => state)(PolicyInput) \ No newline at end of file diff --git a/browser/app/js/reducers.js b/browser/app/js/reducers.js index ca312fbe7..5d80d563b 100644 --- a/browser/app/js/reducers.js +++ b/browser/app/js/reducers.js @@ -56,7 +56,8 @@ export default (state = { url: '', expiry: 604800 }, - prefixWritable: false + prefixWritable: false, + checkedObjects: [] }, action) => { let newState = Object.assign({}, state) switch (action.type) { @@ -185,6 +186,20 @@ export default (state = { if (idx == -1) break newState.objects = [...newState.objects.slice(0, idx), ...newState.objects.slice(idx + 1)] break + + case actions.CHECKED_OBJECTS_ADD: + newState.checkedObjects = [...newState.checkedObjects, action.objectName] + break + case actions.CHECKED_OBJECTS_REMOVE: + let index = newState.checkedObjects.indexOf(action.objectName) + if (index == -1) break + newState.checkedObjects = [...newState.checkedObjects.slice(0, index), ...newState.checkedObjects.slice(index + 1)] + break + case actions.CHECKED_OBJECTS_RESET: + newState.checkedObjects = [] + break } + console.log(newState.checkedObjects) + return newState } diff --git a/browser/app/less/inc/buttons.less b/browser/app/less/inc/buttons.less index 28131641a..6c04a95da 100644 --- a/browser/app/less/inc/buttons.less +++ b/browser/app/less/inc/buttons.less @@ -35,6 +35,10 @@ width: 100%; } +.btn-white { + .btn-variant(#fff, darken(@text-color, 20%)); +} + .btn-link { .btn-variant(#eee, #545454); } diff --git a/browser/app/less/inc/header.less b/browser/app/less/inc/header.less index e95f05d66..50c619bc1 100644 --- a/browser/app/less/inc/header.less +++ b/browser/app/less/inc/header.less @@ -2,14 +2,13 @@ Header ----------------------------*/ .fe-header { - padding: 45px 55px 20px; - - @media(min-width: @screen-md-min) { + @media(min-width: (@screen-sm-min - 100)) { position: relative; + padding: 40px 40px 20px 45px; } @media(max-width: (@screen-xs-max - 100)) { - padding: 25px 25px 20px; + padding: 20px; } h2 { @@ -239,4 +238,3 @@ } - diff --git a/browser/app/less/inc/list.less b/browser/app/less/inc/list.less index d3ee399b1..c38dd2c66 100644 --- a/browser/app/less/inc/list.less +++ b/browser/app/less/inc/list.less @@ -2,17 +2,19 @@ Row ----------------------------*/ .fesl-row { - padding-right: 40px; - padding-top: 5px; - padding-bottom: 5px; position: relative; @media (min-width: (@screen-sm-min - 100px)) { + padding: 5px 20px 5px 40px; display: flex; flex-flow: row nowrap; justify-content: space-between; } + @media(max-width: (@screen-xs-max - 100px)) { + padding: 5px 20px; + } + .clearfix(); } @@ -20,7 +22,7 @@ header.fesl-row { @media (min-width:(@screen-sm-min - 100px)) { margin-bottom: 20px; border-bottom: 1px solid lighten(@text-muted-color, 20%); - padding-left: 40px; + padding-left: 30px; .fesl-item, .fesli-sort { @@ -42,7 +44,7 @@ header.fesl-row { font-size: 14px; } - &:hover:not(.fi-actions) { + &:hover:not(.fesl-item-actions) { background: lighten(@text-muted-color, 22%); color: @dark-gray; @@ -58,54 +60,42 @@ header.fesl-row { } } +.list-type(@background, @icon) { + .fis-icon { + background-color: @background; + + &:before { + content: @icon; + } + } +} + div.fesl-row { - padding-left: 85px; border-bottom: 1px solid transparent; cursor: default; + .transition(background-color); + .transition-duration(500ms); @media (max-width: (@screen-xs-max - 100px)) { - padding-left: 70px; - padding-right: 45px; + padding: 5px 20px; } - &:nth-child(even) { - background-color: #fafafa; + &:not(.fesl-row-selected) { + &:nth-child(even) { + background-color: @list-row-even-bg; + } } &:hover { - background-color: #fbf7dc; - } - - &[data-type]:before { - font-family: @font-family-icon; - width: 35px; - height: 35px; - text-align: center; - line-height: 35px; - position: absolute; - border-radius: 50%; - font-size: 16px; - left: 50px; - top: 9px; - color: @white; - - @media (max-width: (@screen-xs-max - 100px)) { - left: 20px; + .fis-icon { + &:before { + .opacity(0) + } } - } - - &[data-type="folder"] { - @media (max-width: (@screen-xs-max - 100px)) { - .fesl-item { - &.fi-name { - padding-top: 10px; - padding-bottom: 7px; - } - &.fi-size, - &.fi-modified { - display: none; - } + .fis-helper { + &:before { + .opacity(1); } } } @@ -113,54 +103,18 @@ div.fesl-row { /*-------------------------- Icons ----------------------------*/ - &[data-type=folder]:before { - content: '\f114'; - background-color: #a1d6dd; - } - &[data-type=pdf]:before { - content: "\f1c1"; - background-color: #fa7775; - } - &[data-type=zip]:before { - content: "\f1c6"; - background-color: #427089; - } - &[data-type=audio]:before { - content: "\f1c7"; - background-color: #009688 - } - &[data-type=code]:before { - content: "\f1c9"; - background-color: #997867; - } - &[data-type=excel]:before { - content: "\f1c3"; - background-color: #64c866; - } - &[data-type=image]:before { - content: "\f1c5"; - background-color: #f06292; - } - &[data-type=video]:before { - content: "\f1c8"; - background-color: #f8c363; - } - &[data-type=other]:before { - content: "\f016"; - background-color: #afafaf; - } - &[data-type=text]:before { - content: "\f0f6"; - background-color: #8a8a8a; - } - &[data-type=doc]:before { - content: "\f1c2"; - background-color: #2196f5; - } - &[data-type=presentation]:before { - content: "\f1c4"; - background-color: #896ea6; - } + &[data-type=folder] { .list-type(#a1d6dd, '\f114'); } + &[data-type=pdf] {.list-type(#fa7775, '\f1c1'); } + &[data-type=zip] { .list-type(#427089, '\f1c6'); } + &[data-type=audio] { .list-type(#009688, '\f1c7'); } + &[data-type=code] { .list-type(#997867, "\f1c9"); } + &[data-type=excel] { .list-type(#f1c3, '\f1c3'); } + &[data-type=image] { .list-type(#f06292, '\f1c5'); } + &[data-type=video] { .list-type(#f8c363, '\f1c8'); } + &[data-type=other] { .list-type(#afafaf, '\f016'); } + &[data-type=text] { .list-type(#8a8a8a, '\f0f6'); } + &[data-type=doc] { .list-type(#2196f5, '\f1c2'); } + &[data-type=presentation] { .list-type(#896ea6, '\f1c4'); } &.fesl-loading{ &:before { @@ -180,6 +134,113 @@ div.fesl-row { } } +.fesl-row-selected { + background-color: @list-row-selected-bg; + + &, .fesl-item a { + color: darken(@text-color, 10%); + } +} + +.fi-select { + float: left; + position: relative; + width: 35px; + height: 35px; + margin: 3px 0; + + @media(max-width: (@screen-xs-max - 100px)) { + margin-right: 15px; + } + + input { + position: absolute; + left: 0; + top: 0; + width: 35px; + height: 35px; + z-index: 20; + opacity: 0; + cursor: pointer; + + &:checked { + & ~ .fis-icon { + background-color: #32393F; + + &:before { + opacity: 0; + } + } + + & ~ .fis-helper { + &:before { + .scale(0); + } + + &:after { + .scale(1); + } + } + } + } +} + +.fis-icon { + display: inline-block; + vertical-align: top; + border-radius: 50%; + width: 35px; + height: 35px; + .transition(background-color); + .transition-duration(250ms); + + &:before { + width: 100%; + height: 100%; + text-align: center; + position: absolute; + border-radius: 50%; + font-family: @font-family-icon; + line-height: 35px; + font-size: 16px; + color: @white; + .transition(all); + .transition-duration(300ms); + font-style: normal; + } +} + +.fis-helper { + &:before, + &:after { + position: absolute; + .transition(all); + .transition-duration(250ms); + } + + &:before { + content: ''; + width: 15px; + height: 15px; + border: 2px solid @white; + z-index: 10; + border-radius: 2px; + top: 10px; + left: 10px; + opacity: 0; + } + + &:after { + font-family: @font-family-icon; + content: '\f00c'; + top: 8px; + left: 9px; + color: @white; + font-size: 14px; + .scale(0); + } +} + /*-------------------------- Files and Folders @@ -192,26 +253,26 @@ div.fesl-row { } @media(min-width: (@screen-sm-min - 100px)) { - &:not(.fi-actions) { + &:not(.fesl-item-actions):not(.fesl-item-icon) { text-overflow: ellipsis; padding: 10px 15px; white-space: nowrap; overflow: hidden; } - &.fi-name { + &.fesl-item-name { flex: 3; } - &.fi-size { + &.fesl-item-size { width: 140px; } - &.fi-modified { + &.fesl-item-modified { width: 190px; } - &.fi-actions { + &.fesl-item-actions { width: 40px; } } @@ -219,29 +280,29 @@ div.fesl-row { @media(max-width: (@screen-xs-max - 100px)) { padding: 0; - &.fi-name { + &.fesl-item-name { width: 100%; margin-bottom: 3px; } - &.fi-size, - &.fi-modified { + &.fesl-item-size, + &.fesl-item-modified { font-size: 12px; color: #B5B5B5; float: left; } - &.fi-modified { + &.fesl-item-modified { max-width: 72px; white-space: nowrap; overflow: hidden; } - &.fi-size { + &.fesl-item-size { margin-right: 10px; } - &.fi-actions { + &.fesl-item-actions { position: absolute; top: 5px; right: 10px; @@ -266,7 +327,7 @@ div.fesl-row { } } -.fi-actions { +.fesl-item-actions { .dropdown-menu { background-color: transparent; box-shadow: none; @@ -324,6 +385,78 @@ div.fesl-row { } } +.list-actions { + position: fixed; + .translate3d(0, -100%, 0); + .opacity(0); + .transition(all); + .transition-duration(200ms); + padding: 20px 25px; + top: 0; + left: 0; + width: 100%; + background-color: @brand-primary; + z-index: 20; + box-shadow: 0 0 10px rgba(0,0,0,0.3); + text-align: center; + + &.list-actions-toggled { + .translate3d(0, 0, 0); + .opacity(1); + } +} + +.la-close { + position: absolute; + right: 20px; + top: 0; + color: #fff; + width: 30px; + height: 30px; + border-radius: 50%; + text-align: center; + line-height: 30px !important; + background: rgba(255, 255, 255, 0.1); + font-weight: normal; + bottom: 0; + margin: auto; + cursor: pointer; + + &:hover { + background-color: rgba(255, 255, 255, 0.2); + } +} + +.la-label { + color: @white; + float: left; + padding: 4px 0; + + .fa { + font-size: 22px; + vertical-align: top; + margin-right: 10px; + margin-top: -1px; + } +} + +.la-actions { + button { + background-color: transparent; + border: 2px solid rgba(255,255,255,0.9); + color: @white; + border-radius: 2px; + padding: 5px 10px; + font-size: 13px; + .transition(all); + .transition-duration(300ms); + + &:hover { + background-color: @white; + color: @brand-primary; + } + } +} @-webkit-keyframes fiad-action-anim { from { diff --git a/browser/app/less/inc/misc.less b/browser/app/less/inc/misc.less index dba1b43b5..8359d426d 100644 --- a/browser/app/less/inc/misc.less +++ b/browser/app/less/inc/misc.less @@ -99,4 +99,18 @@ content: '7 days'; right: 0; } +} + + +.modal-aheader { + height: 100px; + + &:before { + height: 0 !important; + } + + .modal-dialog { + margin: 0; + vertical-align: top; + } } \ No newline at end of file diff --git a/browser/app/less/inc/mixin.less b/browser/app/less/inc/mixin.less index 528f2f26b..27a764817 100644 --- a/browser/app/less/inc/mixin.less +++ b/browser/app/less/inc/mixin.less @@ -49,4 +49,4 @@ z-index: 1; -webkit-animation: zoomIn 250ms, spin 700ms 250ms infinite linear; animation: zoomIn 250ms, spin 700ms 250ms infinite linear; -} \ No newline at end of file +} diff --git a/browser/app/less/inc/variables.less b/browser/app/less/inc/variables.less index de6589994..8ad938e3d 100644 --- a/browser/app/less/inc/variables.less +++ b/browser/app/less/inc/variables.less @@ -81,7 +81,7 @@ /*------------------------- Colors --------------------------*/ -@brand-primary: #2196F3; +@brand-primary: #2298f7; @brand-success: #4CAF50; @brand-info: #00BCD4; @brand-warning: #FF9800; @@ -91,4 +91,11 @@ /*------------------------- Form --------------------------*/ -@input-border: #eee; \ No newline at end of file +@input-border: #eee; + + +/*------------------------- + List +--------------------------*/ +@list-row-selected-bg: #fbf2bf; +@list-row-even-bg: #fafafa; \ No newline at end of file diff --git a/browser/webpack.config.js b/browser/webpack.config.js index 3ccdaba0b..3c84b329d 100644 --- a/browser/webpack.config.js +++ b/browser/webpack.config.js @@ -71,6 +71,10 @@ var exports = { target: 'http://localhost:9000', secure: false }, + '/minio/zip': { + target: 'http://localhost:9000', + secure: false + } } }, plugins: [ diff --git a/cmd/web-handlers_test.go b/cmd/web-handlers_test.go index 66f7cbd52..912200624 100644 --- a/cmd/web-handlers_test.go +++ b/cmd/web-handlers_test.go @@ -849,7 +849,7 @@ func testWebHandlerDownloadZip(obj ObjectLayer, instanceType string, t TestErrHa return 0, nil } var req *http.Request - req, err = http.NewRequest("GET", path, bytes.NewBuffer(argsData)) + req, err = http.NewRequest("POST", path, bytes.NewBuffer(argsData)) if err != nil { t.Fatalf("Cannot create upload request, %v", err) diff --git a/cmd/web-router.go b/cmd/web-router.go index 66505bdff..333e102ba 100644 --- a/cmd/web-router.go +++ b/cmd/web-router.go @@ -84,7 +84,7 @@ func registerWebRouter(mux *router.Router) error { webBrowserRouter.Methods("POST").Path("/webrpc").Handler(webRPC) webBrowserRouter.Methods("PUT").Path("/upload/{bucket}/{object:.+}").HandlerFunc(web.Upload) webBrowserRouter.Methods("GET").Path("/download/{bucket}/{object:.+}").Queries("token", "{token:.*}").HandlerFunc(web.Download) - webBrowserRouter.Methods("GET").Path("/zip").Queries("token", "{token:.*}").HandlerFunc(web.DownloadZip) + webBrowserRouter.Methods("POST").Path("/zip").Queries("token", "{token:.*}").HandlerFunc(web.DownloadZip) // Add compression for assets. compressedAssets := handlers.CompressHandler(http.StripPrefix(minioReservedBucketPath, http.FileServer(assetFS())))