UNPKG

electron-dl-manager

Version:

A library for implementing file downloads in Electron with 'save as' dialog and id support.

275 lines 11.6 kB
"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