From d14518ac9162801defe1dc05acfcf93dee2b94a2 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Fri, 2 Aug 2019 19:24:24 -0700 Subject: [PATCH] Added ability to upload images to s3 and store them as part of ES object --- blueprints/api.py | 26 ++++++++++++++-- package.json | 3 +- src/actions/apiActions.js | 14 ++++++++- src/actions/userActions.js | 6 ++++ src/components/mediaUploadInput.js | 5 ++- src/containers/editorEditModeContainer.js | 13 ++++++-- src/middleware/joyceAPI.js | 21 +++++++++++-- src/middleware/joyceInterface.js | 8 +++-- src/modules/api.js | 15 +++++++-- src/reducers/inputs.js | 37 +++++++++++++++++------ 10 files changed, 123 insertions(+), 25 deletions(-) diff --git a/blueprints/api.py b/blueprints/api.py index 0b0b930..f2a359a 100644 --- a/blueprints/api.py +++ b/blueprints/api.py @@ -1,10 +1,12 @@ from flask import Blueprint, render_template, abort, jsonify, request from elasticsearch import Elasticsearch, RequestsHttpConnection from requests_aws4auth import AWS4Auth +import boto3 import json import sys import config import setup +import uuid sys.path.insert(0,'..') @@ -13,6 +15,18 @@ sys.path.insert(0,'..') reload(sys) sys.setdefaultencoding("utf-8") +s3 = boto3.client('s3') + +def create_presigned_post(): + bucket_name = config.JOYCE_S3_BUCKET + key_name = str(uuid.uuid4()) + response = s3.generate_presigned_post( + bucket_name, + key_name, + ExpiresIn = 3600 + ) + return response + if config.ENVIRONMENT == 'local': es = Elasticsearch(config.ELASTICSEARCH_LOCAL_HOST) @@ -62,7 +76,6 @@ def es_get_document(index, id): return data def es_index_document(index, id, body): - print(index) res = es.index( index=index, doc_type='doc', @@ -325,7 +338,6 @@ def search_text(): data = json.loads(request.data) results = es_search_text(data.get('data')) return jsonify(results) - # # Refresh ES # TODO: Restrict to dev only @@ -333,3 +345,13 @@ def search_text(): def refresh_es(): setup.es_setup() return 'Success!' + +# +# Get Signed URL for Upload +# +@api.route('/signed_post/') +def media_post_url(): + data = jsonify(create_presigned_post()) + print 'hey' + # + '?signature=' + url.fields.signature + '&AWSAccessKeyId=' + url.fields.AWSAccessKeyId + return data \ No newline at end of file diff --git a/package.json b/package.json index 9dbd8c6..abf2860 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "react-router": "^4.2.0", "react-router-dom": "^4.2.2", "react-router-redux": "^5.0.0-alpha.9", - "redux": "^3.7.2" + "redux": "^3.7.2", + "uuid": "^3.3.2" }, "devDependencies": { "@fortawesome/fontawesome-free": "^5.5.0", diff --git a/src/actions/apiActions.js b/src/actions/apiActions.js index 6cba6c3..81894ea 100644 --- a/src/actions/apiActions.js +++ b/src/actions/apiActions.js @@ -47,7 +47,19 @@ const apiActions = { type: 'GET_SEARCH_RESULTS', data: response.data, status: response.status ? response.status : 'request' - }) + }), + uploadMediaToS3Request: (response, data) => + ({ + type: 'UPLOAD_TO_S3_REQUEST', + file: data, + signed_post: response.data + }), + uploadMediaToS3Response: (response) => + ({ + type: 'UPLOAD_TO_S3_RESPONSE', + status: response.status, + url: response.url ? response.url : undefined + }), } export default apiActions \ No newline at end of file diff --git a/src/actions/userActions.js b/src/actions/userActions.js index d8129f7..5797939 100644 --- a/src/actions/userActions.js +++ b/src/actions/userActions.js @@ -122,6 +122,12 @@ const userActions = { hideAdmin: () => ({ type: 'HIDE_ADMIN_HEADER' + }), + // Media + uploadMediaInput: input => + ({ + type: 'UPLOAD_MEDIA_SUBMIT', + data: input }) } diff --git a/src/components/mediaUploadInput.js b/src/components/mediaUploadInput.js index 914446d..a1d1f12 100644 --- a/src/components/mediaUploadInput.js +++ b/src/components/mediaUploadInput.js @@ -1,11 +1,14 @@ import React from 'react' -const MediaUploadInput = ({input, onChange}) => +const MediaUploadInput = ({input, s3Path, onChange, onUpload}) =>
+
+ +
export default MediaUploadInput \ No newline at end of file diff --git a/src/containers/editorEditModeContainer.js b/src/containers/editorEditModeContainer.js index fd7ef47..0dec22c 100644 --- a/src/containers/editorEditModeContainer.js +++ b/src/containers/editorEditModeContainer.js @@ -23,6 +23,7 @@ const EditorEditMode = ({ onColorPickerInputChange, onColorSwatchClick, onMediaInputChange, + onMediaUpload, cancelEdit, onSubmitClick, onToolButtonClick, @@ -56,9 +57,12 @@ const EditorEditMode = ({ onColorSwatchClick={onColorSwatchClick} /> } - {docType === 'media' && - - } + {docType === 'media' && inputs.s3Path && +

File uploaded!

+ } + {docType === 'media' && !inputs.s3Path && + + } { onMediaInputChange: input => { dispatch(actions.updateMediaInput(input)) }, + onMediaUpload: input => { + dispatch(actions.uploadMediaInput(input)) + }, cancelEdit: () => { dispatch(actions.cancelEdit()) }, diff --git a/src/middleware/joyceAPI.js b/src/middleware/joyceAPI.js index 4f9755e..e528b89 100644 --- a/src/middleware/joyceAPI.js +++ b/src/middleware/joyceAPI.js @@ -24,8 +24,7 @@ const joyceAPI = store => next => action => { if (action.status === 'request') { if (action.id) { api.HTTPPostWriteDocument(action.id, action.docType, action.data).then(response => - store.dispatch(actions.saveDocument(response) - ) + store.dispatch(actions.saveDocument(response)) )} else { api.HTTPPutCreateDocument(action.docType, action.data).then(response => store.dispatch(actions.saveDocument(response)) @@ -46,7 +45,23 @@ const joyceAPI = store => next => action => { store.dispatch(actions.getSearchResults(response)) ) } - break + break + case 'UPLOAD_MEDIA_SUBMIT': + api.HTTPGetSignedPost().then(response => + store.dispatch(actions.uploadMediaToS3Request(response, action.data[0]) + )) + break + case 'UPLOAD_TO_S3_REQUEST': + const formData = new FormData() + const url = action.signed_post.url + formData.append('AWSAccessKeyId', action.signed_post.fields.AWSAccessKeyId) + formData.append('key', action.signed_post.fields.key) + formData.append('policy', action.signed_post.fields.policy) + formData.append('signature', action.signed_post.fields.signature) + formData.append('file', action.file) + api.HTTPPostMedia(url, formData).then(response => + store.dispatch(actions.uploadMediaToS3Response(response)) + ) default: break } diff --git a/src/middleware/joyceInterface.js b/src/middleware/joyceInterface.js index cf53e84..15bad3d 100644 --- a/src/middleware/joyceInterface.js +++ b/src/middleware/joyceInterface.js @@ -3,6 +3,7 @@ import { EditorState, Modifier } from 'draft-js' import { stateToHTML } from 'draft-js-export-html' import actions from '../actions' +import api from '../modules/api' import helpers from '../modules/helpers' import { validateSubmittedDocument, validateSubmittedAnnotation } from '../modules/validation' import { html_export_options, convertToSearchText, linkDecorator } from '../modules/editorSettings.js' @@ -25,11 +26,14 @@ const joyceInterface = store => next => action => { const data = { title: action.inputs.documentTitle, html_source: stateToHTML(textContent, html_export_options), - search_text: convertToSearchText(textContent) + search_text: convertToSearchText(textContent), } if (action.docType === 'tags') { data.color = action.inputs.colorPicker } + if (action.docType === 'media') { + data.s3Path = action.inputs.s3Path + } if (action.currentDocument.id) { data.id = action.currentDocument.id } @@ -90,7 +94,7 @@ const joyceInterface = store => next => action => { // Search Action Middleware case 'CLICK_SEARCH': store.dispatch(actions.getSearchResults({data: action.data})) - break + break default: break } diff --git a/src/modules/api.js b/src/modules/api.js index 2099d93..0828c68 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -43,9 +43,20 @@ const api = { axios.get(apiRoute + 'refresh/').then(res => { return {status: 'success', data: res.data} }).catch(error => { - console.log(error) return {status: 'error', data: error} - }), + }), + HTTPGetSignedPost: () => + axios.get(apiRoute + 'signed_post/').then(res=> { + return {status: 'success', data: res.data} + }).catch(error => { + return {status: 'error', data: error} + }), + HTTPPostMedia: (url, formData) => + axios.post(url, formData, {headers: {'Content-Type': 'image/*', 'ACL': 'public-read'}}).then(res=> { + return {status: 'success', url: url + formData.get('key')} + }).catch(error => { + return {status: 'error', data: error} + }) } export default api \ No newline at end of file diff --git a/src/reducers/inputs.js b/src/reducers/inputs.js index dacfeb0..345f295 100644 --- a/src/reducers/inputs.js +++ b/src/reducers/inputs.js @@ -2,18 +2,34 @@ const initialState = { documentTitle: '', search: '', colorPicker: '', - fileUpload: undefined + uploadFile: undefined, + s3Path: undefined } const inputs = (state=initialState, action) => { switch(action.type) { // Document Title case 'GET_DOCUMENT_TEXT': - if (action.status === 'success' && action.state === 'currentDocument') { + console.log('state', action.state) + console.log('status', action.status) + console.log('docType', action.docType) + if (action.status === 'success' && action.state === 'currentDocument' && ['tags', 'media'].indexOf(action.docType) <= 0 ) { return { ...state, documentTitle: action.data.title } + } else if (action.status === 'success' && action.docType === 'tags') { + return { + ...state, + documentTitle: action.data.title, + colorPicker: action.data.color + } + } else if (action.status === 'success' && action.state === 'currentDocument' && action.docType === 'media') { + return { + ...state, + documentTitle: action.data.title, + s3Path: action.data.s3Path + } } else { return state } case 'CREATE_DOCUMENT': return { @@ -32,13 +48,6 @@ const inputs = (state=initialState, action) => { search: action.data } // Color Picker - case 'GET_DOCUMENT_TEXT': - if (action.status === 'success' && action.docType === 'tags') { - return { - ...state, - colorPicker: action.data.color - } - } else { return state } case 'SAVE_DOCUMENT': if (action.status === 'success' && action.docType === 'tags') { return { @@ -65,10 +74,18 @@ const inputs = (state=initialState, action) => { case 'UPDATE_MEDIA_INPUT': return { ...state, - fileUpload: action.data + uploadFile: action.data } default: return state + // S3 File + case 'UPLOAD_TO_S3_RESPONSE': + if (action.status === 'success') { + return { + ...state, + s3Path: action.url + } + } else { return state } } }