Added ability to upload images to s3 and store them as part of ES object

This commit is contained in:
Alex Hunt 2019-08-02 19:24:24 -07:00
parent 9c74599950
commit d14518ac91
10 changed files with 123 additions and 25 deletions

View File

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

View File

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

View File

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

View File

@ -122,6 +122,12 @@ const userActions = {
hideAdmin: () =>
({
type: 'HIDE_ADMIN_HEADER'
}),
// Media
uploadMediaInput: input =>
({
type: 'UPLOAD_MEDIA_SUBMIT',
data: input
})
}

View File

@ -1,11 +1,14 @@
import React from 'react'
const MediaUploadInput = ({input, onChange}) =>
const MediaUploadInput = ({input, s3Path, onChange, onUpload}) =>
<div className="input-group mb-3">
<div className="custom-file">
<input type="file" className="custom-file-input" id="media_input" onChange={onChange}/>
<label className="custom-file-label" htmlFor="media_input">{input ? input[0].name : 'Choose file'}</label>
</div>
<div className="input-group-append">
<button className="btn btn-outline-warning" type="button" disabled={input ? false : true} onClick={()=>onUpload(input)}>Upload</button>
</div>
</div>
export default MediaUploadInput

View File

@ -23,6 +23,7 @@ const EditorEditMode = ({
onColorPickerInputChange,
onColorSwatchClick,
onMediaInputChange,
onMediaUpload,
cancelEdit,
onSubmitClick,
onToolButtonClick,
@ -56,9 +57,12 @@ const EditorEditMode = ({
onColorSwatchClick={onColorSwatchClick}
/>
}
{docType === 'media' &&
<MediaUploadInput input={inputs.fileUpload} onChange={onMediaInputChange}/>
}
{docType === 'media' && inputs.s3Path &&
<p>File uploaded!</p>
}
{docType === 'media' && !inputs.s3Path &&
<MediaUploadInput input={inputs.uploadFile} onChange={onMediaInputChange} onUpload={onMediaUpload}/>
}
</EditorAttributeContentBlock>
<EditorBottomBarContentBlock>
<EditorSubmitOptions
@ -100,6 +104,9 @@ const mapDispatchToProps = dispatch => {
onMediaInputChange: input => {
dispatch(actions.updateMediaInput(input))
},
onMediaUpload: input => {
dispatch(actions.uploadMediaInput(input))
},
cancelEdit: () => {
dispatch(actions.cancelEdit())
},

View File

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

View File

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

View File

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

View File

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