Merge pull request #1309 from processing/feature/public-api-asset-limit
Feature/public api asset limit, Fixes #168
This commit is contained in:
commit
a6f59fd309
27 changed files with 512 additions and 146 deletions
|
@ -80,6 +80,8 @@ export const SHOW_NEW_FOLDER_MODAL = 'SHOW_NEW_FOLDER_MODAL';
|
||||||
export const CLOSE_NEW_FOLDER_MODAL = 'CLOSE_NEW_FOLDER_MODAL';
|
export const CLOSE_NEW_FOLDER_MODAL = 'CLOSE_NEW_FOLDER_MODAL';
|
||||||
export const SHOW_FOLDER_CHILDREN = 'SHOW_FOLDER_CHILDREN';
|
export const SHOW_FOLDER_CHILDREN = 'SHOW_FOLDER_CHILDREN';
|
||||||
export const HIDE_FOLDER_CHILDREN = 'HIDE_FOLDER_CHILDREN';
|
export const HIDE_FOLDER_CHILDREN = 'HIDE_FOLDER_CHILDREN';
|
||||||
|
export const OPEN_UPLOAD_FILE_MODAL = 'OPEN_UPLOAD_FILE_MODAL';
|
||||||
|
export const CLOSE_UPLOAD_FILE_MODAL = 'CLOSE_UPLOAD_FILE_MODAL';
|
||||||
|
|
||||||
export const SHOW_SHARE_MODAL = 'SHOW_SHARE_MODAL';
|
export const SHOW_SHARE_MODAL = 'SHOW_SHARE_MODAL';
|
||||||
export const CLOSE_SHARE_MODAL = 'CLOSE_SHARE_MODAL';
|
export const CLOSE_SHARE_MODAL = 'CLOSE_SHARE_MODAL';
|
||||||
|
@ -127,6 +129,7 @@ export const CLEAR_PERSISTED_STATE = 'CLEAR_PERSISTED_STATE';
|
||||||
export const HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING';
|
export const HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING';
|
||||||
export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
|
export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
|
||||||
export const SET_ASSETS = 'SET_ASSETS';
|
export const SET_ASSETS = 'SET_ASSETS';
|
||||||
|
export const DELETE_ASSET = 'DELETE_ASSET';
|
||||||
|
|
||||||
export const TOGGLE_DIRECTION = 'TOGGLE_DIRECTION';
|
export const TOGGLE_DIRECTION = 'TOGGLE_DIRECTION';
|
||||||
export const SET_SORTING = 'SET_SORTING';
|
export const SET_SORTING = 'SET_SORTING';
|
||||||
|
|
|
@ -30,8 +30,23 @@ export function getAssets() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteAsset(assetKey, userId) {
|
export function deleteAsset(assetKey) {
|
||||||
return {
|
return {
|
||||||
type: 'PLACEHOLDER'
|
type: ActionTypes.DELETE_ASSET,
|
||||||
|
key: assetKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteAssetRequest(assetKey) {
|
||||||
|
return (dispatch) => {
|
||||||
|
axios.delete(`${ROOT_URL}/S3/${assetKey}`, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
dispatch(deleteAsset(assetKey));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.ERROR
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,19 @@ export function closeNewFileModal() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openUploadFileModal(parentId) {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.OPEN_UPLOAD_FILE_MODAL,
|
||||||
|
parentId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeUploadFileModal() {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.CLOSE_UPLOAD_FILE_MODAL
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function expandSidebar() {
|
export function expandSidebar() {
|
||||||
return {
|
return {
|
||||||
type: ActionTypes.EXPAND_SIDEBAR
|
type: ActionTypes.EXPAND_SIDEBAR
|
||||||
|
|
|
@ -66,11 +66,11 @@ export function dropzoneAcceptCallback(userId, file, done) {
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((response) => {
|
||||||
file.custom_status = 'rejected'; // eslint-disable-line
|
file.custom_status = 'rejected'; // eslint-disable-line
|
||||||
if (response.data.responseText && response.data.responseText.message) {
|
if (response.data && response.data.responseText && response.data.responseText.message) {
|
||||||
done(response.data.responseText.message);
|
done(response.data.responseText.message);
|
||||||
}
|
}
|
||||||
done('error preparing the upload');
|
done('Error: Reached upload limit.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,9 +5,146 @@ import { bindActionCreators } from 'redux';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
|
||||||
import Loader from '../../App/components/loader';
|
import Loader from '../../App/components/loader';
|
||||||
import * as AssetActions from '../actions/assets';
|
import * as AssetActions from '../actions/assets';
|
||||||
|
import downFilledTriangle from '../../../images/down-filled-triangle.svg';
|
||||||
|
|
||||||
|
class AssetListRowBase extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isFocused: false,
|
||||||
|
optionsOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusComponent = () => {
|
||||||
|
this.setState({ isFocused: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlurComponent = () => {
|
||||||
|
this.setState({ isFocused: false });
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.state.isFocused) {
|
||||||
|
this.closeOptions();
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
openOptions = () => {
|
||||||
|
this.setState({
|
||||||
|
optionsOpen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeOptions = () => {
|
||||||
|
this.setState({
|
||||||
|
optionsOpen: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleOptions = () => {
|
||||||
|
if (this.state.optionsOpen) {
|
||||||
|
this.closeOptions();
|
||||||
|
} else {
|
||||||
|
this.openOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDropdownOpen = () => {
|
||||||
|
this.closeOptions();
|
||||||
|
this.openOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAssetDelete = () => {
|
||||||
|
const { key, name } = this.props.asset;
|
||||||
|
this.closeOptions();
|
||||||
|
if (window.confirm(`Are you sure you want to delete "${name}"?`)) {
|
||||||
|
this.props.deleteAssetRequest(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { asset, username } = this.props;
|
||||||
|
const { optionsOpen } = this.state;
|
||||||
|
return (
|
||||||
|
<tr className="asset-table__row" key={asset.key}>
|
||||||
|
<th scope="row">
|
||||||
|
<Link to={asset.url} target="_blank">
|
||||||
|
{asset.name}
|
||||||
|
</Link>
|
||||||
|
</th>
|
||||||
|
<td>{prettyBytes(asset.size)}</td>
|
||||||
|
<td>
|
||||||
|
{ asset.sketchId && <Link to={`/${username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link> }
|
||||||
|
</td>
|
||||||
|
<td className="asset-table__dropdown-column">
|
||||||
|
<button
|
||||||
|
className="asset-table__dropdown-button"
|
||||||
|
onClick={this.toggleOptions}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
<InlineSVG src={downFilledTriangle} alt="Menu" />
|
||||||
|
</button>
|
||||||
|
{optionsOpen &&
|
||||||
|
<ul
|
||||||
|
className="asset-table__action-dialogue"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="asset-table__action-option"
|
||||||
|
onClick={this.handleAssetDelete}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={asset.url}
|
||||||
|
target="_blank"
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
className="asset-table__action-option"
|
||||||
|
>
|
||||||
|
Open in New Tab
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetListRowBase.propTypes = {
|
||||||
|
asset: PropTypes.shape({
|
||||||
|
key: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
sketchId: PropTypes.string,
|
||||||
|
sketchName: PropTypes.string,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
size: PropTypes.number.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
deleteAssetRequest: PropTypes.func.isRequired,
|
||||||
|
username: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToPropsAssetListRow(state) {
|
||||||
|
return {
|
||||||
|
username: state.user.username
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToPropsAssetListRow(dispatch) {
|
||||||
|
return bindActionCreators(AssetActions, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssetListRow = connect(mapStateToPropsAssetListRow, mapDispatchToPropsAssetListRow)(AssetListRowBase);
|
||||||
|
|
||||||
class AssetList extends React.Component {
|
class AssetList extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -16,10 +153,7 @@ class AssetList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAssetsTitle() {
|
getAssetsTitle() {
|
||||||
if (!this.props.username || this.props.username === this.props.user.username) {
|
return 'p5.js Web Editor | My assets';
|
||||||
return 'p5.js Web Editor | My assets';
|
|
||||||
}
|
|
||||||
return `p5.js Web Editor | ${this.props.username}'s assets`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAssets() {
|
hasAssets() {
|
||||||
|
@ -39,7 +173,6 @@ class AssetList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
|
||||||
const { assetList } = this.props;
|
const { assetList } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="asset-table-container">
|
<div className="asset-table-container">
|
||||||
|
@ -52,24 +185,14 @@ class AssetList extends React.Component {
|
||||||
<table className="asset-table">
|
<table className="asset-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Name</th>
|
<th>Name</th>
|
||||||
<th scope="col">Size</th>
|
<th>Size</th>
|
||||||
<th scope="col">Sketch</th>
|
<th>Sketch</th>
|
||||||
|
<th scope="col"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{assetList.map(asset =>
|
{assetList.map(asset => <AssetListRow asset={asset} key={asset.key} />)}
|
||||||
(
|
|
||||||
<tr className="asset-table__row" key={asset.key}>
|
|
||||||
<th scope="row">
|
|
||||||
<Link to={asset.url} target="_blank">
|
|
||||||
{asset.name}
|
|
||||||
</Link>
|
|
||||||
</th>
|
|
||||||
<td>{prettyBytes(asset.size)}</td>
|
|
||||||
<td><Link to={`/${username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link></td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>}
|
</table>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -81,7 +204,6 @@ AssetList.propTypes = {
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
username: PropTypes.string.isRequired,
|
|
||||||
assetList: PropTypes.arrayOf(PropTypes.shape({
|
assetList: PropTypes.arrayOf(PropTypes.shape({
|
||||||
key: PropTypes.string.isRequired,
|
key: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
|
@ -97,7 +219,6 @@ function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
user: state.user,
|
user: state.user,
|
||||||
assetList: state.assets.list,
|
assetList: state.assets.list,
|
||||||
totalSize: state.assets.totalSize,
|
|
||||||
loading: state.loading
|
loading: state.loading
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,9 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
|
||||||
const MB_TO_B = 1000 * 1000;
|
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||||
const MAX_SIZE_B = 250 * MB_TO_B;
|
const limit = __process.env.UPLOAD_LIMIT || 250000000;
|
||||||
|
const MAX_SIZE_B = limit;
|
||||||
|
|
||||||
const formatPercent = (percent) => {
|
const formatPercent = (percent) => {
|
||||||
const percentUsed = percent * 100;
|
const percentUsed = percent * 100;
|
||||||
|
@ -17,7 +18,7 @@ const formatPercent = (percent) => {
|
||||||
|
|
||||||
/* Eventually, this copy should be Total / 250 MB Used */
|
/* Eventually, this copy should be Total / 250 MB Used */
|
||||||
const AssetSize = ({ totalSize }) => {
|
const AssetSize = ({ totalSize }) => {
|
||||||
if (!totalSize) {
|
if (totalSize === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,9 +26,10 @@ const AssetSize = ({ totalSize }) => {
|
||||||
const sizeLimit = prettyBytes(MAX_SIZE_B);
|
const sizeLimit = prettyBytes(MAX_SIZE_B);
|
||||||
const percentValue = totalSize / MAX_SIZE_B;
|
const percentValue = totalSize / MAX_SIZE_B;
|
||||||
const percent = formatPercent(percentValue);
|
const percent = formatPercent(percentValue);
|
||||||
|
const percentSize = percentValue < 1 ? percentValue : 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="asset-size" style={{ '--percent': percentValue }}>
|
<div className="asset-size" style={{ '--percent': percentSize }}>
|
||||||
<div className="asset-size-bar" />
|
<div className="asset-size-bar" />
|
||||||
<p className="asset-current">{currentSize} ({percent})</p>
|
<p className="asset-current">{currentSize} ({percent})</p>
|
||||||
<p className="asset-max">Max: {sizeLimit}</p>
|
<p className="asset-max">Max: {sizeLimit}</p>
|
||||||
|
@ -42,7 +44,7 @@ AssetSize.propTypes = {
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
user: state.user,
|
user: state.user,
|
||||||
totalSize: state.assets.totalSize,
|
totalSize: state.user.totalSize || state.assets.totalSize,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ class FileUploader extends React.Component {
|
||||||
thumbnailWidth: 200,
|
thumbnailWidth: 200,
|
||||||
thumbnailHeight: 200,
|
thumbnailHeight: 200,
|
||||||
acceptedFiles: fileExtensionsAndMimeTypes,
|
acceptedFiles: fileExtensionsAndMimeTypes,
|
||||||
dictDefaultMessage: 'Drop files here to upload or click to use the file browser',
|
dictDefaultMessage: 'Drop files here or click to use the file browser',
|
||||||
accept: this.props.dropzoneAcceptCallback.bind(this, userId),
|
accept: this.props.dropzoneAcceptCallback.bind(this, userId),
|
||||||
sending: this.props.dropzoneSendingCallback,
|
sending: this.props.dropzoneSendingCallback,
|
||||||
complete: this.props.dropzoneCompleteCallback
|
complete: this.props.dropzoneCompleteCallback
|
||||||
|
|
|
@ -22,16 +22,18 @@ class NewFileForm extends React.Component {
|
||||||
handleSubmit(this.createFile)(data);
|
handleSubmit(this.createFile)(data);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label className="new-file-form__name-label" htmlFor="name">Name:</label>
|
<div className="new-file-form__input-wrapper">
|
||||||
<input
|
<label className="new-file-form__name-label" htmlFor="name">Name:</label>
|
||||||
className="new-file-form__name-input"
|
<input
|
||||||
id="name"
|
className="new-file-form__name-input"
|
||||||
type="text"
|
id="name"
|
||||||
placeholder="Name"
|
type="text"
|
||||||
{...domOnlyProps(name)}
|
placeholder="Name"
|
||||||
ref={(element) => { this.fileName = element; }}
|
{...domOnlyProps(name)}
|
||||||
/>
|
ref={(element) => { this.fileName = element; }}
|
||||||
<input type="submit" value="Add File" aria-label="add file" />
|
/>
|
||||||
|
<input type="submit" value="Add File" aria-label="add file" />
|
||||||
|
</div>
|
||||||
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators, compose } from 'redux';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import classNames from 'classnames';
|
|
||||||
import InlineSVG from 'react-inlinesvg';
|
import InlineSVG from 'react-inlinesvg';
|
||||||
import NewFileForm from './NewFileForm';
|
import NewFileForm from './NewFileForm';
|
||||||
import FileUploader from './FileUploader';
|
import { getCanUploadMedia, getreachedTotalSizeLimit } from '../selectors/users';
|
||||||
|
import { closeNewFileModal } from '../actions/ide';
|
||||||
|
import { createFile } from '../actions/files';
|
||||||
import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils';
|
import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
const exitUrl = require('../../../images/exit.svg');
|
||||||
|
@ -28,16 +31,12 @@ class NewFileModal extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const modalClass = classNames({
|
|
||||||
'modal': true,
|
|
||||||
'modal--reduced': !this.props.canUploadMedia
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<section className={modalClass} ref={(element) => { this.modal = element; }}>
|
<section className="modal" ref={(element) => { this.modal = element; }}>
|
||||||
<div className="modal-content">
|
<div className="modal-content">
|
||||||
<div className="modal__header">
|
<div className="modal__header">
|
||||||
<h2 className="modal__title">Add File</h2>
|
<h2 className="modal__title">Create File</h2>
|
||||||
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
<button className="modal__exit-button" onClick={this.props.closeNewFileModal}>
|
||||||
<InlineSVG src={exitUrl} alt="Close New File Modal" />
|
<InlineSVG src={exitUrl} alt="Close New File Modal" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,17 +44,6 @@ class NewFileModal extends React.Component {
|
||||||
focusOnModal={this.focusOnModal}
|
focusOnModal={this.focusOnModal}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
/>
|
/>
|
||||||
{(() => {
|
|
||||||
if (this.props.canUploadMedia) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p className="modal__divider">OR</p>
|
|
||||||
<FileUploader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
@ -63,8 +51,8 @@ class NewFileModal extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
NewFileModal.propTypes = {
|
NewFileModal.propTypes = {
|
||||||
closeModal: PropTypes.func.isRequired,
|
createFile: PropTypes.func.isRequired,
|
||||||
canUploadMedia: PropTypes.bool.isRequired
|
closeNewFileModal: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
function validate(formProps) {
|
function validate(formProps) {
|
||||||
|
@ -79,9 +67,22 @@ function validate(formProps) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
canUploadMedia: getCanUploadMedia(state),
|
||||||
|
reachedTotalSizeLimit: getreachedTotalSizeLimit(state)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default reduxForm({
|
function mapDispatchToProps(dispatch) {
|
||||||
form: 'new-file',
|
return bindActionCreators({ createFile, closeNewFileModal }, dispatch);
|
||||||
fields: ['name'],
|
}
|
||||||
validate
|
|
||||||
})(NewFileModal);
|
export default compose(
|
||||||
|
connect(mapStateToProps, mapDispatchToProps),
|
||||||
|
reduxForm({
|
||||||
|
form: 'new-file',
|
||||||
|
fields: ['name'],
|
||||||
|
validate
|
||||||
|
})
|
||||||
|
)(NewFileModal);
|
||||||
|
|
|
@ -25,16 +25,18 @@ class NewFolderForm extends React.Component {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label className="new-folder-form__name-label" htmlFor="name">Name:</label>
|
<div className="new-folder-form__input-wrapper">
|
||||||
<input
|
<label className="new-folder-form__name-label" htmlFor="name">Name:</label>
|
||||||
className="new-folder-form__name-input"
|
<input
|
||||||
id="name"
|
className="new-folder-form__name-input"
|
||||||
type="text"
|
id="name"
|
||||||
placeholder="Name"
|
type="text"
|
||||||
ref={(element) => { this.fileName = element; }}
|
placeholder="Name"
|
||||||
{...domOnlyProps(name)}
|
ref={(element) => { this.fileName = element; }}
|
||||||
/>
|
{...domOnlyProps(name)}
|
||||||
<input type="submit" value="Add Folder" aria-label="add folder" />
|
/>
|
||||||
|
<input type="submit" value="Add Folder" aria-label="add folder" />
|
||||||
|
</div>
|
||||||
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,7 +16,7 @@ class NewFolderModal extends React.Component {
|
||||||
<section className="modal" ref={(element) => { this.newFolderModal = element; }} >
|
<section className="modal" ref={(element) => { this.newFolderModal = element; }} >
|
||||||
<div className="modal-content-folder">
|
<div className="modal-content-folder">
|
||||||
<div className="modal__header">
|
<div className="modal__header">
|
||||||
<h2 className="modal__title">Add Folder</h2>
|
<h2 className="modal__title">Create Folder</h2>
|
||||||
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
||||||
<InlineSVG src={exitUrl} alt="Close New Folder Modal" />
|
<InlineSVG src={exitUrl} alt="Close New Folder Modal" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -97,7 +97,7 @@ class Sidebar extends React.Component {
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
>
|
>
|
||||||
Add folder
|
Create folder
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -110,7 +110,20 @@ class Sidebar extends React.Component {
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
>
|
>
|
||||||
Add file
|
Create file
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
aria-label="upload file"
|
||||||
|
onClick={() => {
|
||||||
|
this.props.openUploadFileModal(rootFile.id);
|
||||||
|
setTimeout(this.props.closeProjectOptions, 0);
|
||||||
|
}}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Upload file
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -137,6 +150,7 @@ Sidebar.propTypes = {
|
||||||
openProjectOptions: PropTypes.func.isRequired,
|
openProjectOptions: PropTypes.func.isRequired,
|
||||||
closeProjectOptions: PropTypes.func.isRequired,
|
closeProjectOptions: PropTypes.func.isRequired,
|
||||||
newFolder: PropTypes.func.isRequired,
|
newFolder: PropTypes.func.isRequired,
|
||||||
|
openUploadFileModal: PropTypes.func.isRequired,
|
||||||
owner: PropTypes.shape({
|
owner: PropTypes.shape({
|
||||||
id: PropTypes.string
|
id: PropTypes.string
|
||||||
}),
|
}),
|
||||||
|
|
68
client/modules/IDE/components/UploadFileModal.jsx
Normal file
68
client/modules/IDE/components/UploadFileModal.jsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
import FileUploader from './FileUploader';
|
||||||
|
import { getreachedTotalSizeLimit } from '../selectors/users';
|
||||||
|
import exitUrl from '../../../images/exit.svg';
|
||||||
|
|
||||||
|
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||||
|
const limit = __process.env.UPLOAD_LIMIT || 250000000;
|
||||||
|
const limitText = prettyBytes(limit);
|
||||||
|
|
||||||
|
class UploadFileModal extends React.Component {
|
||||||
|
propTypes = {
|
||||||
|
reachedTotalSizeLimit: PropTypes.bool.isRequired,
|
||||||
|
closeModal: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.focusOnModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
focusOnModal = () => {
|
||||||
|
this.modal.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<section className="modal" ref={(element) => { this.modal = element; }}>
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal__header">
|
||||||
|
<h2 className="modal__title">Upload File</h2>
|
||||||
|
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
||||||
|
<InlineSVG src={exitUrl} alt="Close New File Modal" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{ this.props.reachedTotalSizeLimit &&
|
||||||
|
<p>
|
||||||
|
{
|
||||||
|
`Error: You cannot upload any more files. You have reached the total size limit of ${limitText}.
|
||||||
|
If you would like to upload more, please remove the ones you aren't using anymore by
|
||||||
|
in your `
|
||||||
|
}
|
||||||
|
<Link to="/assets" onClick={this.props.closeModal}>assets</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
{ !this.props.reachedTotalSizeLimit &&
|
||||||
|
<div>
|
||||||
|
<FileUploader />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
reachedTotalSizeLimit: getreachedTotalSizeLimit(state)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(UploadFileModal);
|
|
@ -12,6 +12,7 @@ import Toolbar from '../components/Toolbar';
|
||||||
import Preferences from '../components/Preferences';
|
import Preferences from '../components/Preferences';
|
||||||
import NewFileModal from '../components/NewFileModal';
|
import NewFileModal from '../components/NewFileModal';
|
||||||
import NewFolderModal from '../components/NewFolderModal';
|
import NewFolderModal from '../components/NewFolderModal';
|
||||||
|
import UploadFileModal from '../components/UploadFileModal';
|
||||||
import ShareModal from '../components/ShareModal';
|
import ShareModal from '../components/ShareModal';
|
||||||
import KeyboardShortcutModal from '../components/KeyboardShortcutModal';
|
import KeyboardShortcutModal from '../components/KeyboardShortcutModal';
|
||||||
import ErrorModal from '../components/ErrorModal';
|
import ErrorModal from '../components/ErrorModal';
|
||||||
|
@ -238,6 +239,8 @@ class IDEView extends React.Component {
|
||||||
newFolder={this.props.newFolder}
|
newFolder={this.props.newFolder}
|
||||||
user={this.props.user}
|
user={this.props.user}
|
||||||
owner={this.props.project.owner}
|
owner={this.props.project.owner}
|
||||||
|
openUploadFileModal={this.props.openUploadFileModal}
|
||||||
|
closeUploadFileModal={this.props.closeUploadFileModal}
|
||||||
/>
|
/>
|
||||||
<SplitPane
|
<SplitPane
|
||||||
split="vertical"
|
split="vertical"
|
||||||
|
@ -349,12 +352,8 @@ class IDEView extends React.Component {
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
{this.props.ide.modalIsVisible &&
|
{ this.props.ide.modalIsVisible &&
|
||||||
<NewFileModal
|
<NewFileModal />
|
||||||
canUploadMedia={this.props.user.authenticated}
|
|
||||||
closeModal={this.props.closeNewFileModal}
|
|
||||||
createFile={this.props.createFile}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
{this.props.ide.newFolderModalVisible &&
|
{this.props.ide.newFolderModalVisible &&
|
||||||
<NewFolderModal
|
<NewFolderModal
|
||||||
|
@ -362,6 +361,11 @@ class IDEView extends React.Component {
|
||||||
createFolder={this.props.createFolder}
|
createFolder={this.props.createFolder}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
{this.props.ide.uploadFileModalVisible &&
|
||||||
|
<UploadFileModal
|
||||||
|
closeModal={this.props.closeUploadFileModal}
|
||||||
|
/>
|
||||||
|
}
|
||||||
{ this.props.location.pathname === '/about' &&
|
{ this.props.location.pathname === '/about' &&
|
||||||
<Overlay
|
<Overlay
|
||||||
title="About"
|
title="About"
|
||||||
|
@ -475,6 +479,7 @@ IDEView.propTypes = {
|
||||||
justOpenedProject: PropTypes.bool.isRequired,
|
justOpenedProject: PropTypes.bool.isRequired,
|
||||||
errorType: PropTypes.string,
|
errorType: PropTypes.string,
|
||||||
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
||||||
|
uploadFileModalVisible: PropTypes.bool.isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
stopSketch: PropTypes.func.isRequired,
|
stopSketch: PropTypes.func.isRequired,
|
||||||
project: PropTypes.shape({
|
project: PropTypes.shape({
|
||||||
|
@ -532,7 +537,6 @@ IDEView.propTypes = {
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
dispatchConsoleEvent: PropTypes.func.isRequired,
|
dispatchConsoleEvent: PropTypes.func.isRequired,
|
||||||
newFile: PropTypes.func.isRequired,
|
newFile: PropTypes.func.isRequired,
|
||||||
closeNewFileModal: PropTypes.func.isRequired,
|
|
||||||
expandSidebar: PropTypes.func.isRequired,
|
expandSidebar: PropTypes.func.isRequired,
|
||||||
collapseSidebar: PropTypes.func.isRequired,
|
collapseSidebar: PropTypes.func.isRequired,
|
||||||
cloneProject: PropTypes.func.isRequired,
|
cloneProject: PropTypes.func.isRequired,
|
||||||
|
@ -545,7 +549,6 @@ IDEView.propTypes = {
|
||||||
newFolder: PropTypes.func.isRequired,
|
newFolder: PropTypes.func.isRequired,
|
||||||
closeNewFolderModal: PropTypes.func.isRequired,
|
closeNewFolderModal: PropTypes.func.isRequired,
|
||||||
createFolder: PropTypes.func.isRequired,
|
createFolder: PropTypes.func.isRequired,
|
||||||
createFile: PropTypes.func.isRequired,
|
|
||||||
closeShareModal: PropTypes.func.isRequired,
|
closeShareModal: PropTypes.func.isRequired,
|
||||||
showEditorOptions: PropTypes.func.isRequired,
|
showEditorOptions: PropTypes.func.isRequired,
|
||||||
closeEditorOptions: PropTypes.func.isRequired,
|
closeEditorOptions: PropTypes.func.isRequired,
|
||||||
|
@ -577,6 +580,8 @@ IDEView.propTypes = {
|
||||||
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
startSketch: PropTypes.func.isRequired,
|
startSketch: PropTypes.func.isRequired,
|
||||||
|
openUploadFileModal: PropTypes.func.isRequired,
|
||||||
|
closeUploadFileModal: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
|
|
|
@ -10,6 +10,8 @@ const assets = (state = initialState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.SET_ASSETS:
|
case ActionTypes.SET_ASSETS:
|
||||||
return { list: action.assets, totalSize: action.totalSize };
|
return { list: action.assets, totalSize: action.totalSize };
|
||||||
|
case ActionTypes.DELETE_ASSET:
|
||||||
|
return { list: state.list.filter(asset => asset.key !== action.key) };
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ const initialState = {
|
||||||
preferencesIsVisible: false,
|
preferencesIsVisible: false,
|
||||||
projectOptionsVisible: false,
|
projectOptionsVisible: false,
|
||||||
newFolderModalVisible: false,
|
newFolderModalVisible: false,
|
||||||
|
uploadFileModalVisible: false,
|
||||||
shareModalVisible: false,
|
shareModalVisible: false,
|
||||||
shareModalProjectId: 'abcd',
|
shareModalProjectId: 'abcd',
|
||||||
shareModalProjectName: 'My Cute Sketch',
|
shareModalProjectName: 'My Cute Sketch',
|
||||||
|
@ -105,6 +106,10 @@ const ide = (state = initialState, action) => {
|
||||||
return Object.assign({}, state, { runtimeErrorWarningVisible: false });
|
return Object.assign({}, state, { runtimeErrorWarningVisible: false });
|
||||||
case ActionTypes.SHOW_RUNTIME_ERROR_WARNING:
|
case ActionTypes.SHOW_RUNTIME_ERROR_WARNING:
|
||||||
return Object.assign({}, state, { runtimeErrorWarningVisible: true });
|
return Object.assign({}, state, { runtimeErrorWarningVisible: true });
|
||||||
|
case ActionTypes.OPEN_UPLOAD_FILE_MODAL:
|
||||||
|
return Object.assign({}, state, { uploadFileModalVisible: true, parentId: action.parentId });
|
||||||
|
case ActionTypes.CLOSE_UPLOAD_FILE_MODAL:
|
||||||
|
return Object.assign({}, state, { uploadFileModalVisible: false });
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
27
client/modules/IDE/selectors/users.js
Normal file
27
client/modules/IDE/selectors/users.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||||
|
const getAuthenticated = state => state.user.authenticated;
|
||||||
|
const getTotalSize = state => state.user.totalSize;
|
||||||
|
const limit = __process.env.UPLOAD_LIMIT || 250000000;
|
||||||
|
|
||||||
|
export const getCanUploadMedia = createSelector(
|
||||||
|
getAuthenticated,
|
||||||
|
getTotalSize,
|
||||||
|
(authenticated, totalSize) => {
|
||||||
|
if (!authenticated) return false;
|
||||||
|
// eventually do the same thing for verified when
|
||||||
|
// email verification actually works
|
||||||
|
if (totalSize > limit) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getreachedTotalSizeLimit = createSelector(
|
||||||
|
getTotalSize,
|
||||||
|
(totalSize) => {
|
||||||
|
if (totalSize > limit) return true;
|
||||||
|
// if (totalSize > 1000) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
|
@ -9,7 +9,8 @@
|
||||||
|
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
& .sketch-list__dropdown-column {
|
position: relative;
|
||||||
|
& .asset-table__dropdown-column {
|
||||||
width: #{60 / $base-font-size}rem;
|
width: #{60 / $base-font-size}rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -66,3 +67,33 @@
|
||||||
font-size: #{16 / $base-font-size}rem;
|
font-size: #{16 / $base-font-size}rem;
|
||||||
padding: #{42 / $base-font-size}rem 0;
|
padding: #{42 / $base-font-size}rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.asset-table__total {
|
||||||
|
padding: 0 #{20 / $base-font-size}rem;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
@include themify() {
|
||||||
|
background-color: getThemifyVariable('background-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-table__dropdown-button {
|
||||||
|
width:#{25 / $base-font-size}rem;
|
||||||
|
height:#{25 / $base-font-size}rem;
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
& polygon {
|
||||||
|
fill: getThemifyVariable('dropdown-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-table__action-dialogue {
|
||||||
|
@extend %dropdown-open-right;
|
||||||
|
top: 63%;
|
||||||
|
right: calc(100% - 26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-table__action-option {
|
||||||
|
font-size: #{12 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
|
@ -37,15 +37,12 @@
|
||||||
|
|
||||||
.asset-current {
|
.asset-current {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 28px;
|
top: #{28 / $base-font-size}rem;
|
||||||
left: calc(200px * var(--percent));
|
left: 0;
|
||||||
margin-left: -8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-max {
|
.asset-max {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: #{210 / $base-font-size}rem;
|
||||||
transform: translate(210%); // align max label to right of asset-size-bar
|
|
||||||
padding-left: #{8 / $base-font-size}rem;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
.modal-content {
|
.modal-content {
|
||||||
@extend %modal;
|
@extend %modal;
|
||||||
min-height: #{150 / $base-font-size}rem;
|
min-height: #{150 / $base-font-size}rem;
|
||||||
width: #{700 / $base-font-size}rem;
|
width: #{500 / $base-font-size}rem;
|
||||||
padding: #{20 / $base-font-size}rem;
|
padding: #{20 / $base-font-size}rem;
|
||||||
.modal--reduced & {
|
.modal--reduced & {
|
||||||
//min-height: #{150 / $base-font-size}rem;
|
//min-height: #{150 / $base-font-size}rem;
|
||||||
|
@ -32,9 +32,8 @@
|
||||||
margin-bottom: #{20 / $base-font-size}rem;
|
margin-bottom: #{20 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-file-form, .new-file-folder {
|
.new-folder-form__input-wrapper, .new-file-form__input-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-file-form__name-label, .new-folder-form__name-label {
|
.new-file-form__name-label, .new-folder-form__name-label {
|
||||||
|
@ -43,6 +42,7 @@
|
||||||
|
|
||||||
.new-file-form__name-input, .new-folder-form__name-input {
|
.new-file-form__name-input, .new-folder-form__name-input {
|
||||||
margin-right: #{10 / $base-font-size}rem;
|
margin-right: #{10 / $base-font-size}rem;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__divider {
|
.modal__divider {
|
||||||
|
|
|
@ -60,13 +60,24 @@ export function deleteObjectsFromS3(keyList, callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteObjectFromS3(req, res) {
|
export function deleteObjectFromS3(req, res) {
|
||||||
const objectKey = req.params.object_key;
|
const { objectKey, userId } = req.params;
|
||||||
deleteObjectsFromS3([objectKey], () => {
|
let fullObjectKey;
|
||||||
|
if (userId) {
|
||||||
|
fullObjectKey = `${userId}/${objectKey}`;
|
||||||
|
} else {
|
||||||
|
fullObjectKey = objectKey;
|
||||||
|
}
|
||||||
|
deleteObjectsFromS3([fullObjectKey], () => {
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function signS3(req, res) {
|
export function signS3(req, res) {
|
||||||
|
const limit = process.env.UPLOAD_LIMIT || 250000000;
|
||||||
|
if (req.user.totalSize > limit) {
|
||||||
|
res.status(403).send({ message: 'user has uploaded the maximum size of assets.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const fileExtension = getExtension(req.body.name);
|
const fileExtension = getExtension(req.body.name);
|
||||||
const filename = uuid.v4() + fileExtension;
|
const filename = uuid.v4() + fileExtension;
|
||||||
const acl = 'public-read';
|
const acl = 'public-read';
|
||||||
|
@ -84,7 +95,7 @@ export function signS3(req, res) {
|
||||||
policy: p.policy,
|
policy: p.policy,
|
||||||
signature: p.signature
|
signature: p.signature
|
||||||
};
|
};
|
||||||
return res.json(result);
|
res.json(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copyObjectInS3(req, res) {
|
export function copyObjectInS3(req, res) {
|
||||||
|
@ -108,54 +119,65 @@ export function copyObjectInS3(req, res) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listObjectsInS3ForUser(req, res) {
|
export function listObjectsInS3ForUser(userId) {
|
||||||
const { username } = req.user;
|
let assets = [];
|
||||||
findUserByUsername(username, (user) => {
|
return new Promise((resolve) => {
|
||||||
const userId = user.id;
|
|
||||||
const params = {
|
const params = {
|
||||||
s3Params: {
|
s3Params: {
|
||||||
Bucket: `${process.env.S3_BUCKET}`,
|
Bucket: `${process.env.S3_BUCKET}`,
|
||||||
Prefix: `${userId}/`
|
Prefix: `${userId}/`
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let assets = [];
|
|
||||||
client.listObjects(params)
|
client.listObjects(params)
|
||||||
.on('data', (data) => {
|
.on('data', (data) => {
|
||||||
assets = assets.concat(data.Contents.map(object => ({ key: object.Key, size: object.Size })));
|
assets = assets.concat(data.Contents.map(object => ({ key: object.Key, size: object.Size })));
|
||||||
})
|
})
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
const projectAssets = [];
|
resolve();
|
||||||
getProjectsForUserId(userId).then((projects) => {
|
|
||||||
let totalSize = 0;
|
|
||||||
assets.forEach((asset) => {
|
|
||||||
const name = asset.key.split('/').pop();
|
|
||||||
const foundAsset = {
|
|
||||||
key: asset.key,
|
|
||||||
name,
|
|
||||||
size: asset.size,
|
|
||||||
url: `${process.env.S3_BUCKET_URL_BASE}${asset.key}`
|
|
||||||
};
|
|
||||||
totalSize += asset.size;
|
|
||||||
projects.some((project) => {
|
|
||||||
let found = false;
|
|
||||||
project.files.some((file) => {
|
|
||||||
if (!file.url) return false;
|
|
||||||
if (file.url.includes(asset.key)) {
|
|
||||||
found = true;
|
|
||||||
foundAsset.name = file.name;
|
|
||||||
foundAsset.sketchName = project.name;
|
|
||||||
foundAsset.sketchId = project.id;
|
|
||||||
foundAsset.url = file.url;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
return found;
|
|
||||||
});
|
|
||||||
projectAssets.push(foundAsset);
|
|
||||||
});
|
|
||||||
res.json({ assets: projectAssets, totalSize });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}).then(() => getProjectsForUserId(userId)).then((projects) => {
|
||||||
|
const projectAssets = [];
|
||||||
|
let totalSize = 0;
|
||||||
|
assets.forEach((asset) => {
|
||||||
|
const name = asset.key.split('/').pop();
|
||||||
|
const foundAsset = {
|
||||||
|
key: asset.key,
|
||||||
|
name,
|
||||||
|
size: asset.size,
|
||||||
|
url: `${process.env.S3_BUCKET_URL_BASE}${asset.key}`
|
||||||
|
};
|
||||||
|
totalSize += asset.size;
|
||||||
|
projects.some((project) => {
|
||||||
|
let found = false;
|
||||||
|
project.files.some((file) => {
|
||||||
|
if (!file.url) return false;
|
||||||
|
if (file.url.includes(asset.key)) {
|
||||||
|
found = true;
|
||||||
|
foundAsset.name = file.name;
|
||||||
|
foundAsset.sketchName = project.name;
|
||||||
|
foundAsset.sketchId = project.id;
|
||||||
|
foundAsset.url = file.url;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return found;
|
||||||
|
});
|
||||||
|
projectAssets.push(foundAsset);
|
||||||
|
});
|
||||||
|
return Promise.resolve({ assets: projectAssets, totalSize });
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log('got an error');
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listObjectsInS3ForUserRequestHandler(req, res) {
|
||||||
|
const { username } = req.user;
|
||||||
|
findUserByUsername(username, (user) => {
|
||||||
|
const userId = user.id;
|
||||||
|
listObjectsInS3ForUser(userId).then((objects) => {
|
||||||
|
res.json(objects);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,8 @@ export function userResponse(user) {
|
||||||
preferences: user.preferences,
|
preferences: user.preferences,
|
||||||
apiKeys: user.apiKeys,
|
apiKeys: user.apiKeys,
|
||||||
verified: user.verified,
|
verified: user.verified,
|
||||||
id: user._id
|
id: user._id,
|
||||||
|
totalSize: user.totalSize
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
29
server/migrations/populateTotalSize.js
Normal file
29
server/migrations/populateTotalSize.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
import User from '../models/user';
|
||||||
|
import { listObjectsInS3ForUser } from '../controllers/aws.controller';
|
||||||
|
|
||||||
|
// Connect to MongoDB
|
||||||
|
mongoose.Promise = global.Promise;
|
||||||
|
mongoose.connect(process.env.MONGO_URL, { useMongoClient: true });
|
||||||
|
mongoose.connection.on('error', () => {
|
||||||
|
console.error('MongoDB Connection Error. Please make sure that MongoDB is running.');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
User.find({}, {}, { timeout: true }).cursor().eachAsync((user) => {
|
||||||
|
console.log(user.id);
|
||||||
|
if (user.totalSize !== undefined) {
|
||||||
|
console.log('Already updated size for user: ' + user.username);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return listObjectsInS3ForUser(user.id).then((objects) => {
|
||||||
|
return User.findByIdAndUpdate(user.id, { $set: { totalSize: objects.totalSize } });
|
||||||
|
}).then(() => {
|
||||||
|
console.log('Updated new total size for user: ' + user.username);
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
console.log('Done iterating over every user');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
|
@ -1,3 +1,7 @@
|
||||||
require('@babel/register');
|
require('@babel/register');
|
||||||
require('@babel/polyfill');
|
require('@babel/polyfill');
|
||||||
require('./truncate');
|
const path = require('path');
|
||||||
|
require('dotenv').config({ path: path.resolve('.env') });
|
||||||
|
require('./populateTotalSize');
|
||||||
|
// require('./moveBucket');
|
||||||
|
// require('./truncate');
|
||||||
|
|
|
@ -66,7 +66,8 @@ const userSchema = new Schema({
|
||||||
soundOutput: { type: Boolean, default: false },
|
soundOutput: { type: Boolean, default: false },
|
||||||
theme: { type: String, default: 'light' },
|
theme: { type: String, default: 'light' },
|
||||||
autorefresh: { type: Boolean, default: false }
|
autorefresh: { type: Boolean, default: false }
|
||||||
}
|
},
|
||||||
|
totalSize: { type: Number, default: 0 }
|
||||||
}, { timestamps: true, usePushEach: true });
|
}, { timestamps: true, usePushEach: true });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,7 +6,7 @@ const router = new Router();
|
||||||
|
|
||||||
router.post('/S3/sign', isAuthenticated, AWSController.signS3);
|
router.post('/S3/sign', isAuthenticated, AWSController.signS3);
|
||||||
router.post('/S3/copy', isAuthenticated, AWSController.copyObjectInS3);
|
router.post('/S3/copy', isAuthenticated, AWSController.copyObjectInS3);
|
||||||
router.delete('/S3/:object_key', isAuthenticated, AWSController.deleteObjectFromS3);
|
router.delete('/S3/:userId?/:objectKey', isAuthenticated, AWSController.deleteObjectFromS3);
|
||||||
router.get('/S3/objects', AWSController.listObjectsInS3ForUser);
|
router.get('/S3/objects', AWSController.listObjectsInS3ForUserRequestHandler);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -31,6 +31,7 @@ export function renderIndex() {
|
||||||
window.process.env.EXAMPLES_ENABLED = ${process.env.EXAMPLES_ENABLED === 'false' ? false : true};
|
window.process.env.EXAMPLES_ENABLED = ${process.env.EXAMPLES_ENABLED === 'false' ? false : true};
|
||||||
window.process.env.UI_ACCESS_TOKEN_ENABLED = ${process.env.UI_ACCESS_TOKEN_ENABLED === 'false' ? false : true};
|
window.process.env.UI_ACCESS_TOKEN_ENABLED = ${process.env.UI_ACCESS_TOKEN_ENABLED === 'false' ? false : true};
|
||||||
window.process.env.UI_COLLECTIONS_ENABLED = ${process.env.UI_COLLECTIONS_ENABLED === 'false' ? false : true};
|
window.process.env.UI_COLLECTIONS_ENABLED = ${process.env.UI_COLLECTIONS_ENABLED === 'false' ? false : true};
|
||||||
|
window.process.env.UPLOAD_LIMIT = ${process.env.UPLOAD_LIMIT ? `${process.env.UPLOAD_LIMIT}` : undefined};
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
Loading…
Reference in a new issue