Skip to content

Instantly share code, notes, and snippets.

@sheldonbaker
Last active June 24, 2016 02:37
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sheldonbaker/54255e5588046912c07e to your computer and use it in GitHub Desktop.
Save sheldonbaker/54255e5588046912c07e to your computer and use it in GitHub Desktop.
s3 image upload with ember-data - requires a Policy model (required by Amazon for browser uploads) generated by your server
`import DS from 'ember-data'`
# https://aws.amazon.com/articles/1434
Policy = DS.Model.extend
key: DS.attr('string')
bucket: DS.attr('string')
acl: DS.attr('string')
'AWSAccessKeyId': DS.attr('string')
'Content-Type': DS.attr('string')
policy: DS.attr('string')
signature: DS.attr('string')
`export default Policy`
`import DS from 'ember-data'`
S3Image = DS.Model.extend
key: DS.attr('string')
policy: DS.belongsTo('policy')
blob: DS.attr('raw')
fileName: DS.attr('string')
baseName: Ember.computed.alias 'id'
extension: ''
blobTypeDidChange: (->
return unless blob = @get('blob')
blobType = blob.type
extension = switch blobType
when 'image/png' then '.png'
when 'image/jpg', 'image/jpeg' then '.jpg'
else throw new Ember.Error("Unexpected S3Image blob type: " + blobType)
@set('extension', extension)
).observes('blob')
_fileName: (->
return unless baseName = @get('baseName')
return unless extension = @get('extension')
[baseName, extension].join('')
).property('baseName', 'extension')
_key: (->
return unless policyKey = @get('policy.key')
return unless fileName = @get('fileName')
policyKey.replace('${filename}', fileName)
).property('policy.key', 'fileName')
# HACK: Using observers to replicate behaviour of computed properties
# since we need key and fileName (which are DS.attrs) to change when we set
# a key or filename
keyShouldChange: (->
@set('key', @get('_key'))
).observes('policy.key', 'fileName')
fileNameShouldChange: (->
@set 'fileName', @get('_fileName')
).observes('baseName', 'extension', 'blob')
# HACK: Set by adapter for controllers' convenience
currentSave: null
`export default S3Image`
`import DS from 'ember-data'`
Attachment = DS.Model.extend
caption: DS.attr('string')
imageKey: DS.attr('string')
imageProcessedAt: DS.attr('moment', { readOnly: true })
imageUrl: DS.attr('string', { readOnly: true })
imageThumbnailUrl: DS.attr('string', { readOnly: true })
createdAt: DS.attr('moment', { readOnly: true })
imageBlob: null
`export default Attachment`
`import DS from 'ember-data'`
S3ImageAdapter = DS.RESTAdapter.extend Ember.Evented,
# http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
generateIdForRecord: ->
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace /[xy]/g, (c) ->
r = Math.random() * 16 | 0
v = (if c is "x" then r else r & 0x3 | 0x8)
v.toString 16
updateRecord: ->
throw new Ember.Error("Cannot update an s3Image")
deleteRecord: ->
throw new Ember.Error("Cannot delete an s3Image")
buildURL: (type, record) ->
['https://', record.get('policy.bucket'), '.s3-us-west-2.amazonaws.com'].join('')
createRecord: (store, type, record) ->
data = {}
serializer = store.serializerFor(type.typeKey)
serializer.serializeIntoHash(data, type, record, { includeId: true })
promise = @ajax(this.buildURL(type.typeKey, record), 'POST', { data: data })
# HACK: Allows consuming controller to gracefully abort an upload
# since the promise-spec doesn't accomodate for that
record.set('currentSave', promise)
promise.finally ->
record.set('currentSave', null)
promise
# Populate a FormData instead of an ajax options hash
ajaxOptions: (url, type, hash) ->
hash = hash || {}
hash.url = url
hash.type = type
data = hash.data.s3_image
formData = new FormData
for key of data
args = [].concat(data[key])
args.unshift(key)
formData.append.apply(formData, args)
hash.data = formData
hash
ajax: (url, type, hash) ->
adapter = @
hash = @ajaxOptions(url, type, hash)
xhr = null
proxy = null
promise = new Ember.RSVP.Promise (resolve, reject) ->
xhr = new XMLHttpRequest()
xhr.upload.addEventListener 'progress', (e) ->
proxy.trigger 'progress', e.loaded / e.total if e.lengthComputable
, false
xhr.upload.addEventListener 'abort', (e) ->
Ember.run null, reject, adapter.ajaxError(xhr)
, false
xhr.upload.addEventListener 'error', (e) ->
Ember.run null, reject, adapter.ajaxError(xhr)
, false
xhr.addEventListener 'load', (e) ->
status = xhr.status
if status >= 200 && status < 300 || status == 304
Ember.run null, resolve, null
else if status == 400
error = xhr.responseXML.getElementsByTagName('Error')[0]
message = error.getElementsByTagName('Message')[0].textContent
Ember.run null, reject, new DS.InvalidError { blob: [message] }
else
Ember.run null, reject, adapter.ajaxError(xhr)
, false
xhr.open(hash.type, hash.url, true)
xhr.send(hash.data)
# Use a proxy that we can register a progress handler and abort method on
proxy = Ember.Object.createWithMixins Ember.PromiseProxyMixin, Ember.Evented,
promise: promise
abort: ->
xhr.abort() unless @get('isSettled')
ajaxError: (xhr) ->
xhr
`export default S3ImageAdapter`
`import Ember from 'ember'`
ImageEditView = Ember.View.extend
classNames: ['incident incident-modal']
# Remind IE8 users to upgrade...
cannotUpload: (->
!window.Blob || !window.FormData
).property().volatile()
change: (e) ->
$input = @$(e.target)
# Only listen for change events to file inputs
return unless $input.attr('type') == 'file'
files = $input.get(0).files
@controller.send('filesWereAdded', files) if files
# Just replace to fully clear the input
$input.replaceWith($input.clone())
`export default ImageEditView`
`import Ember from 'ember'`
`import DS from 'ember-data'`
UsageController = Ember.ObjectController.extend
needs: ['company']
company: Ember.computed.alias 'controllers.company'
policyBinding: 'controllers.company.policy'
addFiles: (files) ->
for i in [0..files.length - 1] by 1
@addFile(files[i])
addFile: (file) ->
store = @store
policy = @get('policy')
# Pass along the file blob
s3Image = store.createRecord 's3Image',
blob: file
policy: policy
# Use an 'attachment' record to wrap an s3Image (and have a caption, comments, etc.)
attachment = @get('attachments').createRecord
s3Image: s3Image # For progress bars
imageKey: s3Image.get('key')
imageBlob: s3Image.get('blob')
s3Image.save() # Send off to s3
# HACK - adapter sets `currentSave` xhr that we can ask for progress events on or call abort on
Ember.run.once ->
if save = s3Image.get('currentSave')
save.on 'progress', (pct) ->
# Use for a progress bar somewhere in a template
actions:
filesWereAdded: (files) ->
@addFiles(files)
`export default UsageController`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment