Merge pull request #1309 from processing/feature/public-api-asset-limit

Feature/public api asset limit, Fixes #168
This commit is contained in:
Cassie Tarakajian 2020-03-04 16:54:00 -05:00 committed by GitHub
commit a6f59fd309
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 512 additions and 146 deletions

View file

@ -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';

View file

@ -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
});
});
}; };
} }

View file

@ -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

View file

@ -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.');
}); });
} }
}; };

View file

@ -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
}; };
} }

View file

@ -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,
}; };
} }

View file

@ -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

View file

@ -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>
); );

View file

@ -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);

View file

@ -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>
); );

View file

@ -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>

View file

@ -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
}), }),

View 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);

View file

@ -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) {

View file

@ -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;
} }

View file

@ -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;
} }

View 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;
}
);

View file

@ -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;
}

View file

@ -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;
} }

View file

@ -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 {

View file

@ -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);
});
}); });
} }

View file

@ -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
}; };
} }

View 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);
});

View file

@ -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');

View file

@ -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 });
/** /**

View file

@ -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;

View file

@ -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>