Mailspring/src/flux/tasks/file-upload-task.coffee
Evan Morikawa 61eec6b21a fix(database): add DatabaseStore.atomically to handle read/write issues
Summary:
Fixes T3566
Fixes T3567

Test Plan: new tests

Reviewers: dillon, bengotow

Reviewed By: bengotow

Maniphest Tasks: T3566, T3567

Differential Revision: https://phab.nylas.com/D2025
2015-09-15 20:27:52 -04:00

162 lines
5.6 KiB
CoffeeScript

fs = require 'fs'
_ = require 'underscore'
crypto = require 'crypto'
pathUtils = require 'path'
Task = require './task'
{APIError} = require '../errors'
File = require '../models/file'
Message = require '../models/message'
Actions = require '../actions'
AccountStore = require '../stores/account-store'
DatabaseStore = require '../stores/database-store'
{isTempId} = require '../models/utils'
NylasAPI = require '../nylas-api'
Utils = require '../models/utils'
UploadCounter = 0
class FileUploadTask extends Task
constructor: (@filePath, @messageClientId) ->
super
@_startDate = Date.now()
@_startId = UploadCounter
UploadCounter += 1
@progress = null # The progress checking timer.
performLocal: ->
return Promise.reject(new Error("Must pass an absolute path to upload")) unless @filePath?.length
return Promise.reject(new Error("Must be attached to a messageClientId")) unless isTempId(@messageClientId)
Actions.uploadStateChanged @_uploadData("pending")
Promise.resolve()
performRemote: ->
Actions.uploadStateChanged @_uploadData("started")
DatabaseStore.findBy(Message, {clientId: @messageClientId}).then (draft) =>
return Promise.resolve(Task.Status.Finished) unless draft
@_accountId = draft.accountId
@_makeRequest()
.then @_performRemoteParseFile
.then @_performRemoteAttachFile
.then (file) =>
Actions.uploadStateChanged @_uploadData("completed")
Actions.fileUploaded(file: file, uploadData: @_uploadData("completed"))
return Promise.resolve(Task.Status.Finished)
.catch APIError, (err) =>
if err.statusCode in NylasAPI.PermanentErrorCodes
msg = "There was a problem uploading this file. Please try again later."
Actions.uploadStateChanged(@_uploadData("failed"))
Actions.postNotification({message: msg, type: "error"})
return Promise.resolve(Task.Status.Finished)
else if err.statusCode is NylasAPI.CancelledErrorCode
Actions.uploadStateChanged(@_uploadData("aborted"))
Actions.fileAborted(@_uploadData("aborted"))
return Promise.resolve(Task.Status.Finished)
else
return Promise.resolve(Task.Status.Retry)
_makeRequest: =>
started = (req) =>
@req = req
@progress = setInterval =>
Actions.uploadStateChanged(@_uploadData("progress"))
, 250
cleanup = =>
clearInterval(@progress)
@req = null
NylasAPI.makeRequest
path: "/files"
accountId: @_accountId
method: "POST"
json: false
formData: @_formData()
started: started
.finally(cleanup)
_performRemoteParseFile: (rawResponseString) =>
# The Nylas API returns the file json wrapped in an array.
# Since we requested `json:false` the response will come back as
# a raw string.
json = JSON.parse(rawResponseString)
file = (new File).fromJSON(json[0])
Promise.resolve(file)
_performRemoteAttachFile: (file) =>
# The minute we know what file is associated with the upload, we need
# to fire an Action to notify a popout window's FileUploadStore that
# these two objects are linked. We unfortunately can't wait until
# `_attachFileToDraft` resolves, because that will resolve after the
# DB transaction is completed AND all of the callbacks have fired.
# Unfortunately in the callback chain is a render method which means
# that the upload will be left on the page for a split second before
# we know the file has been uploaded.
#
# Associating the upload with the file ahead of time can let the
# Composer know which ones to ignore when de-duping the upload/file
# listing.
Actions.linkFileToUpload(file: file, uploadData: @_uploadData("completed"))
DraftStore = require '../stores/draft-store'
# We have a `DatabaseStore.atomically` block surrounding the object
# right before we persist changes
DraftStore.sessionForClientId(@messageClientId).then (session) =>
files = _.clone(session.draft().files) ? []
files.push(file)
session.changes.add({files})
session.changes.commit().then ->
return file
cancel: ->
super
# Note: When you call cancel, we stop the request, which causes
# NylasAPI.makeRequest to reject with an error.
return unless @req
@req.abort()
# Helper Methods
_formData: ->
file: # Must be named `file` as per the Nylas API spec
value: fs.createReadStream(@filePath)
options:
filename: @_uploadData().fileName
# returns:
# messageClientId - The clientId of the message (draft) we're uploading to
# filePath - The full absolute local system file path
# fileSize - The size in bytes
# fileName - The basename of the file
# bytesUploaded - Current number of bytes uploaded
# state - one of "pending" "started" "progress" "completed" "aborted" "failed"
_uploadData: (state) ->
@_memoUploadData ?=
uploadTaskId: @id
startDate: @_startDate
startId: @_startId
messageClientId: @messageClientId
filePath: @filePath
fileSize: @_getFileSize(@filePath)
fileName: pathUtils.basename(@filePath)
@_memoUploadData.bytesUploaded = @_getBytesUploaded()
@_memoUploadData.state = state if state?
return _.extend({}, @_memoUploadData)
_getFileSize: (path) ->
fs.statSync(path)["size"]
_getBytesUploaded: ->
# https://github.com/request/request/issues/941
# http://stackoverflow.com/questions/12098713/upload-progress-request
@req?.req?.connection?._bytesDispatched ? 0
_accountId: ->
AccountStore.current()?.id
module.exports = FileUploadTask