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 SHOW_FOLDER_CHILDREN = 'SHOW_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 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 SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
export const SET_ASSETS = 'SET_ASSETS';
export const DELETE_ASSET = 'DELETE_ASSET';
export const TOGGLE_DIRECTION = 'TOGGLE_DIRECTION';
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 {
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() {
return {
type: ActionTypes.EXPAND_SIDEBAR

View file

@ -66,11 +66,11 @@ export function dropzoneAcceptCallback(userId, file, done) {
done();
})
.catch((response) => {
file.custom_status = 'rejected'; // eslint-disable-line
if (response.data.responseText && response.data.responseText.message) {
file.custom_status = 'rejected'; // eslint-disable-line
if (response.data && response.data.responseText && 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 { Helmet } from 'react-helmet';
import prettyBytes from 'pretty-bytes';
import InlineSVG from 'react-inlinesvg';
import Loader from '../../App/components/loader';
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 {
constructor(props) {
@ -16,10 +153,7 @@ class AssetList extends React.Component {
}
getAssetsTitle() {
if (!this.props.username || this.props.username === this.props.user.username) {
return 'p5.js Web Editor | My assets';
}
return `p5.js Web Editor | ${this.props.username}'s assets`;
return 'p5.js Web Editor | My assets';
}
hasAssets() {
@ -39,7 +173,6 @@ class AssetList extends React.Component {
}
render() {
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
const { assetList } = this.props;
return (
<div className="asset-table-container">
@ -52,24 +185,14 @@ class AssetList extends React.Component {
<table className="asset-table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Size</th>
<th scope="col">Sketch</th>
<th>Name</th>
<th>Size</th>
<th>Sketch</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{assetList.map(asset =>
(
<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>
))}
{assetList.map(asset => <AssetListRow asset={asset} key={asset.key} />)}
</tbody>
</table>}
</div>
@ -81,7 +204,6 @@ AssetList.propTypes = {
user: PropTypes.shape({
username: PropTypes.string
}).isRequired,
username: PropTypes.string.isRequired,
assetList: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
@ -97,7 +219,6 @@ function mapStateToProps(state) {
return {
user: state.user,
assetList: state.assets.list,
totalSize: state.assets.totalSize,
loading: state.loading
};
}

View file

@ -3,8 +3,9 @@ import React from 'react';
import { connect } from 'react-redux';
import prettyBytes from 'pretty-bytes';
const MB_TO_B = 1000 * 1000;
const MAX_SIZE_B = 250 * MB_TO_B;
const __process = (typeof global !== 'undefined' ? global : window).process;
const limit = __process.env.UPLOAD_LIMIT || 250000000;
const MAX_SIZE_B = limit;
const formatPercent = (percent) => {
const percentUsed = percent * 100;
@ -17,7 +18,7 @@ const formatPercent = (percent) => {
/* Eventually, this copy should be Total / 250 MB Used */
const AssetSize = ({ totalSize }) => {
if (!totalSize) {
if (totalSize === undefined) {
return null;
}
@ -25,9 +26,10 @@ const AssetSize = ({ totalSize }) => {
const sizeLimit = prettyBytes(MAX_SIZE_B);
const percentValue = totalSize / MAX_SIZE_B;
const percent = formatPercent(percentValue);
const percentSize = percentValue < 1 ? percentValue : 1;
return (
<div className="asset-size" style={{ '--percent': percentValue }}>
<div className="asset-size" style={{ '--percent': percentSize }}>
<div className="asset-size-bar" />
<p className="asset-current">{currentSize} ({percent})</p>
<p className="asset-max">Max: {sizeLimit}</p>
@ -42,7 +44,7 @@ AssetSize.propTypes = {
function mapStateToProps(state) {
return {
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,
thumbnailHeight: 200,
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),
sending: this.props.dropzoneSendingCallback,
complete: this.props.dropzoneCompleteCallback

View file

@ -22,16 +22,18 @@ class NewFileForm extends React.Component {
handleSubmit(this.createFile)(data);
}}
>
<label className="new-file-form__name-label" htmlFor="name">Name:</label>
<input
className="new-file-form__name-input"
id="name"
type="text"
placeholder="Name"
{...domOnlyProps(name)}
ref={(element) => { this.fileName = element; }}
/>
<input type="submit" value="Add File" aria-label="add file" />
<div className="new-file-form__input-wrapper">
<label className="new-file-form__name-label" htmlFor="name">Name:</label>
<input
className="new-file-form__name-input"
id="name"
type="text"
placeholder="Name"
{...domOnlyProps(name)}
ref={(element) => { this.fileName = element; }}
/>
<input type="submit" value="Add File" aria-label="add file" />
</div>
{name.touched && name.error && <span className="form-error">{name.error}</span>}
</form>
);

View file

@ -1,10 +1,13 @@
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { reduxForm } from 'redux-form';
import classNames from 'classnames';
import InlineSVG from 'react-inlinesvg';
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';
const exitUrl = require('../../../images/exit.svg');
@ -28,16 +31,12 @@ class NewFileModal extends React.Component {
}
render() {
const modalClass = classNames({
'modal': true,
'modal--reduced': !this.props.canUploadMedia
});
return (
<section className={modalClass} ref={(element) => { this.modal = element; }}>
<section className="modal" ref={(element) => { this.modal = element; }}>
<div className="modal-content">
<div className="modal__header">
<h2 className="modal__title">Add File</h2>
<button className="modal__exit-button" onClick={this.props.closeModal}>
<h2 className="modal__title">Create File</h2>
<button className="modal__exit-button" onClick={this.props.closeNewFileModal}>
<InlineSVG src={exitUrl} alt="Close New File Modal" />
</button>
</div>
@ -45,17 +44,6 @@ class NewFileModal extends React.Component {
focusOnModal={this.focusOnModal}
{...this.props}
/>
{(() => {
if (this.props.canUploadMedia) {
return (
<div>
<p className="modal__divider">OR</p>
<FileUploader />
</div>
);
}
return '';
})()}
</div>
</section>
);
@ -63,8 +51,8 @@ class NewFileModal extends React.Component {
}
NewFileModal.propTypes = {
closeModal: PropTypes.func.isRequired,
canUploadMedia: PropTypes.bool.isRequired
createFile: PropTypes.func.isRequired,
closeNewFileModal: PropTypes.func.isRequired
};
function validate(formProps) {
@ -79,9 +67,22 @@ function validate(formProps) {
return errors;
}
function mapStateToProps(state) {
return {
canUploadMedia: getCanUploadMedia(state),
reachedTotalSizeLimit: getreachedTotalSizeLimit(state)
};
}
export default reduxForm({
form: 'new-file',
fields: ['name'],
validate
})(NewFileModal);
function mapDispatchToProps(dispatch) {
return bindActionCreators({ createFile, closeNewFileModal }, dispatch);
}
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>
<input
className="new-folder-form__name-input"
id="name"
type="text"
placeholder="Name"
ref={(element) => { this.fileName = element; }}
{...domOnlyProps(name)}
/>
<input type="submit" value="Add Folder" aria-label="add folder" />
<div className="new-folder-form__input-wrapper">
<label className="new-folder-form__name-label" htmlFor="name">Name:</label>
<input
className="new-folder-form__name-input"
id="name"
type="text"
placeholder="Name"
ref={(element) => { this.fileName = element; }}
{...domOnlyProps(name)}
/>
<input type="submit" value="Add Folder" aria-label="add folder" />
</div>
{name.touched && name.error && <span className="form-error">{name.error}</span>}
</form>
);

View file

@ -16,7 +16,7 @@ class NewFolderModal extends React.Component {
<section className="modal" ref={(element) => { this.newFolderModal = element; }} >
<div className="modal-content-folder">
<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}>
<InlineSVG src={exitUrl} alt="Close New Folder Modal" />
</button>

View file

@ -97,7 +97,7 @@ class Sidebar extends React.Component {
onBlur={this.onBlurComponent}
onFocus={this.onFocusComponent}
>
Add folder
Create folder
</button>
</li>
<li>
@ -110,7 +110,20 @@ class Sidebar extends React.Component {
onBlur={this.onBlurComponent}
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>
</li>
</ul>
@ -137,6 +150,7 @@ Sidebar.propTypes = {
openProjectOptions: PropTypes.func.isRequired,
closeProjectOptions: PropTypes.func.isRequired,
newFolder: PropTypes.func.isRequired,
openUploadFileModal: PropTypes.func.isRequired,
owner: PropTypes.shape({
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 NewFileModal from '../components/NewFileModal';
import NewFolderModal from '../components/NewFolderModal';
import UploadFileModal from '../components/UploadFileModal';
import ShareModal from '../components/ShareModal';
import KeyboardShortcutModal from '../components/KeyboardShortcutModal';
import ErrorModal from '../components/ErrorModal';
@ -238,6 +239,8 @@ class IDEView extends React.Component {
newFolder={this.props.newFolder}
user={this.props.user}
owner={this.props.project.owner}
openUploadFileModal={this.props.openUploadFileModal}
closeUploadFileModal={this.props.closeUploadFileModal}
/>
<SplitPane
split="vertical"
@ -349,12 +352,8 @@ class IDEView extends React.Component {
</SplitPane>
</SplitPane>
</div>
{this.props.ide.modalIsVisible &&
<NewFileModal
canUploadMedia={this.props.user.authenticated}
closeModal={this.props.closeNewFileModal}
createFile={this.props.createFile}
/>
{ this.props.ide.modalIsVisible &&
<NewFileModal />
}
{this.props.ide.newFolderModalVisible &&
<NewFolderModal
@ -362,6 +361,11 @@ class IDEView extends React.Component {
createFolder={this.props.createFolder}
/>
}
{this.props.ide.uploadFileModalVisible &&
<UploadFileModal
closeModal={this.props.closeUploadFileModal}
/>
}
{ this.props.location.pathname === '/about' &&
<Overlay
title="About"
@ -475,6 +479,7 @@ IDEView.propTypes = {
justOpenedProject: PropTypes.bool.isRequired,
errorType: PropTypes.string,
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
uploadFileModalVisible: PropTypes.bool.isRequired
}).isRequired,
stopSketch: PropTypes.func.isRequired,
project: PropTypes.shape({
@ -532,7 +537,6 @@ IDEView.propTypes = {
}).isRequired,
dispatchConsoleEvent: PropTypes.func.isRequired,
newFile: PropTypes.func.isRequired,
closeNewFileModal: PropTypes.func.isRequired,
expandSidebar: PropTypes.func.isRequired,
collapseSidebar: PropTypes.func.isRequired,
cloneProject: PropTypes.func.isRequired,
@ -545,7 +549,6 @@ IDEView.propTypes = {
newFolder: PropTypes.func.isRequired,
closeNewFolderModal: PropTypes.func.isRequired,
createFolder: PropTypes.func.isRequired,
createFile: PropTypes.func.isRequired,
closeShareModal: PropTypes.func.isRequired,
showEditorOptions: PropTypes.func.isRequired,
closeEditorOptions: PropTypes.func.isRequired,
@ -577,6 +580,8 @@ IDEView.propTypes = {
showRuntimeErrorWarning: PropTypes.func.isRequired,
hideRuntimeErrorWarning: PropTypes.func.isRequired,
startSketch: PropTypes.func.isRequired,
openUploadFileModal: PropTypes.func.isRequired,
closeUploadFileModal: PropTypes.func.isRequired
};
function mapStateToProps(state) {

View file

@ -10,6 +10,8 @@ const assets = (state = initialState, action) => {
switch (action.type) {
case ActionTypes.SET_ASSETS:
return { list: action.assets, totalSize: action.totalSize };
case ActionTypes.DELETE_ASSET:
return { list: state.list.filter(asset => asset.key !== action.key) };
default:
return state;
}

View file

@ -9,6 +9,7 @@ const initialState = {
preferencesIsVisible: false,
projectOptionsVisible: false,
newFolderModalVisible: false,
uploadFileModalVisible: false,
shareModalVisible: false,
shareModalProjectId: 'abcd',
shareModalProjectName: 'My Cute Sketch',
@ -105,6 +106,10 @@ const ide = (state = initialState, action) => {
return Object.assign({}, state, { runtimeErrorWarningVisible: false });
case ActionTypes.SHOW_RUNTIME_ERROR_WARNING:
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:
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%;
border-spacing: 0;
& .sketch-list__dropdown-column {
position: relative;
& .asset-table__dropdown-column {
width: #{60 / $base-font-size}rem;
position: relative;
}
@ -66,3 +67,33 @@
font-size: #{16 / $base-font-size}rem;
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 {
position: absolute;
top: 28px;
left: calc(200px * var(--percent));
margin-left: -8px;
top: #{28 / $base-font-size}rem;
left: 0;
}
.asset-max {
position: absolute;
top: 0;
left: 0;
transform: translate(210%); // align max label to right of asset-size-bar
padding-left: #{8 / $base-font-size}rem;
left: #{210 / $base-font-size}rem;
}

View file

@ -10,7 +10,7 @@
.modal-content {
@extend %modal;
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;
.modal--reduced & {
//min-height: #{150 / $base-font-size}rem;
@ -32,9 +32,8 @@
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;
flex-wrap: wrap;
}
.new-file-form__name-label, .new-folder-form__name-label {
@ -43,6 +42,7 @@
.new-file-form__name-input, .new-folder-form__name-input {
margin-right: #{10 / $base-font-size}rem;
flex: 1;
}
.modal__divider {

View file

@ -60,13 +60,24 @@ export function deleteObjectsFromS3(keyList, callback) {
}
export function deleteObjectFromS3(req, res) {
const objectKey = req.params.object_key;
deleteObjectsFromS3([objectKey], () => {
const { objectKey, userId } = req.params;
let fullObjectKey;
if (userId) {
fullObjectKey = `${userId}/${objectKey}`;
} else {
fullObjectKey = objectKey;
}
deleteObjectsFromS3([fullObjectKey], () => {
res.json({ success: true });
});
}
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 filename = uuid.v4() + fileExtension;
const acl = 'public-read';
@ -84,7 +95,7 @@ export function signS3(req, res) {
policy: p.policy,
signature: p.signature
};
return res.json(result);
res.json(result);
}
export function copyObjectInS3(req, res) {
@ -108,54 +119,65 @@ export function copyObjectInS3(req, res) {
});
}
export function listObjectsInS3ForUser(req, res) {
const { username } = req.user;
findUserByUsername(username, (user) => {
const userId = user.id;
export function listObjectsInS3ForUser(userId) {
let assets = [];
return new Promise((resolve) => {
const params = {
s3Params: {
Bucket: `${process.env.S3_BUCKET}`,
Prefix: `${userId}/`
}
};
let assets = [];
client.listObjects(params)
.on('data', (data) => {
assets = assets.concat(data.Contents.map(object => ({ key: object.Key, size: object.Size })));
})
.on('end', () => {
const projectAssets = [];
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 });
});
resolve();
});
}).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,
apiKeys: user.apiKeys,
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/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 },
theme: { type: String, default: 'light' },
autorefresh: { type: Boolean, default: false }
}
},
totalSize: { type: Number, default: 0 }
}, { timestamps: true, usePushEach: true });
/**

View file

@ -6,7 +6,7 @@ const router = new Router();
router.post('/S3/sign', isAuthenticated, AWSController.signS3);
router.post('/S3/copy', isAuthenticated, AWSController.copyObjectInS3);
router.delete('/S3/:object_key', isAuthenticated, AWSController.deleteObjectFromS3);
router.get('/S3/objects', AWSController.listObjectsInS3ForUser);
router.delete('/S3/:userId?/:objectKey', isAuthenticated, AWSController.deleteObjectFromS3);
router.get('/S3/objects', AWSController.listObjectsInS3ForUserRequestHandler);
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.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.UPLOAD_LIMIT = ${process.env.UPLOAD_LIMIT ? `${process.env.UPLOAD_LIMIT}` : undefined};
</script>
</head>
<body>