fix(uploads): Consolidate logic, use Promisify, path.join

This commit is contained in:
Ben Gotow 2016-01-29 14:43:48 -08:00
parent 960d2cf67b
commit 2027901deb
6 changed files with 68 additions and 92 deletions

View file

@ -32,12 +32,11 @@ class ImageAttachmentComponent extends AttachmentComponent
<RetinaImg name="image-download-button.png" mode={RetinaImg.Mode.ContentPreserve} /> <RetinaImg name="image-download-button.png" mode={RetinaImg.Mode.ContentPreserve} />
_imgOrLoader: -> _imgOrLoader: ->
if @props.download if @props.download and @props.download.percent <= 5
if @props.download.percent <= 5
<div style={width: "100%", height: "100px"}> <div style={width: "100%", height: "100px"}>
<Spinner visible={true} /> <Spinner visible={true} />
</div> </div>
else else if @props.download and @props.download.percent < 100
<DraggableImg src={"#{@props.targetPath}?percent=#{@props.download.percent}"} /> <DraggableImg src={"#{@props.targetPath}?percent=#{@props.download.percent}"} />
else else
<DraggableImg src={@props.targetPath} /> <DraggableImg src={@props.targetPath} />

View file

@ -53,7 +53,7 @@ describe 'FileUploadStore', ->
spyOn(FileUploadStore, '_verifyUpload').andCallFake -> Promise.resolve() spyOn(FileUploadStore, '_verifyUpload').andCallFake -> Promise.resolve()
spyOn(FileUploadStore, '_prepareTargetDir').andCallFake -> Promise.resolve() spyOn(FileUploadStore, '_prepareTargetDir').andCallFake -> Promise.resolve()
spyOn(FileUploadStore, '_copyUpload').andCallFake => Promise.resolve(@upload) spyOn(FileUploadStore, '_copyUpload').andCallFake => Promise.resolve(@upload)
spyOn(FileUploadStore, '_saveUpload').andCallThrough() spyOn(FileUploadStore, '_applySessionChanges').andCallThrough()
it "throws if no messageClientId or path is provided", -> it "throws if no messageClientId or path is provided", ->
expect(-> Actions.addAttachment()).toThrow() expect(-> Actions.addAttachment()).toThrow()
@ -68,7 +68,7 @@ describe 'FileUploadStore', ->
expect(FileUploadStore._verifyUpload).toHaveBeenCalled() expect(FileUploadStore._verifyUpload).toHaveBeenCalled()
expect(FileUploadStore._prepareTargetDir).toHaveBeenCalled() expect(FileUploadStore._prepareTargetDir).toHaveBeenCalled()
expect(FileUploadStore._copyUpload).toHaveBeenCalled() expect(FileUploadStore._copyUpload).toHaveBeenCalled()
expect(FileUploadStore._saveUpload).toHaveBeenCalled() expect(FileUploadStore._applySessionChanges).toHaveBeenCalled()
expect(@session.changes.add).toHaveBeenCalledWith({uploads: [@upload]}) expect(@session.changes.add).toHaveBeenCalledWith({uploads: [@upload]})
@ -78,7 +78,7 @@ describe 'FileUploadStore', ->
spyOn(FileUploadStore, '_deleteUpload').andCallFake => Promise.resolve(@upload) spyOn(FileUploadStore, '_deleteUpload').andCallFake => Promise.resolve(@upload)
spyOn(fs, 'rmdir') spyOn(fs, 'rmdir')
it 'removes upload correctly', -> it 'removes the upload from the draft', ->
@draft.uploads = [{id: 'u2'}, @upload] @draft.uploads = [{id: 'u2'}, @upload]
waitsForPromise => waitsForPromise =>
FileUploadStore._onRemoveAttachment(@upload) FileUploadStore._onRemoveAttachment(@upload)
@ -86,13 +86,12 @@ describe 'FileUploadStore', ->
expect(@session.changes.add).toHaveBeenCalledWith uploads: [{id: 'u2'}] expect(@session.changes.add).toHaveBeenCalledWith uploads: [{id: 'u2'}]
expect(fs.rmdir).not.toHaveBeenCalled() expect(fs.rmdir).not.toHaveBeenCalled()
it 'removes upload and removes directory if no more uploads left dor message', -> it 'calls deleteUpload to clean up the filesystem', ->
@draft.uploads = [@upload] @draft.uploads = [@upload]
waitsForPromise => waitsForPromise =>
FileUploadStore._onRemoveAttachment(@upload) FileUploadStore._onRemoveAttachment(@upload)
.then => .then =>
expect(@session.changes.add).toHaveBeenCalledWith uploads: [] expect(FileUploadStore._deleteUpload).toHaveBeenCalled()
expect(fs.rmdir).toHaveBeenCalled()
describe '_getFileStats', -> describe '_getFileStats', ->
@ -111,8 +110,8 @@ describe 'FileUploadStore', ->
waitsForPromise -> waitsForPromise ->
FileUploadStore._getFileStats(argsObj) FileUploadStore._getFileStats(argsObj)
.then -> throw new Error('It should fail.') .then -> throw new Error('It should fail.')
.catch (msg) -> .catch (error) ->
expect(msg.indexOf(fpath)).toBe 0 expect(error.message.indexOf(fpath)).toBe 0
describe '_verifyUpload', -> describe '_verifyUpload', ->
@ -122,16 +121,16 @@ describe 'FileUploadStore', ->
waitsForPromise -> waitsForPromise ->
FileUploadStore._verifyUpload(upload) FileUploadStore._verifyUpload(upload)
.then -> throw new Error('It should fail.') .then -> throw new Error('It should fail.')
.catch (msg) -> .catch (error) ->
expect(msg.indexOf(filename + ' is a directory')).toBe 0 expect(error.message.indexOf(filename + ' is a directory')).toBe 0
it 'throws if the file is more than 25MB', -> it 'throws if the file is more than 25MB', ->
upload = new Upload(msgId, fpath, {size: 25*1000000+1, isDirectory: -> false}) upload = new Upload(msgId, fpath, {size: 25*1000000+1, isDirectory: -> false})
waitsForPromise -> waitsForPromise ->
FileUploadStore._verifyUpload(upload) FileUploadStore._verifyUpload(upload)
.then -> throw new Error('It should fail.') .then -> throw new Error('It should fail.')
.catch (msg) -> .catch (error) ->
expect(msg.indexOf(filename + ' cannot')).toBe 0 expect(error.message.indexOf(filename + ' cannot')).toBe 0
it 'resolves otherwise', -> it 'resolves otherwise', ->
upload = new Upload(msgId, fpath, {size: 1234, isDirectory: -> false}) upload = new Upload(msgId, fpath, {size: 1234, isDirectory: -> false})

View file

@ -445,9 +445,11 @@ class Actions
# File Actions # File Actions
# Some file actions only need to be processed in their current window # Some file actions only need to be processed in their current window
@addAttachment: ActionScopeGlobal @addAttachment: ActionScopeWindow
@selectAttachment: ActionScopeWindow @selectAttachment: ActionScopeWindow
@removeAttachment: ActionScopeGlobal @removeAttachment: ActionScopeWindow
@attachmentUploaded: ActionScopeWindow
@fetchAndOpenFile: ActionScopeWindow @fetchAndOpenFile: ActionScopeWindow
@fetchAndSaveFile: ActionScopeWindow @fetchAndSaveFile: ActionScopeWindow
@fetchFile: ActionScopeWindow @fetchFile: ActionScopeWindow

View file

@ -12,11 +12,7 @@ NylasAPI = require '../nylas-api'
RegExpUtils = require '../../regexp-utils' RegExpUtils = require '../../regexp-utils'
Promise.promisifyAll(fs) Promise.promisifyAll(fs)
mkdirpAsync = Promise.promisify(mkdirp)
mkdirpAsync = (folder) ->
new Promise (resolve, reject) ->
mkdirp folder, (err) ->
if err then reject(err) else resolve(folder)
State = State =
Unstarted: 'unstarted' Unstarted: 'unstarted'

View file

@ -9,6 +9,8 @@ Message = require '../models/message'
DraftStore = require './draft-store' DraftStore = require './draft-store'
DatabaseStore = require './database-store' DatabaseStore = require './database-store'
Promise.promisifyAll(fs)
mkdirpAsync = Promise.promisify(mkdirp)
UPLOAD_DIR = path.join(NylasEnv.getConfigDirPath(), 'uploads') UPLOAD_DIR = path.join(NylasEnv.getConfigDirPath(), 'uploads')
@ -27,9 +29,10 @@ class FileUploadStore extends NylasStore
Upload: Upload Upload: Upload
constructor: -> constructor: ->
@listenTo Actions.selectAttachment, @_onSelectAttachment
@listenTo Actions.addAttachment, @_onAddAttachment @listenTo Actions.addAttachment, @_onAddAttachment
@listenTo Actions.selectAttachment, @_onSelectAttachment
@listenTo Actions.removeAttachment, @_onRemoveAttachment @listenTo Actions.removeAttachment, @_onRemoveAttachment
@listenTo Actions.attachmentUploaded, @_onAttachmentUploaded
@listenTo DatabaseStore, @_onDataChanged @listenTo DatabaseStore, @_onDataChanged
mkdirp.sync(UPLOAD_DIR) mkdirp.sync(UPLOAD_DIR)
@ -39,63 +42,49 @@ class FileUploadStore extends NylasStore
_onDataChanged: (change) => _onDataChanged: (change) =>
return unless NylasEnv.isMainWindow() return unless NylasEnv.isMainWindow()
return unless change.objectClass is Message.name and change.type is 'unpersist' return unless change.objectClass is Message.name and change.type is 'unpersist'
change.objects.forEach (message) =>
messageDir = "#{UPLOAD_DIR}/#{message.clientId}"
uploads = message.uploads
if uploads and uploads.length > 0 change.objects.forEach (message) =>
Promise.all(uploads.map (upload) => @_deleteUpload(upload)) messageDir = path.join(UPLOAD_DIR, message.clientId)
.then -> for upload in message.uploads
fs.rmdir(messageDir) @_deleteUpload(upload)
.catch (err) ->
console.warn(err)
_onSelectAttachment: ({messageClientId}) -> _onSelectAttachment: ({messageClientId}) ->
@_verifyId(messageClientId) @_verifyId(messageClientId)
# When the dialog closes, it triggers `Actions.addAttachment` # When the dialog closes, it triggers `Actions.addAttachment`
NylasEnv.showOpenDialog {properties: ['openFile', 'multiSelections']}, (pathsToOpen) -> NylasEnv.showOpenDialog {properties: ['openFile', 'multiSelections']}, (pathsToOpen) ->
return if not pathsToOpen? return unless pathsToOpen?
pathsToOpen = [pathsToOpen] if _.isString(pathsToOpen) pathsToOpen = [pathsToOpen] if _.isString(pathsToOpen)
pathsToOpen.forEach (filePath) -> pathsToOpen.forEach (filePath) ->
Actions.addAttachment({messageClientId, filePath}) Actions.addAttachment({messageClientId, filePath})
_onAddAttachment: ({messageClientId, filePath}) -> _onAddAttachment: ({messageClientId, filePath}) ->
return unless NylasEnv.isMainWindow()
@_verifyId(messageClientId) @_verifyId(messageClientId)
@_getFileStats({messageClientId, filePath}) @_getFileStats({messageClientId, filePath})
.then(@_makeUpload) .then(@_makeUpload)
.then(@_verifyUpload) .then(@_verifyUpload)
.then(@_prepareTargetDir) .then(@_prepareTargetDir)
.then(@_copyUpload) .then(@_copyUpload)
.then(@_saveUpload) .then (upload) =>
@_applySessionChanges upload.messageClientId, (uploads) ->
uploads.concat([upload])
.catch(@_onAttachFileError) .catch(@_onAttachFileError)
_onRemoveAttachment: (upload) -> _onRemoveAttachment: (upload) ->
return unless NylasEnv.isMainWindow()
return Promise.resolve() unless upload return Promise.resolve() unless upload
{messageClientId} = upload
@_applySessionChanges upload.messageClientId, (uploads) ->
_.reject(uploads, _.matcher({id: upload.id}))
@_deleteUpload(upload).catch(@_onAttachFileError)
_onAttachmentUploaded: (upload) ->
return Promise.resolve() unless upload
@_deleteUpload(upload) @_deleteUpload(upload)
.then (upload) =>
DraftStore.sessionForClientId(messageClientId)
.then (session) =>
uploads = session.draft().uploads
uploads = _.reject(uploads, ({id}) -> id is upload.id)
if uploads.length is 0
fs.rmdir("#{UPLOAD_DIR}/#{messageClientId}")
session.changes.add({uploads})
.catch(@_onAttachFileError)
_onAttachFileError: (message) ->
{remote} = require('electron')
dialog = remote.require('dialog')
console.error(message)
dialog.showMessageBox
type: 'info',
buttons: ['OK'],
message: 'Cannot Attach File',
detail: message
_onAttachFileError: (error) ->
NylasEnv.showErrorDialog(error.message)
# Helpers # Helpers
@ -104,12 +93,10 @@ class FileUploadStore extends NylasStore
throw new Error "You need to pass the ID of the message (draft) this Action refers to" throw new Error "You need to pass the ID of the message (draft) this Action refers to"
_getFileStats: ({messageClientId, filePath}) -> _getFileStats: ({messageClientId, filePath}) ->
return new Promise (resolve, reject) -> fs.statAsync(filePath).then (stats) =>
fs.stat filePath, (err, stats) => Promise.resolve({messageClientId, filePath, stats})
if err .catch (err) ->
reject("#{filePath} could not be found, or has invalid file permissions.") Promise.reject(new Error("#{filePath} could not be found, or has invalid file permissions."))
else
resolve({messageClientId, filePath, stats})
_makeUpload: ({messageClientId, filePath, stats}) -> _makeUpload: ({messageClientId, filePath, stats}) ->
Promise.resolve(new Upload(messageClientId, filePath, stats)) Promise.resolve(new Upload(messageClientId, filePath, stats))
@ -117,19 +104,14 @@ class FileUploadStore extends NylasStore
_verifyUpload: (upload) -> _verifyUpload: (upload) ->
{filename, stats} = upload {filename, stats} = upload
if stats.isDirectory() if stats.isDirectory()
Promise.reject("#{filename} is a directory. Try compressing it and attaching it again.") Promise.reject(new Error("#{filename} is a directory. Try compressing it and attaching it again."))
else if stats.size > 25 * 1000000 else if stats.size > 25 * 1000000
Promise.reject("#{filename} cannot be attached because it is larger than 25MB.") Promise.reject(new Error("#{filename} cannot be attached because it is larger than 25MB."))
else else
Promise.resolve(upload) Promise.resolve(upload)
_prepareTargetDir: (upload) => _prepareTargetDir: (upload) =>
return new Promise (resolve, reject) -> mkdirpAsync(upload.targetDir).thenReturn(upload)
mkdirp upload.targetDir, (err) ->
if err
reject("Error creating folder for upload: `#{upload.filename}`")
else
resolve(upload)
_copyUpload: (upload) -> _copyUpload: (upload) ->
return new Promise (resolve, reject) => return new Promise (resolve, reject) =>
@ -138,25 +120,25 @@ class FileUploadStore extends NylasStore
writeStream = fs.createWriteStream(targetPath) writeStream = fs.createWriteStream(targetPath)
readStream.on 'error', -> readStream.on 'error', ->
reject("Error while reading file from #{originPath}") reject(new Error("Could not read file at path: #{originPath}"))
writeStream.on 'error', -> writeStream.on 'error', ->
reject("Error while writing file #{upload.filename}") reject(new Error("Could not write #{upload.filename} to uploads directory."))
readStream.on 'end', -> readStream.on 'end', ->
resolve(upload) resolve(upload)
readStream.pipe(writeStream) readStream.pipe(writeStream)
_deleteUpload: (upload) -> _deleteUpload: (upload) =>
return new Promise (resolve, reject) -> fs.unlinkAsync(upload.targetPath).then ->
fs.unlink upload.targetPath, (err) -> fs.rmdirAsync(upload.targetDir).then ->
reject("Error removing file #{upload.filename}") if err fs.rmdir path.join(UPLOAD_DIR, upload.messageClientId), (err) ->
fs.rmdir upload.targetDir, (err) -> # Will fail if it's not empty, which is fine.
reject("Error removing directory for file #{upload.filename}") if err Promise.resolve(upload)
resolve(upload) .catch ->
Promise.reject(new Error("Error cleaning up file #{upload.filename}"))
_saveUpload: (upload) => _applySessionChanges: (messageClientId, changeFunction) =>
DraftStore.sessionForClientId(upload.messageClientId) DraftStore.sessionForClientId(messageClientId).then (session) =>
.then (session) => uploads = changeFunction(session.draft().uploads)
uploads = session.draft().uploads.concat [upload]
session.changes.add({uploads}) session.changes.add({uploads})
module.exports = new FileUploadStore() module.exports = new FileUploadStore()

View file

@ -111,6 +111,9 @@ class SendDraftTask extends Task
@draft.uploads.splice(@draft.uploads.indexOf(upload), 1) @draft.uploads.splice(@draft.uploads.indexOf(upload), 1)
@draft.files.push(file) @draft.files.push(file)
# Deletes the attachment from the uploads folder
Actions.attachmentUploaded(upload)
_sendAndCreateMessage: => _sendAndCreateMessage: =>
NylasAPI.makeRequest NylasAPI.makeRequest
path: "/send" path: "/send"
@ -170,11 +173,6 @@ class SendDraftTask extends Task
if NylasEnv.config.get("core.sending.sounds") if NylasEnv.config.get("core.sending.sounds")
SoundRegistry.playSound('send') SoundRegistry.playSound('send')
# Remove attachments we were waiting to upload
# Call the Action to do this
for upload in @draft.uploads
Actions.removeAttachment(upload)
return Promise.resolve(Task.Status.Success) return Promise.resolve(Task.Status.Success)
_onError: (err) => _onError: (err) =>