diff --git a/client/constants.js b/client/constants.js index b5d477b2..477409fc 100644 --- a/client/constants.js +++ b/client/constants.js @@ -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'; diff --git a/client/modules/IDE/actions/assets.js b/client/modules/IDE/actions/assets.js index e2b49cf6..483e6d4e 100644 --- a/client/modules/IDE/actions/assets.js +++ b/client/modules/IDE/actions/assets.js @@ -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 + }); + }); }; } diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index cf573573..239dc6c7 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -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 diff --git a/client/modules/IDE/actions/uploader.js b/client/modules/IDE/actions/uploader.js index f2f12585..602ac3e5 100644 --- a/client/modules/IDE/actions/uploader.js +++ b/client/modules/IDE/actions/uploader.js @@ -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.'); }); } }; diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index 8c5d0826..da834dc2 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -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 ( + + + + {asset.name} + + + {prettyBytes(asset.size)} + + { asset.sketchId && {asset.sketchName} } + + + + {optionsOpen && + } + + + ); + } +} + +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 (
@@ -52,24 +185,14 @@ class AssetList extends React.Component { - - - + + + + - {assetList.map(asset => - ( - - - - - - ))} + {assetList.map(asset => )}
NameSizeSketchNameSizeSketch
- - {asset.name} - - {prettyBytes(asset.size)}{asset.sketchName}
}
@@ -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 }; } diff --git a/client/modules/IDE/components/AssetSize.jsx b/client/modules/IDE/components/AssetSize.jsx index db083708..2e4c1282 100644 --- a/client/modules/IDE/components/AssetSize.jsx +++ b/client/modules/IDE/components/AssetSize.jsx @@ -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 ( -
+

{currentSize} ({percent})

Max: {sizeLimit}

@@ -42,7 +44,7 @@ AssetSize.propTypes = { function mapStateToProps(state) { return { user: state.user, - totalSize: state.assets.totalSize, + totalSize: state.user.totalSize || state.assets.totalSize, }; } diff --git a/client/modules/IDE/components/FileUploader.jsx b/client/modules/IDE/components/FileUploader.jsx index 50fe6062..c9515f5c 100644 --- a/client/modules/IDE/components/FileUploader.jsx +++ b/client/modules/IDE/components/FileUploader.jsx @@ -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 diff --git a/client/modules/IDE/components/NewFileForm.jsx b/client/modules/IDE/components/NewFileForm.jsx index d0a6b8ab..926a9925 100644 --- a/client/modules/IDE/components/NewFileForm.jsx +++ b/client/modules/IDE/components/NewFileForm.jsx @@ -22,16 +22,18 @@ class NewFileForm extends React.Component { handleSubmit(this.createFile)(data); }} > - - { this.fileName = element; }} - /> - +
+ + { this.fileName = element; }} + /> + +
{name.touched && name.error && {name.error}} ); diff --git a/client/modules/IDE/components/NewFileModal.jsx b/client/modules/IDE/components/NewFileModal.jsx index 433c3daa..e7747d2d 100644 --- a/client/modules/IDE/components/NewFileModal.jsx +++ b/client/modules/IDE/components/NewFileModal.jsx @@ -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 ( -
{ this.modal = element; }}> +
{ this.modal = element; }}>
-

Add File

-
@@ -45,17 +44,6 @@ class NewFileModal extends React.Component { focusOnModal={this.focusOnModal} {...this.props} /> - {(() => { - if (this.props.canUploadMedia) { - return ( -
-

OR

- -
- ); - } - return ''; - })()}
); @@ -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); diff --git a/client/modules/IDE/components/NewFolderForm.jsx b/client/modules/IDE/components/NewFolderForm.jsx index 3490de64..92b91442 100644 --- a/client/modules/IDE/components/NewFolderForm.jsx +++ b/client/modules/IDE/components/NewFolderForm.jsx @@ -25,16 +25,18 @@ class NewFolderForm extends React.Component { } }} > - - { this.fileName = element; }} - {...domOnlyProps(name)} - /> - +
+ + { this.fileName = element; }} + {...domOnlyProps(name)} + /> + +
{name.touched && name.error && {name.error}} ); diff --git a/client/modules/IDE/components/NewFolderModal.jsx b/client/modules/IDE/components/NewFolderModal.jsx index 60483ce8..4af43a89 100644 --- a/client/modules/IDE/components/NewFolderModal.jsx +++ b/client/modules/IDE/components/NewFolderModal.jsx @@ -16,7 +16,7 @@ class NewFolderModal extends React.Component {
{ this.newFolderModal = element; }} >
-

Add Folder

+

Create Folder

diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx index 2ea492c2..9a57ff4a 100644 --- a/client/modules/IDE/components/Sidebar.jsx +++ b/client/modules/IDE/components/Sidebar.jsx @@ -97,7 +97,7 @@ class Sidebar extends React.Component { onBlur={this.onBlurComponent} onFocus={this.onFocusComponent} > - Add folder + Create folder
  • @@ -110,7 +110,20 @@ class Sidebar extends React.Component { onBlur={this.onBlurComponent} onFocus={this.onFocusComponent} > - Add file + Create file + +
  • +
  • +
  • @@ -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 }), diff --git a/client/modules/IDE/components/UploadFileModal.jsx b/client/modules/IDE/components/UploadFileModal.jsx new file mode 100644 index 00000000..491a9d10 --- /dev/null +++ b/client/modules/IDE/components/UploadFileModal.jsx @@ -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 ( +
    { this.modal = element; }}> +
    +
    +

    Upload File

    + +
    + { this.props.reachedTotalSizeLimit && +

    + { + `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 ` + } + assets + . +

    + } + { !this.props.reachedTotalSizeLimit && +
    + +
    + } +
    +
    + ); + } +} + +function mapStateToProps(state) { + return { + reachedTotalSizeLimit: getreachedTotalSizeLimit(state) + }; +} + +export default connect(mapStateToProps)(UploadFileModal); diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index b573d223..f7179a14 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -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} />
    - {this.props.ide.modalIsVisible && - + { this.props.ide.modalIsVisible && + } {this.props.ide.newFolderModalVisible && } + {this.props.ide.uploadFileModalVisible && + + } { this.props.location.pathname === '/about' && { 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; } diff --git a/client/modules/IDE/reducers/ide.js b/client/modules/IDE/reducers/ide.js index 852b718a..db2c31e6 100644 --- a/client/modules/IDE/reducers/ide.js +++ b/client/modules/IDE/reducers/ide.js @@ -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; } diff --git a/client/modules/IDE/selectors/users.js b/client/modules/IDE/selectors/users.js new file mode 100644 index 00000000..c656e283 --- /dev/null +++ b/client/modules/IDE/selectors/users.js @@ -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; + } +); diff --git a/client/styles/components/_asset-list.scss b/client/styles/components/_asset-list.scss index 6d4e30b9..0a34f343 100644 --- a/client/styles/components/_asset-list.scss +++ b/client/styles/components/_asset-list.scss @@ -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; +} diff --git a/client/styles/components/_asset-size.scss b/client/styles/components/_asset-size.scss index 809f56e5..56a6c9b9 100644 --- a/client/styles/components/_asset-size.scss +++ b/client/styles/components/_asset-size.scss @@ -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; } diff --git a/client/styles/components/_modal.scss b/client/styles/components/_modal.scss index b7b56f78..752b4434 100644 --- a/client/styles/components/_modal.scss +++ b/client/styles/components/_modal.scss @@ -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 { diff --git a/server/controllers/aws.controller.js b/server/controllers/aws.controller.js index 3cdd7dfc..0b7d5d85 100644 --- a/server/controllers/aws.controller.js +++ b/server/controllers/aws.controller.js @@ -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); + }); }); } diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index 9c0d5076..d326e373 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -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 }; } diff --git a/server/migrations/populateTotalSize.js b/server/migrations/populateTotalSize.js new file mode 100644 index 00000000..19e9f623 --- /dev/null +++ b/server/migrations/populateTotalSize.js @@ -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); +}); \ No newline at end of file diff --git a/server/migrations/start.js b/server/migrations/start.js index 0c68e0eb..a3127620 100644 --- a/server/migrations/start.js +++ b/server/migrations/start.js @@ -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'); diff --git a/server/models/user.js b/server/models/user.js index 0a1a4dc3..98c0e1fd 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -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 }); /** diff --git a/server/routes/aws.routes.js b/server/routes/aws.routes.js index 0bda0f8b..850bb190 100644 --- a/server/routes/aws.routes.js +++ b/server/routes/aws.routes.js @@ -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; diff --git a/server/views/index.js b/server/views/index.js index d8848351..52b98985 100644 --- a/server/views/index.js +++ b/server/views/index.js @@ -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};