/* * Minio Cloud Storage (C) 2016 Minio, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import React from 'react' import classNames from 'classnames' import browserHistory from 'react-router/lib/browserHistory' import humanize from 'humanize' import Moment from 'moment' import Modal from 'react-bootstrap/lib/Modal' import ModalBody from 'react-bootstrap/lib/ModalBody' import ModalHeader from 'react-bootstrap/lib/ModalHeader' import Alert from 'react-bootstrap/lib/Alert' 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' import SideBar from '../components/SideBar' import Path from '../components/Path' import BrowserUpdate from '../components/BrowserUpdate' import UploadModal from '../components/UploadModal' import SettingsModal from '../components/SettingsModal' import PolicyInput from '../components/PolicyInput' import Policy from '../components/Policy' import BrowserDropdown from '../components/BrowserDropdown' import ConfirmModal from './ConfirmModal' import logo from '../../img/logo.svg' import * as actions from '../actions' import * as utils from '../utils' import * as mime from '../mime' import { minioBrowserPrefix } from '../constants' import CopyToClipboard from 'react-copy-to-clipboard' import storage from 'local-storage-fallback' import InfiniteScroll from 'react-infinite-scroller'; export default class Browse extends React.Component { componentDidMount() { const {web, dispatch, currentBucket} = this.props if (!web.LoggedIn()) return web.StorageInfo() .then(res => { let storageInfo = Object.assign({}, { total: res.storageInfo.Total, free: res.storageInfo.Free }) storageInfo.used = storageInfo.total - storageInfo.free dispatch(actions.setStorageInfo(storageInfo)) return web.ServerInfo() }) .then(res => { let serverInfo = Object.assign({}, { version: res.MinioVersion, memory: res.MinioMemory, platform: res.MinioPlatform, runtime: res.MinioRuntime, info: res.MinioGlobalInfo }) dispatch(actions.setServerInfo(serverInfo)) }) .catch(err => { dispatch(actions.showAlert({ type: 'danger', message: err.message })) }) } componentWillMount() { const {dispatch} = this.props // Clear out any stale message in the alert of Login page dispatch(actions.showAlert({ type: 'danger', message: '' })) if (web.LoggedIn()) { web.ListBuckets() .then(res => { let buckets if (!res.buckets) buckets = [] else buckets = res.buckets.map(bucket => bucket.name) if (buckets.length) { dispatch(actions.setBuckets(buckets)) dispatch(actions.setVisibleBuckets(buckets)) if (location.pathname === minioBrowserPrefix || location.pathname === minioBrowserPrefix + '/') { browserHistory.push(utils.pathJoin(buckets[0])) } } }) } this.history = browserHistory.listen(({pathname}) => { let decPathname = decodeURI(pathname) if (decPathname === `${minioBrowserPrefix}/login`) return // FIXME: better organize routes and remove this if (!decPathname.endsWith('/')) decPathname += '/' if (decPathname === minioBrowserPrefix + '/') { return } let obj = utils.pathSlice(decPathname) if (!web.LoggedIn()) { dispatch(actions.setBuckets([obj.bucket])) dispatch(actions.setVisibleBuckets([obj.bucket])) } dispatch(actions.selectBucket(obj.bucket, obj.prefix)) }) } componentWillUnmount() { this.history() } selectBucket(e, bucket) { e.preventDefault() if (bucket === this.props.currentBucket) return browserHistory.push(utils.pathJoin(bucket)) } searchBuckets(e) { e.preventDefault() let {buckets} = this.props this.props.dispatch(actions.setVisibleBuckets(buckets.filter(bucket => bucket.indexOf(e.target.value) > -1))) } listObjects() { const {dispatch} = this.props dispatch(actions.listObjects()) } selectPrefix(e, prefix) { e.preventDefault() const {dispatch, currentPath, web, currentBucket} = this.props const encPrefix = encodeURI(prefix) if (prefix.endsWith('/') || prefix === '') { if (prefix === currentPath) return browserHistory.push(utils.pathJoin(currentBucket, encPrefix)) } else { if (!web.LoggedIn()) { let url = `${window.location.origin}/minio/download/${currentBucket}/${encPrefix}?token=''` window.location = url } else { // Download the selected file. web.CreateURLToken() .then(res => { let url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encPrefix}?token=${res.token}` window.location = url }) .catch(err => dispatch(actions.showAlert({ type: 'danger', message: err.message }))) } } } makeBucket(e) { e.preventDefault() const bucketName = this.refs.makeBucketRef.value this.refs.makeBucketRef.value = '' const {web, dispatch} = this.props this.hideMakeBucketModal() web.MakeBucket({ bucketName }) .then(() => { dispatch(actions.addBucket(bucketName)) dispatch(actions.selectBucket(bucketName)) }) .catch(err => dispatch(actions.showAlert({ type: 'danger', message: err.message }))) } hideMakeBucketModal() { const {dispatch} = this.props dispatch(actions.hideMakeBucketModal()) } showMakeBucketModal(e) { e.preventDefault() const {dispatch} = this.props dispatch(actions.showMakeBucketModal()) } showAbout(e) { e.preventDefault() const {dispatch} = this.props dispatch(actions.showAbout()) } hideAbout(e) { e.preventDefault() const {dispatch} = this.props dispatch(actions.hideAbout()) } showBucketPolicy(e) { e.preventDefault() const {dispatch} = this.props dispatch(actions.showBucketPolicy()) } hideBucketPolicy(e) { e.preventDefault() const {dispatch} = this.props dispatch(actions.hideBucketPolicy()) } uploadFile(e) { e.preventDefault() const {dispatch, buckets} = this.props if (buckets.length === 0) { dispatch(actions.showAlert({ type: 'danger', message: "Bucket needs to be created before trying to upload files." })) return } let file = e.target.files[0] e.target.value = null this.xhr = new XMLHttpRequest() dispatch(actions.uploadFile(file, this.xhr)) } removeObject() { const {web, dispatch, currentPath, currentBucket, deleteConfirmation, checkedObjects} = this.props let objects = [] if (checkedObjects.length > 0) { objects = checkedObjects.map(obj => `${currentPath}${obj}`) } else { objects = [deleteConfirmation.object] } web.RemoveObject({ bucketname: currentBucket, objects: objects }) .then(() => { this.hideDeleteConfirmation() if (checkedObjects.length > 0) { for (let i = 0; i < checkedObjects.length; i++) { dispatch(actions.removeObject(checkedObjects[i].replace(currentPath, ''))) } dispatch(actions.checkedObjectsReset()) } else { let delObject = deleteConfirmation.object.replace(currentPath, '') dispatch(actions.removeObject(delObject)) } }) .catch(e => dispatch(actions.showAlert({ type: 'danger', message: e.message }))) } hideAlert(e) { e.preventDefault() const {dispatch} = this.props dispatch(actions.hideAlert()) } showDeleteConfirmation(e, object) { e.preventDefault() const {dispatch} = this.props dispatch(actions.showDeleteConfirmation(object)) } hideDeleteConfirmation() { const {dispatch} = this.props dispatch(actions.hideDeleteConfirmation()) } shareObject(e, object) { e.preventDefault() const {dispatch} = this.props // let expiry = 5 * 24 * 60 * 60 // 5 days expiry by default dispatch(actions.shareObject(object, 5, 0, 0)) } hideShareObjectModal() { const {dispatch} = this.props dispatch(actions.hideShareObject()) } dataType(name, contentType) { return mime.getDataType(name, contentType) } sortObjectsByName(e) { const {dispatch, objects, sortNameOrder} = this.props dispatch(actions.setObjects(utils.sortObjectsByName(objects, !sortNameOrder))) dispatch(actions.setSortNameOrder(!sortNameOrder)) } sortObjectsBySize() { const {dispatch, objects, sortSizeOrder} = this.props dispatch(actions.setObjects(utils.sortObjectsBySize(objects, !sortSizeOrder))) dispatch(actions.setSortSizeOrder(!sortSizeOrder)) } sortObjectsByDate() { const {dispatch, objects, sortDateOrder} = this.props dispatch(actions.setObjects(utils.sortObjectsByDate(objects, !sortDateOrder))) dispatch(actions.setSortDateOrder(!sortDateOrder)) } logout(e) { const {web} = this.props e.preventDefault() web.Logout() browserHistory.push(`${minioBrowserPrefix}/login`) } fullScreen(e) { e.preventDefault() let el = document.documentElement if (el.requestFullscreen) { el.requestFullscreen() } if (el.mozRequestFullScreen) { el.mozRequestFullScreen() } if (el.webkitRequestFullscreen) { el.webkitRequestFullscreen() } if (el.msRequestFullscreen) { el.msRequestFullscreen() } } toggleSidebar(status) { this.props.dispatch(actions.setSidebarStatus(status)) } hideSidebar(event) { let e = event || window.event; // Support all browsers. let target = e.srcElement || e.target; if (target.nodeType === 3) // Safari support. target = target.parentNode; let targetID = target.id; if (!(targetID === 'feh-trigger')) { this.props.dispatch(actions.setSidebarStatus(false)) } } showSettings(e) { e.preventDefault() const {dispatch} = this.props dispatch(actions.showSettings()) } showMessage() { const {dispatch} = this.props dispatch(actions.showAlert({ type: 'success', message: 'Link copied to clipboard!' })) this.hideShareObjectModal() } selectTexts() { this.refs.copyTextInput.select() } handleExpireValue(targetInput, inc, object) { let value = this.refs[targetInput].value let maxValue = (targetInput == 'expireHours') ? 23 : (targetInput == 'expireMins') ? 59 : (targetInput == 'expireDays') ? 7 : 0 value = isNaN(value) ? 0 : value // Use custom step count to support browser Edge if((inc === -1)) { if(value != 0) { value-- } } else { if(value != maxValue) { value++ } } this.refs[targetInput].value = value // Reset hours and mins when days reaches it's max value if (this.refs.expireDays.value == 7) { this.refs.expireHours.value = 0 this.refs.expireMins.value = 0 } if (this.refs.expireDays.value + this.refs.expireHours.value + this.refs.expireMins.value == 0) { this.refs.expireDays.value = 7 } const {dispatch} = this.props dispatch(actions.shareObject(object, this.refs.expireDays.value, this.refs.expireHours.value, this.refs.expireMins.value)) } checkObject(e, objectName) { const {dispatch} = this.props e.target.checked ? dispatch(actions.checkedObjectsAdd(objectName)) : dispatch(actions.checkedObjectsRemove(objectName)) } downloadSelected() { const {dispatch, web} = this.props let req = { bucketName: this.props.currentBucket, objects: this.props.checkedObjects, prefix: this.props.currentPath } if (!web.LoggedIn()) { let requestUrl = location.origin + "/minio/zip?token=''" this.xhr = new XMLHttpRequest() dispatch(actions.downloadSelected(requestUrl, req, this.xhr)) } else { web.CreateURLToken() .then(res => { let requestUrl = location.origin + minioBrowserPrefix + "/zip?token=" + res.token this.xhr = new XMLHttpRequest() dispatch(actions.downloadSelected(requestUrl, req, this.xhr)) }) .catch(err => dispatch(actions.showAlert({ type: 'danger', message: err.message }))) } } clearSelected() { const {dispatch} = this.props dispatch(actions.checkedObjectsReset()) } render() { const {total, free} = this.props.storageInfo 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 const {policies, currentBucket, currentPath} = this.props const {deleteConfirmation} = this.props const {shareObject} = this.props const {web, prefixWritable, istruncated} = this.props // Don't always show the SettingsModal. This is done here instead of in // SettingsModal.js so as to allow for #componentWillMount to handle // the loading of the settings. let settingsModal = showSettings ? <SettingsModal /> : <noscript></noscript> let alertBox = <Alert className={ classNames({ 'alert': true, 'animated': true, 'fadeInDown': alert.show, 'fadeOutUp': !alert.show }) } bsStyle={ alert.type } onDismiss={ this.hideAlert.bind(this) }> <div className='text-center'> { alert.message } </div> </Alert> // Make sure you don't show a fading out alert box on the initial web-page load. if (!alert.message) alertBox = '' let signoutTooltip = <Tooltip id="tt-sign-out"> Sign out </Tooltip> let uploadTooltip = <Tooltip id="tt-upload-file"> Upload file </Tooltip> let makeBucketTooltip = <Tooltip id="tt-create-bucket"> Create bucket </Tooltip> let loginButton = '' let browserDropdownButton = '' let storageUsageDetails = '' let used = total - free let usedPercent = (used / total) * 100 + '%' let freePercent = free * 100 / total if (web.LoggedIn()) { browserDropdownButton = <BrowserDropdown fullScreenFunc={ this.fullScreen.bind(this) } aboutFunc={ this.showAbout.bind(this) } settingsFunc={ this.showSettings.bind(this) } logoutFunc={ this.logout.bind(this) } /> } else { loginButton = <a className='btn btn-danger' href={minioBrowserPrefix+'/login'}>Login</a> } if (web.LoggedIn()) { if (!(used === 0 && free === 0)) { storageUsageDetails = <div className="feh-usage"> <div className="fehu-chart"> <div style={ { width: usedPercent } }></div> </div> <ul> <li> <span>Used: </span> { humanize.filesize(total - free) } </li> <li className="pull-right"> <span>Free: </span> { humanize.filesize(total - used) } </li> </ul> </div> } } let createButton = '' if (web.LoggedIn()) { createButton = <Dropdown dropup className="feb-actions" id="fe-action-toggle"> <Dropdown.Toggle noCaret className="feba-toggle"> <span><i className="fa fa-plus"></i></span> </Dropdown.Toggle> <Dropdown.Menu> <OverlayTrigger placement="left" overlay={ uploadTooltip }> <a href="#" className="feba-btn feba-upload"> <input type="file" onChange={ this.uploadFile.bind(this) } style={ { display: 'none' } } id="file-input"></input> <label htmlFor="file-input"> <i className="fa fa-cloud-upload"></i> </label> </a> </OverlayTrigger> <OverlayTrigger placement="left" overlay={ makeBucketTooltip }> <a href="#" className="feba-btn feba-bucket" onClick={ this.showMakeBucketModal.bind(this) }><i className="fa fa-hdd-o"></i></a> </OverlayTrigger> </Dropdown.Menu> </Dropdown> } else { if (prefixWritable) createButton = <Dropdown dropup className="feb-actions" id="fe-action-toggle"> <Dropdown.Toggle noCaret className="feba-toggle"> <span><i className="fa fa-plus"></i></span> </Dropdown.Toggle> <Dropdown.Menu> <OverlayTrigger placement="left" overlay={ uploadTooltip }> <a href="#" className="feba-btn feba-upload"> <input type="file" onChange={ this.uploadFile.bind(this) } style={ { display: 'none' } } id="file-input"></input> <label htmlFor="file-input"> <i className="fa fa-cloud-upload"></i> </label> </a> </OverlayTrigger> </Dropdown.Menu> </Dropdown> } return ( <div className={ classNames({ 'file-explorer': true, 'toggled': sidebarStatus }) }> <SideBar searchBuckets={ this.searchBuckets.bind(this) } selectBucket={ this.selectBucket.bind(this) } clickOutside={ this.hideSidebar.bind(this) } showPolicy={ this.showBucketPolicy.bind(this) } /> <div className="fe-body"> <div className={ 'list-actions' + (classNames({ ' list-actions-toggled': checkedObjects.length > 0 })) }> <span className="la-label"><i className="fa fa-check-circle" /> { checkedObjects.length } Objects selected</span> <span className="la-actions pull-right"><button onClick={ this.downloadSelected.bind(this) }> Download all as zip </button></span> <span className="la-actions pull-right"><button onClick={ this.showDeleteConfirmation.bind(this) }> Delete selected </button></span> <i className="la-close fa fa-times" onClick={ this.clearSelected.bind(this) }></i> </div> <Dropzone> { alertBox } <header className="fe-header-mobile hidden-lg hidden-md"> <div id="feh-trigger" className={ 'feh-trigger ' + (classNames({ 'feht-toggled': sidebarStatus })) } onClick={ this.toggleSidebar.bind(this, !sidebarStatus) }> <div className="feht-lines"> <div className="top"></div> <div className="center"></div> <div className="bottom"></div> </div> </div> <img className="mh-logo" src={ logo } alt="" /> </header> <header className="fe-header"> <Path selectPrefix={ this.selectPrefix.bind(this) } /> { storageUsageDetails } <ul className="feh-actions"> <BrowserUpdate /> { loginButton } { browserDropdownButton } </ul> </header> <div className="feb-container"> <header className="fesl-row" data-type="folder"> <div className="fesl-item fesl-item-icon"></div> <div className="fesl-item fesl-item-name" onClick={ this.sortObjectsByName.bind(this) } data-sort="name"> Name <i className={ classNames({ 'fesli-sort': true, 'fa': true, 'fa-sort-alpha-desc': sortNameOrder, 'fa-sort-alpha-asc': !sortNameOrder }) } /> </div> <div className="fesl-item fesl-item-size" onClick={ this.sortObjectsBySize.bind(this) } data-sort="size"> Size <i className={ classNames({ 'fesli-sort': true, 'fa': true, 'fa-sort-amount-desc': sortSizeOrder, 'fa-sort-amount-asc': !sortSizeOrder }) } /> </div> <div className="fesl-item fesl-item-modified" onClick={ this.sortObjectsByDate.bind(this) } data-sort="last-modified"> Last Modified <i className={ classNames({ 'fesli-sort': true, 'fa': true, 'fa-sort-numeric-desc': sortDateOrder, 'fa-sort-numeric-asc': !sortDateOrder }) } /> </div> <div className="fesl-item fesl-item-actions"></div> </header> </div> <div className="feb-container"> <InfiniteScroll loadMore={ this.listObjects.bind(this) } hasMore={ istruncated } useWindow={ true } initialLoad={ false }> <ObjectsList dataType={ this.dataType.bind(this) } selectPrefix={ this.selectPrefix.bind(this) } showDeleteConfirmation={ this.showDeleteConfirmation.bind(this) } shareObject={ this.shareObject.bind(this) } checkObject={ this.checkObject.bind(this) } checkedObjectsArray={ checkedObjects } /> </InfiniteScroll> <div className="text-center" style={ { display: (istruncated && currentBucket) ? 'block' : 'none' } }> <span>Loading...</span> </div> </div> <UploadModal /> { createButton } <Modal className="modal-create-bucket" bsSize="small" animation={ false } show={ showMakeBucketModal } onHide={ this.hideMakeBucketModal.bind(this) }> <button className="close close-alt" onClick={ this.hideMakeBucketModal.bind(this) }> <span>×</span> </button> <ModalBody> <form onSubmit={ this.makeBucket.bind(this) }> <div className="input-group"> <input className="ig-text" type="text" ref="makeBucketRef" placeholder="Bucket Name" autoFocus/> <i className="ig-helpers"></i> </div> </form> </ModalBody> </Modal> <Modal className="modal-about modal-dark" animation={ false } show={ showAbout } onHide={ this.hideAbout.bind(this) }> <button className="close" onClick={ this.hideAbout.bind(this) }> <span>×</span> </button> <div className="ma-inner"> <div className="mai-item hidden-xs"> <a href="https://minio.io" target="_blank"><img className="maii-logo" src={ logo } alt="" /></a> </div> <div className="mai-item"> <ul className="maii-list"> <li> <div> Version </div> <small>{ version }</small> </li> <li> <div> Memory </div> <small>{ memory }</small> </li> <li> <div> Platform </div> <small>{ platform }</small> </li> <li> <div> Runtime </div> <small>{ runtime }</small> </li> </ul> </div> </div> </Modal> <Modal className="modal-policy" animation={ false } show={ showBucketPolicy } onHide={ this.hideBucketPolicy.bind(this) }> <ModalHeader> Bucket Policy ( { currentBucket }) <button className="close close-alt" onClick={ this.hideBucketPolicy.bind(this) }> <span>×</span> </button> </ModalHeader> <div className="pm-body"> <PolicyInput bucket={ currentBucket } /> { policies.map((policy, i) => <Policy key={ i } prefix={ policy.prefix } policy={ policy.policy } /> ) } </div> </Modal> <ConfirmModal show={ deleteConfirmation.show } icon='fa fa-exclamation-triangle mci-red' text='Are you sure you want to delete?' sub='This cannot be undone!' okText='Delete' cancelText='Cancel' okHandler={ this.removeObject.bind(this) } cancelHandler={ this.hideDeleteConfirmation.bind(this) }> </ConfirmModal> <Modal show={ shareObject.show } animation={ false } onHide={ this.hideShareObjectModal.bind(this) } bsSize="small"> <ModalHeader> Share Object </ModalHeader> <ModalBody> <div className="input-group copy-text"> <label> Shareable Link </label> <input type="text" ref="copyTextInput" readOnly="readOnly" value={ window.location.protocol + '//' + shareObject.url } onClick={ this.selectTexts.bind(this) } /> </div> <div className="input-group" style={ { display: web.LoggedIn() ? 'block' : 'none' } }> <label> Expires in (Max 7 days) </label> <div className="set-expire"> <div className="set-expire-item"> <i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireDays', 1, shareObject.object) } /> <div className="set-expire-title"> Days </div> <div className="set-expire-value"> <input ref="expireDays" type="number" min={ 0 } max={ 7 } defaultValue={ 5 } readOnly="readOnly" /> </div> <i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireDays', -1, shareObject.object) } /> </div> <div className="set-expire-item"> <i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireHours', 1, shareObject.object) } /> <div className="set-expire-title"> Hours </div> <div className="set-expire-value"> <input ref="expireHours" type="number" min={ 0 } max={ 23 } defaultValue={ 0 } readOnly="readOnly" /> </div> <i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireHours', -1, shareObject.object) } /> </div> <div className="set-expire-item"> <i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireMins', 1, shareObject.object) } /> <div className="set-expire-title"> Minutes </div> <div className="set-expire-value"> <input ref="expireMins" type="number" min={ 0 } max={ 59 } defaultValue={ 0 } readOnly="readOnly" /> </div> <i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireMins', -1, shareObject.object) } /> </div> </div> </div> </ModalBody> <div className="modal-footer"> <CopyToClipboard text={ window.location.protocol + '//' + shareObject.url } onCopy={ this.showMessage.bind(this) }> <button className="btn btn-success"> Copy Link </button> </CopyToClipboard> <button className="btn btn-link" onClick={ this.hideShareObjectModal.bind(this) }> Cancel </button> </div> </Modal> { settingsModal } </Dropzone> </div> </div> ) } }