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