mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-02-25 08:35:16 +08:00
fix(es6): Convert FileDownloadStore to JS
This commit is contained in:
parent
20b6a6e070
commit
5fa379bccf
3 changed files with 440 additions and 346 deletions
|
@ -4,10 +4,10 @@ path = require 'path'
|
|||
NylasAPI = require '../../src/flux/nylas-api'
|
||||
File = require('../../src/flux/models/file').default
|
||||
Message = require('../../src/flux/models/message').default
|
||||
FileDownloadStore = require '../../src/flux/stores/file-download-store'
|
||||
FileDownloadStore = require('../../src/flux/stores/file-download-store').default
|
||||
{Download} = require('../../src/flux/stores/file-download-store')
|
||||
AccountStore = require '../../src/flux/stores/account-store'
|
||||
|
||||
Download = FileDownloadStore.Download
|
||||
|
||||
describe "FileDownloadStore.Download", ->
|
||||
beforeEach ->
|
||||
|
@ -113,13 +113,13 @@ describe "FileDownloadStore", ->
|
|||
FileDownloadStore._checkForDownloadedFile(f).then (downloaded) ->
|
||||
expect(downloaded).toBe(false)
|
||||
|
||||
describe "_newMailReceived", ->
|
||||
describe "_onNewMailReceived", ->
|
||||
it "should fetch attachments if the setting is on-receive", ->
|
||||
spyOn(FileDownloadStore, '_fetch')
|
||||
spyOn(NylasEnv.config, 'get').andCallFake (key) ->
|
||||
return 'on-receive' if key is 'core.attachments.downloadPolicy'
|
||||
return null
|
||||
FileDownloadStore._newMailReceived(message: [new Message(files: [new File()])])
|
||||
FileDownloadStore._onNewMailReceived(message: [new Message(files: [new File()])])
|
||||
expect(FileDownloadStore._fetch).toHaveBeenCalled()
|
||||
|
||||
it "should not fetch attachments otherwise", ->
|
||||
|
@ -127,7 +127,7 @@ describe "FileDownloadStore", ->
|
|||
spyOn(NylasEnv.config, 'get').andCallFake (key) ->
|
||||
return 'on-read' if key is 'core.attachments.downloadPolicy'
|
||||
return null
|
||||
FileDownloadStore._newMailReceived(message: [new Message(files: [new File()])])
|
||||
FileDownloadStore._onNewMailReceived(message: [new Message(files: [new File()])])
|
||||
expect(FileDownloadStore._fetch).not.toHaveBeenCalled()
|
||||
|
||||
describe "_runDownload", ->
|
||||
|
|
|
@ -1,341 +0,0 @@
|
|||
os = require 'os'
|
||||
fs = require 'fs'
|
||||
path = require 'path'
|
||||
{remote, shell} = require 'electron'
|
||||
mkdirp = require 'mkdirp'
|
||||
Utils = require '../models/utils'
|
||||
Reflux = require 'reflux'
|
||||
_ = require 'underscore'
|
||||
Actions = require('../actions').default
|
||||
progress = require 'request-progress'
|
||||
NylasAPI = require '../nylas-api'
|
||||
RegExpUtils = require '../../regexp-utils'
|
||||
|
||||
Promise.promisifyAll(fs)
|
||||
mkdirpAsync = Promise.promisify(mkdirp)
|
||||
|
||||
State =
|
||||
Unstarted: 'unstarted'
|
||||
Downloading: 'downloading'
|
||||
Finished: 'finished'
|
||||
Failed: 'failed'
|
||||
|
||||
class Download
|
||||
@State: State
|
||||
|
||||
constructor: ({@accountId, @fileId, @targetPath, @filename, @filesize, @progressCallback}) ->
|
||||
if not @accountId
|
||||
throw new Error("Download.constructor: You must provide a non-empty accountId.")
|
||||
if not @filename or @filename.length is 0
|
||||
throw new Error("Download.constructor: You must provide a non-empty filename.")
|
||||
if not @fileId
|
||||
throw new Error("Download.constructor: You must provide a fileID to download.")
|
||||
if not @targetPath
|
||||
throw new Error("Download.constructor: You must provide a target path to download.")
|
||||
|
||||
@percent = 0
|
||||
@promise = null
|
||||
@state = State.Unstarted
|
||||
@
|
||||
|
||||
# We need to pass a plain object so we can have fresh references for the
|
||||
# React views while maintaining the single object with the running
|
||||
# request.
|
||||
data: -> Object.freeze _.clone
|
||||
state: @state
|
||||
fileId: @fileId
|
||||
percent: @percent
|
||||
filename: @filename
|
||||
filesize: @filesize
|
||||
targetPath: @targetPath
|
||||
|
||||
run: ->
|
||||
# If run has already been called, return the existing promise. Never
|
||||
# initiate multiple downloads for the same file
|
||||
return @promise if @promise
|
||||
|
||||
# Note: we must resolve or reject with `this`
|
||||
@promise = new Promise (resolve, reject) =>
|
||||
stream = fs.createWriteStream(@targetPath)
|
||||
@state = State.Downloading
|
||||
|
||||
onFailed = (err) =>
|
||||
@request = null
|
||||
stream.end()
|
||||
@state = State.Failed
|
||||
if fs.existsSync(@targetPath)
|
||||
fs.unlinkSync(@targetPath)
|
||||
reject(err)
|
||||
|
||||
onSuccess = =>
|
||||
@request = null
|
||||
stream.end()
|
||||
@state = State.Finished
|
||||
@percent = 100
|
||||
resolve(@)
|
||||
|
||||
NylasAPI.makeRequest
|
||||
json: false
|
||||
path: "/files/#{@fileId}/download"
|
||||
accountId: @accountId
|
||||
encoding: null # Tell `request` not to parse the response data
|
||||
started: (req) =>
|
||||
@request = req
|
||||
progress(@request, {throtte: 250})
|
||||
.on "progress", (progress) =>
|
||||
@percent = progress.percent
|
||||
@progressCallback()
|
||||
|
||||
# This is a /socket/ error event, not an HTTP error event. It fires
|
||||
# when the conn is dropped, user if offline, but not on HTTP status codes.
|
||||
# It is sometimes called in place of "end", not before or after.
|
||||
.on("error", onFailed)
|
||||
|
||||
.on "end", =>
|
||||
return if @state is State.Failed
|
||||
|
||||
statusCode = @request.response?.statusCode
|
||||
if [200, 202, 204].includes(statusCode)
|
||||
onSuccess()
|
||||
else
|
||||
onFailed(new Error("Server returned a #{statusCode}"))
|
||||
|
||||
.pipe(stream)
|
||||
|
||||
ensureClosed: ->
|
||||
@request?.abort()
|
||||
|
||||
|
||||
module.exports =
|
||||
FileDownloadStore = Reflux.createStore
|
||||
init: ->
|
||||
@listenTo Actions.fetchFile, @_fetch
|
||||
@listenTo Actions.fetchAndOpenFile, @_fetchAndOpen
|
||||
@listenTo Actions.fetchAndSaveFile, @_fetchAndSave
|
||||
@listenTo Actions.fetchAndSaveAllFiles, @_fetchAndSaveAll
|
||||
@listenTo Actions.abortFetchFile, @_abortFetchFile
|
||||
@listenTo Actions.didPassivelyReceiveNewModels, @_newMailReceived
|
||||
|
||||
@_downloads = {}
|
||||
@_downloadDirectory = path.join(NylasEnv.getConfigDirPath(), 'downloads')
|
||||
mkdirp(@_downloadDirectory)
|
||||
|
||||
######### PUBLIC #######################################################
|
||||
|
||||
# Returns a path on disk for saving the file. Note that we must account
|
||||
# for files that don't have a name and avoid returning <downloads/dir/"">
|
||||
# which causes operations to happen on the directory (badness!)
|
||||
#
|
||||
pathForFile: (file) ->
|
||||
return undefined unless file
|
||||
path.join(@_downloadDirectory, file.id, file.safeDisplayName())
|
||||
|
||||
downloadDataForFile: (fileId) ->
|
||||
@_downloads[fileId]?.data()
|
||||
|
||||
# Returns a hash of download objects keyed by fileId
|
||||
#
|
||||
downloadDataForFiles: (fileIds=[]) ->
|
||||
downloadData = {}
|
||||
fileIds.forEach (fileId) =>
|
||||
downloadData[fileId] = @downloadDataForFile(fileId)
|
||||
return downloadData
|
||||
|
||||
########### PRIVATE ####################################################
|
||||
|
||||
_newMailReceived: (incoming) ->
|
||||
if NylasEnv.config.get('core.attachments.downloadPolicy') is 'on-receive'
|
||||
return unless incoming['message']
|
||||
for message in incoming['message']
|
||||
for file in message.files
|
||||
@_fetch(file)
|
||||
|
||||
# Returns a promise with a Download object, allowing other actions to be
|
||||
# daisy-chained to the end of the download operation.
|
||||
_runDownload: (file) ->
|
||||
targetPath = @pathForFile(file)
|
||||
|
||||
# is there an existing download for this file? If so,
|
||||
# return that promise so users can chain to the end of it.
|
||||
download = @_downloads[file.id]
|
||||
return download.run() if download
|
||||
|
||||
# create a new download for this file
|
||||
download = new Download
|
||||
accountId: file.accountId
|
||||
fileId: file.id
|
||||
filesize: file.size
|
||||
filename: file.displayName()
|
||||
targetPath: targetPath
|
||||
progressCallback: => @trigger()
|
||||
|
||||
# Do we actually need to queue and run the download? Queuing a download
|
||||
# for an already-downloaded file has side-effects, like making the UI
|
||||
# flicker briefly.
|
||||
@_prepareFolder(file).then =>
|
||||
@_checkForDownloadedFile(file).then (alreadyHaveFile) =>
|
||||
if alreadyHaveFile
|
||||
# If we have the file, just resolve with a resolved download representing the file.
|
||||
download.promise = Promise.resolve()
|
||||
download.state = State.Finished
|
||||
return Promise.resolve(download)
|
||||
else
|
||||
@_downloads[file.id] = download
|
||||
@trigger()
|
||||
return download.run().finally =>
|
||||
download.ensureClosed()
|
||||
if download.state is State.Failed
|
||||
delete @_downloads[file.id]
|
||||
@trigger()
|
||||
|
||||
# Returns a promise that resolves with true or false. True if the file has
|
||||
# been downloaded, false if it should be downloaded.
|
||||
#
|
||||
_checkForDownloadedFile: (file) ->
|
||||
fs.statAsync(@pathForFile(file)).catch (err) =>
|
||||
return Promise.resolve(false)
|
||||
.then (stats) =>
|
||||
return Promise.resolve(stats.size >= file.size)
|
||||
|
||||
# Checks that the folder for the download is ready. Returns a promise that
|
||||
# resolves when the download directory for the file has been created.
|
||||
#
|
||||
_prepareFolder: (file) ->
|
||||
targetFolder = path.join(@_downloadDirectory, file.id)
|
||||
fs.statAsync(targetFolder).catch =>
|
||||
mkdirpAsync(targetFolder)
|
||||
|
||||
_fetch: (file) ->
|
||||
@_runDownload(file)
|
||||
.catch(@_catchFSErrors)
|
||||
.catch (error) ->
|
||||
# Passively ignore
|
||||
|
||||
_fetchAndOpen: (file) ->
|
||||
@_runDownload(file).then (download) ->
|
||||
shell.openItem(download.targetPath)
|
||||
.catch(@_catchFSErrors)
|
||||
.catch (error) =>
|
||||
@_presentError({file, error})
|
||||
|
||||
_saveDownload: (download, savePath) =>
|
||||
return new Promise (resolve, reject) =>
|
||||
stream = fs.createReadStream(download.targetPath)
|
||||
stream.pipe(fs.createWriteStream(savePath))
|
||||
stream.on 'error', (err) -> reject(err)
|
||||
stream.on 'end', -> resolve()
|
||||
|
||||
_fetchAndSave: (file) ->
|
||||
defaultPath = @_defaultSavePath(file)
|
||||
defaultExtension = path.extname(defaultPath)
|
||||
|
||||
NylasEnv.showSaveDialog {defaultPath}, (savePath) =>
|
||||
return unless savePath
|
||||
|
||||
newDownloadDirectory = path.dirname(savePath)
|
||||
saveExtension = path.extname(savePath)
|
||||
didLoseExtension = defaultExtension isnt '' and saveExtension is ''
|
||||
if didLoseExtension
|
||||
savePath = savePath + defaultExtension
|
||||
|
||||
@_runDownload(file)
|
||||
.then (download) => @_saveDownload(download, savePath)
|
||||
.then =>
|
||||
if NylasEnv.savedState.lastDownloadDirectory isnt newDownloadDirectory
|
||||
shell.showItemInFolder(savePath)
|
||||
NylasEnv.savedState.lastDownloadDirectory = newDownloadDirectory
|
||||
.catch(@_catchFSErrors)
|
||||
.catch (error) =>
|
||||
@_presentError({file, error})
|
||||
|
||||
_fetchAndSaveAll: (files) ->
|
||||
defaultPath = @_defaultSaveDir()
|
||||
options = {
|
||||
defaultPath,
|
||||
title: 'Save Into...',
|
||||
buttonLabel: 'Download All',
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
}
|
||||
|
||||
return new Promise (resolve, reject) =>
|
||||
NylasEnv.showOpenDialog options, (selected) =>
|
||||
return unless selected
|
||||
dirPath = selected[0]
|
||||
return unless dirPath
|
||||
NylasEnv.savedState.lastDownloadDirectory = dirPath
|
||||
|
||||
lastSavePaths = []
|
||||
savePromises = files.map (file) =>
|
||||
savePath = path.join(dirPath, file.safeDisplayName())
|
||||
@_runDownload(file)
|
||||
.then (download) => @_saveDownload(download, savePath)
|
||||
.then ->
|
||||
lastSavePaths.push(savePath)
|
||||
|
||||
Promise.all(savePromises)
|
||||
.then =>
|
||||
shell.showItemInFolder(lastSavePaths[0]) if lastSavePaths.length > 0
|
||||
resolve(lastSavePaths)
|
||||
.catch(@_catchFSErrors)
|
||||
.catch =>
|
||||
@_presentError(file)
|
||||
|
||||
_abortFetchFile: (file) ->
|
||||
download = @_downloads[file.id]
|
||||
return unless download
|
||||
download.ensureClosed()
|
||||
@trigger()
|
||||
|
||||
downloadPath = @pathForFile(file)
|
||||
fs.exists downloadPath, (exists) ->
|
||||
fs.unlink(downloadPath) if exists
|
||||
|
||||
_defaultSaveDir: ->
|
||||
if process.platform is 'win32'
|
||||
home = process.env.USERPROFILE
|
||||
else
|
||||
home = process.env.HOME
|
||||
|
||||
downloadDir = path.join(home, 'Downloads')
|
||||
if not fs.existsSync(downloadDir)
|
||||
downloadDir = os.tmpdir()
|
||||
|
||||
if NylasEnv.savedState.lastDownloadDirectory
|
||||
if fs.existsSync(NylasEnv.savedState.lastDownloadDirectory)
|
||||
downloadDir = NylasEnv.savedState.lastDownloadDirectory
|
||||
|
||||
return downloadDir
|
||||
|
||||
_defaultSavePath: (file) ->
|
||||
downloadDir = @_defaultSaveDir()
|
||||
path.join(downloadDir, file.safeDisplayName())
|
||||
|
||||
_presentError: ({file, error} = {}) ->
|
||||
name = if file then file.displayName() else "one or more files"
|
||||
errorString = if error then error.toString() else ""
|
||||
|
||||
remote.dialog.showMessageBox
|
||||
type: 'warning'
|
||||
message: "Download Failed"
|
||||
detail: "Unable to download #{name}. Check your network connection and try again. #{errorString}"
|
||||
buttons: ["OK"]
|
||||
|
||||
_catchFSErrors: (error) ->
|
||||
message = null
|
||||
if error.code in ['EPERM', 'EMFILE', 'EACCES']
|
||||
message = "N1 could not save an attachment. Check that permissions are set correctly and try restarting N1 if the issue persists."
|
||||
if error.code in ['ENOSPC']
|
||||
message = "N1 could not save an attachment because you have run out of disk space."
|
||||
|
||||
if message
|
||||
remote.dialog.showMessageBox
|
||||
type: 'warning'
|
||||
message: "Download Failed"
|
||||
detail: "#{message}\n\n#{error.message}"
|
||||
buttons: ["OK"]
|
||||
return Promise.resolve()
|
||||
else
|
||||
return Promise.reject(error)
|
||||
|
||||
# Expose the Download class for our tests, and possibly for other things someday
|
||||
FileDownloadStore.Download = Download
|
435
src/flux/stores/file-download-store.es6
Normal file
435
src/flux/stores/file-download-store.es6
Normal file
|
@ -0,0 +1,435 @@
|
|||
import _ from 'underscore';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { remote, shell } from 'electron';
|
||||
import mkdirp from 'mkdirp';
|
||||
import progress from 'request-progress';
|
||||
import NylasStore from 'nylas-store';
|
||||
import Actions from '../actions';
|
||||
import NylasAPI from '../nylas-api';
|
||||
|
||||
Promise.promisifyAll(fs);
|
||||
const mkdirpAsync = Promise.promisify(mkdirp);
|
||||
|
||||
const State = {
|
||||
Unstarted: 'unstarted',
|
||||
Downloading: 'downloading',
|
||||
Finished: 'finished',
|
||||
Failed: 'failed',
|
||||
};
|
||||
|
||||
export class Download {
|
||||
static State = State
|
||||
|
||||
constructor({accountId, fileId, targetPath, filename, filesize, progressCallback}) {
|
||||
this.accountId = accountId;
|
||||
this.fileId = fileId;
|
||||
this.targetPath = targetPath;
|
||||
this.filename = filename;
|
||||
this.filesize = filesize;
|
||||
this.progressCallback = progressCallback;
|
||||
if (!this.accountId) {
|
||||
throw new Error("Download.constructor: You must provide a non-empty accountId.");
|
||||
}
|
||||
if (!this.filename || this.filename.length === 0) {
|
||||
throw new Error("Download.constructor: You must provide a non-empty filename.");
|
||||
}
|
||||
if (!this.fileId) {
|
||||
throw new Error("Download.constructor: You must provide a fileID to download.");
|
||||
}
|
||||
if (!this.targetPath) {
|
||||
throw new Error("Download.constructor: You must provide a target path to download.");
|
||||
}
|
||||
|
||||
this.percent = 0;
|
||||
this.promise = null;
|
||||
this.state = State.Unstarted;
|
||||
}
|
||||
|
||||
// We need to pass a plain object so we can have fresh references for the
|
||||
// React views while maintaining the single object with the running
|
||||
// request.
|
||||
data() {
|
||||
return Object.freeze(_.clone({
|
||||
state: this.state,
|
||||
fileId: this.fileId,
|
||||
percent: this.percent,
|
||||
filename: this.filename,
|
||||
filesize: this.filesize,
|
||||
targetPath: this.targetPath,
|
||||
}));
|
||||
}
|
||||
|
||||
run() {
|
||||
// If run has already been called, return the existing promise. Never
|
||||
// initiate multiple downloads for the same file
|
||||
if (this.promise) { return this.promise; }
|
||||
|
||||
// Note: we must resolve or reject with `this`
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
const stream = fs.createWriteStream(this.targetPath);
|
||||
this.state = State.Downloading;
|
||||
|
||||
const onFailed = (err) => {
|
||||
this.request = null;
|
||||
stream.end();
|
||||
this.state = State.Failed;
|
||||
if (fs.existsSync(this.targetPath)) {
|
||||
fs.unlinkSync(this.targetPath);
|
||||
}
|
||||
reject(err);
|
||||
};
|
||||
|
||||
const onSuccess = () => {
|
||||
this.request = null;
|
||||
stream.end();
|
||||
this.state = State.Finished;
|
||||
this.percent = 100;
|
||||
resolve(this);
|
||||
};
|
||||
|
||||
NylasAPI.makeRequest({
|
||||
json: false,
|
||||
path: `/files/${this.fileId}/download`,
|
||||
accountId: this.accountId,
|
||||
encoding: null, // Tell `request` not to parse the response data
|
||||
started: (req) => {
|
||||
this.request = req;
|
||||
return progress(this.request, {throtte: 250})
|
||||
.on('progress', (prog) => {
|
||||
this.percent = prog.percent;
|
||||
this.progressCallback();
|
||||
})
|
||||
|
||||
// This is a /socket/ error event, not an HTTP error event. It fires
|
||||
// when the conn is dropped, user if offline, but not on HTTP status codes.
|
||||
// It is sometimes called in place of "end", not before or after.
|
||||
.on('error', onFailed)
|
||||
|
||||
.on('end', () => {
|
||||
if (this.state === State.Failed) { return; }
|
||||
|
||||
const {response} = this.request
|
||||
const statusCode = response ? response.statusCode : null;
|
||||
if ([200, 202, 204].includes(statusCode)) {
|
||||
onSuccess();
|
||||
} else {
|
||||
onFailed(new Error(`Server returned a ${statusCode}`));
|
||||
}
|
||||
})
|
||||
|
||||
.pipe(stream);
|
||||
},
|
||||
});
|
||||
});
|
||||
return this.promise
|
||||
}
|
||||
|
||||
ensureClosed() {
|
||||
if (this.request) {
|
||||
this.request.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FileDownloadStore extends NylasStore {
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.listenTo(Actions.fetchFile, this._fetch);
|
||||
this.listenTo(Actions.fetchAndOpenFile, this._fetchAndOpen);
|
||||
this.listenTo(Actions.fetchAndSaveFile, this._fetchAndSave);
|
||||
this.listenTo(Actions.fetchAndSaveAllFiles, this._fetchAndSaveAll);
|
||||
this.listenTo(Actions.abortFetchFile, this._abortFetchFile);
|
||||
this.listenTo(Actions.didPassivelyReceiveNewModels, this._onNewMailReceived);
|
||||
|
||||
this._downloads = {};
|
||||
this._downloadDirectory = path.join(NylasEnv.getConfigDirPath(), 'downloads');
|
||||
mkdirp(this._downloadDirectory);
|
||||
}
|
||||
|
||||
// Expose the Download class for our tests, and possibly for other things someday
|
||||
get Download() {
|
||||
return Download
|
||||
}
|
||||
|
||||
// Returns a path on disk for saving the file. Note that we must account
|
||||
// for files that don't have a name and avoid returning <downloads/dir/"">
|
||||
// which causes operations to happen on the directory (badness!)
|
||||
//
|
||||
pathForFile(file) {
|
||||
if (!file) { return null; }
|
||||
return path.join(this._downloadDirectory, file.id, file.safeDisplayName());
|
||||
}
|
||||
|
||||
downloadDataForFile(fileId) {
|
||||
const download = this._downloads[fileId]
|
||||
if (!download) { return null; }
|
||||
return download.data()
|
||||
}
|
||||
|
||||
// Returns a hash of download objects keyed by fileId
|
||||
//
|
||||
downloadDataForFiles(fileIds = []) {
|
||||
const downloadData = {};
|
||||
fileIds.forEach((fileId) => {
|
||||
downloadData[fileId] = this.downloadDataForFile(fileId);
|
||||
});
|
||||
return downloadData;
|
||||
}
|
||||
|
||||
|
||||
_onNewMailReceived = (incoming) => {
|
||||
if (NylasEnv.config.get('core.attachments.downloadPolicy') !== 'on-receive') {
|
||||
return;
|
||||
}
|
||||
if (!incoming.message) { return; }
|
||||
incoming.message.forEach((message) => {
|
||||
message.files.forEach((file) => this._fetch(file));
|
||||
})
|
||||
}
|
||||
|
||||
// Returns a promise with a Download object, allowing other actions to be
|
||||
// daisy-chained to the end of the download operation.
|
||||
_runDownload(file) {
|
||||
const targetPath = this.pathForFile(file);
|
||||
|
||||
// is there an existing download for this file? If so,
|
||||
// return that promise so users can chain to the end of it.
|
||||
let download = this._downloads[file.id];
|
||||
if (download) { return download.run(); }
|
||||
|
||||
// create a new download for this file
|
||||
download = new Download({
|
||||
accountId: file.accountId,
|
||||
fileId: file.id,
|
||||
filesize: file.size,
|
||||
filename: file.displayName(),
|
||||
targetPath,
|
||||
progressCallback: () => this.trigger(),
|
||||
});
|
||||
|
||||
// Do we actually need to queue and run the download? Queuing a download
|
||||
// for an already-downloaded file has side-effects, like making the UI
|
||||
// flicker briefly.
|
||||
return this._prepareFolder(file).then(() => {
|
||||
return this._checkForDownloadedFile(file).then((alreadyHaveFile) => {
|
||||
if (alreadyHaveFile) {
|
||||
// If we have the file, just resolve with a resolved download representing the file.
|
||||
download.promise = Promise.resolve();
|
||||
download.state = State.Finished;
|
||||
return Promise.resolve(download);
|
||||
}
|
||||
this._downloads[file.id] = download;
|
||||
this.trigger();
|
||||
return download.run().finally(() => {
|
||||
download.ensureClosed();
|
||||
if (download.state === State.Failed) {
|
||||
delete this._downloads[file.id];
|
||||
}
|
||||
this.trigger();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_fetchPreview(download) {
|
||||
|
||||
}
|
||||
|
||||
// Returns a promise that resolves with true or false. True if the file has
|
||||
// been downloaded, false if it should be downloaded.
|
||||
//
|
||||
_checkForDownloadedFile(file) {
|
||||
return fs.statAsync(this.pathForFile(file))
|
||||
.then((stats) => {
|
||||
return Promise.resolve(stats.size >= file.size);
|
||||
})
|
||||
.catch(() => {
|
||||
return Promise.resolve(false);
|
||||
})
|
||||
}
|
||||
|
||||
// Checks that the folder for the download is ready. Returns a promise that
|
||||
// resolves when the download directory for the file has been created.
|
||||
//
|
||||
_prepareFolder(file) {
|
||||
const targetFolder = path.join(this._downloadDirectory, file.id);
|
||||
return fs.statAsync(targetFolder)
|
||||
.catch(() => {
|
||||
return mkdirpAsync(targetFolder);
|
||||
});
|
||||
}
|
||||
|
||||
_fetch = (file) => {
|
||||
return this._runDownload(file)
|
||||
.catch(this._catchFSErrors)
|
||||
// Passively ignore
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
_fetchAndOpen = (file) => {
|
||||
return this._runDownload(file)
|
||||
.then((download) => shell.openItem(download.targetPath))
|
||||
.catch(this._catchFSErrors)
|
||||
.catch((error) => {
|
||||
return this._presentError({file, error});
|
||||
});
|
||||
}
|
||||
|
||||
_saveDownload = (download, savePath) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stream = fs.createReadStream(download.targetPath);
|
||||
stream.pipe(fs.createWriteStream(savePath));
|
||||
stream.on('error', err => reject(err));
|
||||
stream.on('end', () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
_fetchAndSave = (file) => {
|
||||
const defaultPath = this._defaultSavePath(file);
|
||||
const defaultExtension = path.extname(defaultPath);
|
||||
|
||||
NylasEnv.showSaveDialog({defaultPath}, (savePath) => {
|
||||
if (!savePath) { return; }
|
||||
|
||||
const saveExtension = path.extname(savePath);
|
||||
const newDownloadDirectory = path.dirname(savePath);
|
||||
const didLoseExtension = defaultExtension !== '' && saveExtension === '';
|
||||
let actualSavePath = savePath
|
||||
if (didLoseExtension) {
|
||||
actualSavePath += defaultExtension;
|
||||
}
|
||||
|
||||
this._runDownload(file)
|
||||
.then((download) => this._saveDownload(download, actualSavePath))
|
||||
.then(() => {
|
||||
if (NylasEnv.savedState.lastDownloadDirectory !== newDownloadDirectory) {
|
||||
shell.showItemInFolder(actualSavePath);
|
||||
NylasEnv.savedState.lastDownloadDirectory = newDownloadDirectory;
|
||||
}
|
||||
})
|
||||
.catch(this._catchFSErrors)
|
||||
.catch(error => {
|
||||
this._presentError({file, error});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_fetchAndSaveAll = (files) => {
|
||||
const defaultPath = this._defaultSaveDir();
|
||||
const options = {
|
||||
defaultPath,
|
||||
title: 'Save Into...',
|
||||
buttonLabel: 'Download All',
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
NylasEnv.showOpenDialog(options, (selected) => {
|
||||
if (!selected) { return; }
|
||||
const dirPath = selected[0];
|
||||
if (!dirPath) { return; }
|
||||
NylasEnv.savedState.lastDownloadDirectory = dirPath;
|
||||
|
||||
const lastSavePaths = [];
|
||||
const savePromises = files.map((file) => {
|
||||
const savePath = path.join(dirPath, file.safeDisplayName());
|
||||
return this._runDownload(file)
|
||||
.then((download) => this._saveDownload(download, savePath))
|
||||
.then(() => lastSavePaths.push(savePath));
|
||||
});
|
||||
|
||||
Promise.all(savePromises)
|
||||
.then(() => {
|
||||
if (lastSavePaths.length > 0) {
|
||||
shell.showItemInFolder(lastSavePaths[0]);
|
||||
}
|
||||
return resolve(lastSavePaths);
|
||||
})
|
||||
.catch(this._catchFSErrors)
|
||||
.catch((error) => {
|
||||
return this._presentError({error});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_abortFetchFile = (file) => {
|
||||
const download = this._downloads[file.id];
|
||||
if (!download) { return; }
|
||||
download.ensureClosed();
|
||||
this.trigger();
|
||||
|
||||
const downloadPath = this.pathForFile(file);
|
||||
fs.exists(downloadPath, (exists) => {
|
||||
if (exists) {
|
||||
fs.unlink(downloadPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_defaultSaveDir() {
|
||||
let home = ''
|
||||
if (process.platform === 'win32') {
|
||||
home = process.env.USERPROFILE;
|
||||
} else {
|
||||
home = process.env.HOME;
|
||||
}
|
||||
|
||||
let downloadDir = path.join(home, 'Downloads');
|
||||
if (!fs.existsSync(downloadDir)) {
|
||||
downloadDir = os.tmpdir();
|
||||
}
|
||||
|
||||
if (NylasEnv.savedState.lastDownloadDirectory) {
|
||||
if (fs.existsSync(NylasEnv.savedState.lastDownloadDirectory)) {
|
||||
downloadDir = NylasEnv.savedState.lastDownloadDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
return downloadDir;
|
||||
}
|
||||
|
||||
_defaultSavePath(file) {
|
||||
const downloadDir = this._defaultSaveDir();
|
||||
return path.join(downloadDir, file.safeDisplayName());
|
||||
}
|
||||
|
||||
_presentError({file, error} = {}) {
|
||||
const name = file ? file.displayName() : "one or more files";
|
||||
const errorString = error ? error.toString() : "";
|
||||
|
||||
return remote.dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
message: "Download Failed",
|
||||
detail: `Unable to download ${name}. Check your network connection and try again. ${errorString}`,
|
||||
buttons: ["OK"],
|
||||
});
|
||||
}
|
||||
|
||||
_catchFSErrors(error) {
|
||||
let message = null;
|
||||
if (['EPERM', 'EMFILE', 'EACCES'].includes(error.code)) {
|
||||
message = "N1 could not save an attachment. Check that permissions are set correctly and try restarting N1 if the issue persists.";
|
||||
}
|
||||
if (['ENOSPC'].includes(error.code)) {
|
||||
message = "N1 could not save an attachment because you have run out of disk space.";
|
||||
}
|
||||
|
||||
if (message) {
|
||||
remote.dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
message: "Download Failed",
|
||||
detail: `${message}\n\n${error.message}`,
|
||||
buttons: ["OK"],
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
export default new FileDownloadStore()
|
Loading…
Reference in a new issue