electron-dl-manager
Version:
A library for implementing file downloads in Electron with 'save as' dialog and id support.
275 lines • 11.6 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DownloadInitiator = void 0;
const path = __importStar(require("node:path"));
const CallbackDispatcher_1 = require("./CallbackDispatcher");
const DownloadData_1 = require("./DownloadData");
const utils_1 = require("./utils");
class DownloadInitiator {
logger;
/**
* The handler for the DownloadItem's `updated` event.
*/
onItemUpdated;
/**
* The handler for the DownloadItem's `done` event.
*/
onItemDone;
/**
* When the download is initiated
*/
onDownloadInit;
/**
* When cleanup is called
*/
onCleanup;
/**
* The callback dispatcher for handling download events.
*/
callbackDispatcher;
/**
* The data for the download.
*/
downloadData;
config;
onUpdateHandler;
constructor(config) {
this.downloadData = new DownloadData_1.DownloadData();
this.logger = config.debugLogger || (() => { });
this.onItemUpdated = () => Promise.resolve();
this.onItemDone = () => Promise.resolve();
this.onCleanup = config.onCleanup || (() => { });
this.onDownloadInit = config.onDownloadInit || (() => { });
this.config = {};
this.callbackDispatcher = {};
}
log(message) {
this.logger(`[${this.downloadData.id}] ${message}`);
}
/**
* Returns the download id
*/
getDownloadId() {
return this.downloadData.id;
}
/**
* Returns the current download data
*/
getDownloadData() {
return this.downloadData;
}
/**
* Generates the handler that attaches to the session `will-download` event,
* which will execute the workflows for handling a download.
*/
generateOnWillDownload(downloadParams) {
this.config = downloadParams;
this.callbackDispatcher = new CallbackDispatcher_1.CallbackDispatcher(this.downloadData.id, downloadParams.callbacks, this.logger);
return async (event, item, webContents) => {
item.pause();
this.downloadData.item = item;
this.downloadData.webContents = webContents;
this.downloadData.event = event;
if (this.onDownloadInit) {
this.onDownloadInit(this.downloadData);
}
if (this.config.saveDialogOptions) {
this.initSaveAsInteractiveDownload();
return;
}
await this.initNonInteractiveDownload();
};
}
/**
* Flow for handling a download that requires user interaction via a "Save as" dialog.
*/
initSaveAsInteractiveDownload() {
this.log("Prompting save as dialog");
const { directory, overwrite, saveDialogOptions } = this.config;
const { item } = this.downloadData;
const filePath = (0, utils_1.determineFilePath)({ directory, item, overwrite });
// This actually isn't what shows the save dialog
// If item.setSavePath() isn't called at all after some tiny period of time,
// then the save dialog will show up, and it will use the options we set it to here
item.setSaveDialogOptions({ ...saveDialogOptions, defaultPath: filePath });
// Because the download happens concurrently as the user is choosing a save location
// we need to wait for the save location to be chosen before we can start to fire out events
// there's no good way to listen for this, so we need to poll
const interval = setInterval(async () => {
// It seems to unpause sometimes in the dialog situation ???
// item.getState() value becomes 'completed' for small files
// before item.resume() is called
item.pause();
if (item.getSavePath()) {
clearInterval(interval);
this.log(`User selected save path to ${item.getSavePath()}`);
this.log("Initiating download item handlers");
this.downloadData.resolvedFilename = path.basename(item.getSavePath());
this.augmentDownloadItem(item);
await this.callbackDispatcher.onDownloadStarted(this.downloadData);
// If for some reason the above pause didn't work...
// We'll manually call the completed handler
if (this.downloadData.isDownloadCompleted()) {
await this.callbackDispatcher.onDownloadCompleted(this.downloadData);
}
else {
this.onUpdateHandler = this.generateItemOnUpdated();
item.on("updated", this.onUpdateHandler);
item.once("done", this.generateItemOnDone());
}
if (!item["_userInitiatedPause"]) {
item.resume();
}
}
else if (this.downloadData.isDownloadCancelled()) {
clearInterval(interval);
this.log("Download was cancelled");
this.downloadData.cancelledFromSaveAsDialog = true;
await this.callbackDispatcher.onDownloadCancelled(this.downloadData);
}
else {
this.log("Waiting for save path to be chosen by user");
}
}, 1000);
}
augmentDownloadItem(item) {
// This covers if the user manually pauses the download
// before we have set up the event listeners on the item
item["_userInitiatedPause"] = false;
const oldPause = item.pause.bind(item);
item.pause = () => {
item["_userInitiatedPause"] = true;
if (this.onUpdateHandler) {
// Don't fire progress updates in a paused state
item.off("updated", this.onUpdateHandler);
this.onUpdateHandler = undefined;
}
oldPause();
};
const oldResume = item.resume.bind(item);
item.resume = () => {
if (!this.onUpdateHandler) {
this.onUpdateHandler = this.generateItemOnUpdated();
item.on("updated", this.onUpdateHandler);
}
oldResume();
};
}
/**
* Flow for handling a download that doesn't require user interaction.
*/
async initNonInteractiveDownload() {
const { directory, saveAsFilename, overwrite } = this.config;
const { item } = this.downloadData;
const filePath = (0, utils_1.determineFilePath)({ directory, saveAsFilename, item, overwrite });
this.log(`Setting save path to ${filePath}`);
item.setSavePath(filePath);
this.log("Initiating download item handlers");
this.downloadData.resolvedFilename = path.basename(filePath);
this.augmentDownloadItem(item);
await this.callbackDispatcher.onDownloadStarted(this.downloadData);
this.onUpdateHandler = this.generateItemOnUpdated();
item.on("updated", this.onUpdateHandler);
item.once("done", this.generateItemOnDone());
if (!item["_userInitiatedPause"]) {
item.resume();
}
}
updateProgress() {
const { item } = this.downloadData;
const metrics = (0, utils_1.calculateDownloadMetrics)(item);
const downloadedBytes = item.getReceivedBytes();
const totalBytes = item.getTotalBytes();
if (downloadedBytes > item.getTotalBytes()) {
// Note: This situation will happen when using data: URIs
this.log(`Downloaded bytes (${downloadedBytes}) is greater than total bytes (${totalBytes})`);
}
this.downloadData.downloadRateBytesPerSecond = metrics.downloadRateBytesPerSecond;
this.downloadData.estimatedTimeRemainingSeconds = metrics.estimatedTimeRemainingSeconds;
this.downloadData.percentCompleted = metrics.percentCompleted;
}
/**
* Generates the handler for hooking into the DownloadItem's `updated` event.
*/
generateItemOnUpdated() {
return async (_event, state) => {
switch (state) {
case "progressing": {
this.updateProgress();
await this.callbackDispatcher.onDownloadProgress(this.downloadData);
break;
}
case "interrupted": {
this.downloadData.interruptedVia = "in-progress";
await this.callbackDispatcher.onDownloadInterrupted(this.downloadData);
break;
}
default:
this.log(`Unexpected itemOnUpdated state: ${state}`);
}
};
}
/**
* Generates the handler for hooking into the DownloadItem's `done` event.
*/
generateItemOnDone() {
return async (_event, state) => {
switch (state) {
case "completed": {
this.log(`Download completed. Total bytes: ${this.downloadData.item.getTotalBytes()}`);
await this.callbackDispatcher.onDownloadCompleted(this.downloadData);
break;
}
case "cancelled":
this.log(`Download cancelled. Total bytes: ${this.downloadData.item.getReceivedBytes()} / ${this.downloadData.item.getTotalBytes()}`);
await this.callbackDispatcher.onDownloadCancelled(this.downloadData);
break;
case "interrupted":
this.log(`Download interrupted. Total bytes: ${this.downloadData.item.getReceivedBytes()} / ${this.downloadData.item.getTotalBytes()}`);
this.downloadData.interruptedVia = "completed";
await this.callbackDispatcher.onDownloadInterrupted(this.downloadData);
break;
default:
this.log(`Unexpected itemOnDone state: ${state}`);
}
this.cleanup();
};
}
cleanup() {
const { item } = this.downloadData;
if (item) {
this.log("Cleaning up download item event listeners");
item.removeListener("updated", this.onItemUpdated);
item.removeListener("done", this.onItemDone);
}
if (this.onCleanup) {
this.onCleanup(this.downloadData);
}
this.onUpdateHandler = undefined;
}
}
exports.DownloadInitiator = DownloadInitiator;
//# sourceMappingURL=DownloadInitiator.js.map