
* Rename MattermostTeam -> UniqueServer, MattermostTab -> UniqueView * Rename 'team' to 'server' * Some further cleanup * Rename weirdly named function * Rename 'tab' to 'view' in most instances * Fix i18n * PR feedback
719 lines
25 KiB
TypeScript
719 lines
25 KiB
TypeScript
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
|
|
import {DownloadItem, Event, WebContents, FileFilter, ipcMain, dialog, shell, Menu, app, IpcMainInvokeEvent} from 'electron';
|
|
import {ProgressInfo, UpdateInfo} from 'electron-updater';
|
|
import {DownloadedItem, DownloadItemDoneEventState, DownloadedItems, DownloadItemState, DownloadItemUpdatedEventState} from 'types/downloads';
|
|
|
|
import {
|
|
CANCEL_UPDATE_DOWNLOAD,
|
|
CLOSE_DOWNLOADS_DROPDOWN,
|
|
CLOSE_DOWNLOADS_DROPDOWN_MENU,
|
|
DOWNLOADS_DROPDOWN_FOCUSED,
|
|
GET_DOWNLOAD_LOCATION,
|
|
HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE,
|
|
NO_UPDATE_AVAILABLE,
|
|
OPEN_DOWNLOADS_DROPDOWN,
|
|
REQUEST_HAS_DOWNLOADS,
|
|
SHOW_DOWNLOADS_DROPDOWN_BUTTON_BADGE,
|
|
UPDATE_AVAILABLE,
|
|
UPDATE_DOWNLOADED,
|
|
UPDATE_DOWNLOADS_DROPDOWN,
|
|
UPDATE_PATHS,
|
|
UPDATE_PROGRESS,
|
|
} from 'common/communication';
|
|
import Config from 'common/config';
|
|
import JsonFileManager from 'common/JsonFileManager';
|
|
import {Logger} from 'common/log';
|
|
import {APP_UPDATE_KEY, UPDATE_DOWNLOAD_ITEM} from 'common/constants';
|
|
import {DOWNLOADS_DROPDOWN_AUTOCLOSE_TIMEOUT, DOWNLOADS_DROPDOWN_MAX_ITEMS} from 'common/utils/constants';
|
|
import * as Validator from 'common/Validator';
|
|
import {localizeMessage} from 'main/i18nManager';
|
|
import {displayDownloadCompleted} from 'main/notifications';
|
|
import ViewManager from 'main/views/viewManager';
|
|
import MainWindow from 'main/windows/mainWindow';
|
|
import {doubleSecToMs, getPercentage, isStringWithLength, readFilenameFromContentDispositionHeader, shouldIncrementFilename} from 'main/utils';
|
|
|
|
import appVersionManager from './AppVersionManager';
|
|
import {downloadsJson} from './constants';
|
|
|
|
const log = new Logger('DownloadsManager');
|
|
|
|
export enum DownloadItemTypeEnum {
|
|
FILE = 'file',
|
|
UPDATE = 'update',
|
|
}
|
|
|
|
export class DownloadsManager extends JsonFileManager<DownloadedItems> {
|
|
autoCloseTimeout: NodeJS.Timeout | null;
|
|
open: boolean;
|
|
fileSizes: Map<string, string>;
|
|
progressingItems: Map<string, DownloadItem>;
|
|
downloads: DownloadedItems;
|
|
willDownloadURLs: Map<string, {filePath: string; bookmark?: string}>;
|
|
bookmarks: Map<string, {originalPath: string; bookmark: string}>;
|
|
|
|
constructor(file: string) {
|
|
super(file);
|
|
|
|
this.open = false;
|
|
this.fileSizes = new Map();
|
|
this.progressingItems = new Map();
|
|
this.willDownloadURLs = new Map();
|
|
this.bookmarks = new Map();
|
|
this.autoCloseTimeout = null;
|
|
this.downloads = {};
|
|
|
|
this.init();
|
|
}
|
|
|
|
private init = () => {
|
|
// ensure data loaded from file is valid
|
|
const validatedJSON = Validator.validateDownloads(this.json);
|
|
log.debug('init', {'this.json': this.json, validatedJSON});
|
|
if (validatedJSON) {
|
|
this.saveAll(validatedJSON);
|
|
} else {
|
|
this.saveAll({});
|
|
}
|
|
this.checkForDeletedFiles();
|
|
this.reloadFilesForMAS();
|
|
|
|
this.loadIPCHandlers();
|
|
};
|
|
|
|
private loadIPCHandlers = () => {
|
|
ipcMain.removeHandler(REQUEST_HAS_DOWNLOADS);
|
|
ipcMain.handle(REQUEST_HAS_DOWNLOADS, () => {
|
|
return this.hasDownloads();
|
|
});
|
|
|
|
ipcMain.removeHandler(GET_DOWNLOAD_LOCATION);
|
|
ipcMain.removeListener(DOWNLOADS_DROPDOWN_FOCUSED, this.clearAutoCloseTimeout);
|
|
ipcMain.removeListener(UPDATE_AVAILABLE, this.onUpdateAvailable);
|
|
ipcMain.removeListener(UPDATE_DOWNLOADED, this.onUpdateDownloaded);
|
|
ipcMain.removeListener(UPDATE_PROGRESS, this.onUpdateProgress);
|
|
ipcMain.removeListener(NO_UPDATE_AVAILABLE, this.noUpdateAvailable);
|
|
|
|
ipcMain.handle(GET_DOWNLOAD_LOCATION, this.handleSelectDownload);
|
|
ipcMain.on(DOWNLOADS_DROPDOWN_FOCUSED, this.clearAutoCloseTimeout);
|
|
ipcMain.on(UPDATE_AVAILABLE, this.onUpdateAvailable);
|
|
ipcMain.on(UPDATE_DOWNLOADED, this.onUpdateDownloaded);
|
|
ipcMain.on(UPDATE_PROGRESS, this.onUpdateProgress);
|
|
ipcMain.on(NO_UPDATE_AVAILABLE, this.noUpdateAvailable);
|
|
};
|
|
|
|
handleNewDownload = async (event: Event, item: DownloadItem, webContents: WebContents) => {
|
|
log.debug('handleNewDownload', {item, sourceURL: webContents.getURL()});
|
|
|
|
const url = item.getURL();
|
|
|
|
if (this.willDownloadURLs.has(url)) {
|
|
const info = this.willDownloadURLs.get(url)!;
|
|
this.willDownloadURLs.delete(url);
|
|
|
|
if (info.bookmark) {
|
|
item.setSavePath(path.resolve(app.getPath('temp'), path.basename(info.filePath)));
|
|
this.bookmarks.set(this.getFileId(item), {originalPath: info.filePath, bookmark: info.bookmark!});
|
|
} else {
|
|
item.setSavePath(info.filePath);
|
|
}
|
|
|
|
this.upsertFileToDownloads(item, 'progressing');
|
|
this.progressingItems.set(this.getFileId(item), item);
|
|
this.handleDownloadItemEvents(item, webContents);
|
|
this.openDownloadsDropdown();
|
|
this.toggleAppMenuDownloadsEnabled(true);
|
|
} else {
|
|
event.preventDefault();
|
|
|
|
if (this.shouldShowSaveDialog(item, Config.downloadLocation)) {
|
|
const saveDialogResult = await this.showSaveDialog(item);
|
|
if (saveDialogResult.canceled || !saveDialogResult.filePath) {
|
|
return;
|
|
}
|
|
this.willDownloadURLs.set(url, {filePath: saveDialogResult.filePath, bookmark: saveDialogResult.bookmark});
|
|
} else {
|
|
const filename = this.createFilename(item);
|
|
const downloadLocation = await this.verifyMacAppStoreDownloadFolder(filename);
|
|
const savePath = this.getSavePath(`${downloadLocation}`, filename);
|
|
this.willDownloadURLs.set(url, {filePath: savePath});
|
|
}
|
|
|
|
webContents.downloadURL(url);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This function monitors webRequests and retrieves the total file size (of files being downloaded)
|
|
* from the custom HTTP header "x-uncompressed-content-length".
|
|
*/
|
|
webRequestOnHeadersReceivedHandler = (details: Electron.OnHeadersReceivedListenerDetails, cb: (headersReceivedResponse: Electron.HeadersReceivedResponse) => void) => {
|
|
const headers = details.responseHeaders ?? {};
|
|
|
|
if (headers?.['content-encoding']?.includes('gzip') && headers?.['x-uncompressed-content-length'] && headers?.['content-disposition'].join(';')?.includes('filename=')) {
|
|
const filename = readFilenameFromContentDispositionHeader(headers['content-disposition']);
|
|
const fileSize = headers['x-uncompressed-content-length']?.[0] || '0';
|
|
if (filename && (!this.fileSizes.has(filename) || this.fileSizes.get(filename)?.toString() !== fileSize)) {
|
|
this.fileSizes.set(filename, fileSize);
|
|
}
|
|
}
|
|
|
|
// With no arguments it uses the same headers
|
|
cb({});
|
|
};
|
|
|
|
reloadFilesForMAS = () => {
|
|
// eslint-disable-next-line no-undef
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
if (!__IS_MAC_APP_STORE__) {
|
|
return;
|
|
}
|
|
|
|
for (const file of Object.values(this.downloads)) {
|
|
if (file.bookmark) {
|
|
this.bookmarks.set(this.getDownloadedFileId(file), {originalPath: file.location, bookmark: file.bookmark});
|
|
|
|
if (file.mimeType?.toLowerCase().startsWith('image/')) {
|
|
const func = app.startAccessingSecurityScopedResource(file.bookmark);
|
|
fs.copyFileSync(file.location, path.resolve(app.getPath('temp'), path.basename(file.location)));
|
|
func();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
checkForDeletedFiles = () => {
|
|
log.debug('checkForDeletedFiles');
|
|
const downloads = this.downloads;
|
|
let modified = false;
|
|
|
|
for (const fileId in downloads) {
|
|
if (Object.prototype.hasOwnProperty.call(downloads, fileId)) {
|
|
const file = downloads[fileId];
|
|
|
|
if (this.isInvalidFile(file)) {
|
|
delete downloads[fileId];
|
|
modified = true;
|
|
continue;
|
|
}
|
|
|
|
// Remove update if app was updated and restarted
|
|
if (fileId === APP_UPDATE_KEY) {
|
|
if (appVersionManager.lastAppVersion === file.filename) {
|
|
delete downloads[APP_UPDATE_KEY];
|
|
modified = true;
|
|
continue;
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (file.state === 'completed') {
|
|
if (!file.location || !fs.existsSync(file.location)) {
|
|
downloads[fileId].state = 'deleted';
|
|
modified = true;
|
|
}
|
|
} else if (file.state === 'progressing') {
|
|
downloads[fileId].state = 'interrupted';
|
|
modified = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (modified) {
|
|
this.saveAll(downloads);
|
|
}
|
|
};
|
|
|
|
clearDownloadsDropDown = () => {
|
|
log.debug('clearDownloadsDropDown');
|
|
|
|
if (this.hasUpdate()) {
|
|
this.saveAll({
|
|
[APP_UPDATE_KEY]: this.downloads[APP_UPDATE_KEY],
|
|
});
|
|
} else {
|
|
this.saveAll({});
|
|
this.toggleAppMenuDownloadsEnabled(false);
|
|
}
|
|
this.closeDownloadsDropdown();
|
|
this.fileSizes = new Map();
|
|
};
|
|
|
|
showFileInFolder = (item?: DownloadedItem) => {
|
|
log.debug('showFileInFolder', {item});
|
|
|
|
if (!item) {
|
|
log.debug('showFileInFolder', 'ITEM_UNDEFINED');
|
|
return;
|
|
}
|
|
|
|
if (item.type === DownloadItemTypeEnum.UPDATE) {
|
|
return;
|
|
}
|
|
|
|
if (fs.existsSync(item.location)) {
|
|
shell.showItemInFolder(item.location);
|
|
return;
|
|
}
|
|
this.markFileAsDeleted(item);
|
|
|
|
if (Config.downloadLocation) {
|
|
shell.openPath(Config.downloadLocation);
|
|
return;
|
|
}
|
|
|
|
log.debug('showFileInFolder', 'NO_DOWNLOAD_LOCATION');
|
|
};
|
|
|
|
openFile = (item?: DownloadedItem) => {
|
|
log.debug('openFile', {item});
|
|
|
|
if (!item) {
|
|
log.debug('openFile', 'FILE_UNDEFINED');
|
|
return;
|
|
}
|
|
|
|
if (item.type === DownloadItemTypeEnum.UPDATE) {
|
|
return;
|
|
}
|
|
|
|
if (fs.existsSync(item.location)) {
|
|
let func;
|
|
const bookmark = this.bookmarks.get(this.getDownloadedFileId(item));
|
|
if (bookmark) {
|
|
func = app.startAccessingSecurityScopedResource(bookmark.bookmark);
|
|
}
|
|
shell.openPath(item.location).catch((err) => {
|
|
log.debug('openFileError', {err});
|
|
this.showFileInFolder(item);
|
|
});
|
|
func?.();
|
|
} else {
|
|
log.debug('openFile', 'COULD_NOT_OPEN_FILE');
|
|
this.markFileAsDeleted(item);
|
|
this.showFileInFolder(item);
|
|
}
|
|
};
|
|
|
|
clearFile = (item?: DownloadedItem) => {
|
|
log.debug('clearFile', {item});
|
|
|
|
if (!item || item.type === DownloadItemTypeEnum.UPDATE) {
|
|
return;
|
|
}
|
|
|
|
const fileId = this.getDownloadedFileId(item);
|
|
const downloads = this.downloads;
|
|
delete downloads[fileId];
|
|
this.saveAll(downloads);
|
|
|
|
if (!this.hasDownloads()) {
|
|
this.closeDownloadsDropdown();
|
|
}
|
|
};
|
|
|
|
cancelDownload = (item?: DownloadedItem) => {
|
|
log.debug('cancelDownload', {item});
|
|
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
const fileId = this.getDownloadedFileId(item);
|
|
|
|
if (this.isAppUpdate(item)) {
|
|
ipcMain.emit(CANCEL_UPDATE_DOWNLOAD);
|
|
const update = this.downloads[APP_UPDATE_KEY];
|
|
update.state = 'cancelled';
|
|
this.save(APP_UPDATE_KEY, update);
|
|
} else if (this.progressingItems.has(fileId)) {
|
|
this.progressingItems.get(fileId)?.cancel?.();
|
|
this.progressingItems.delete(fileId);
|
|
}
|
|
};
|
|
|
|
onOpen = () => {
|
|
this.open = true;
|
|
MainWindow.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE);
|
|
};
|
|
|
|
onClose = () => {
|
|
this.open = false;
|
|
ipcMain.emit(CLOSE_DOWNLOADS_DROPDOWN_MENU);
|
|
this.clearAutoCloseTimeout();
|
|
};
|
|
|
|
getIsOpen = () => {
|
|
return this.open;
|
|
};
|
|
|
|
hasDownloads = () => {
|
|
log.debug('hasDownloads');
|
|
return (Object.keys(this.downloads)?.length || 0) > 0;
|
|
};
|
|
|
|
getDownloads = () => {
|
|
return this.downloads;
|
|
};
|
|
|
|
openDownloadsDropdown = () => {
|
|
log.debug('openDownloadsDropdown');
|
|
this.open = true;
|
|
ipcMain.emit(OPEN_DOWNLOADS_DROPDOWN);
|
|
MainWindow.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE);
|
|
};
|
|
|
|
hasUpdate = () => {
|
|
return Boolean(this.downloads[APP_UPDATE_KEY]?.type === DownloadItemTypeEnum.UPDATE);
|
|
};
|
|
|
|
removeUpdateBeforeRestart = (): void => {
|
|
const downloads = this.downloads;
|
|
delete downloads[APP_UPDATE_KEY];
|
|
this.saveAll(downloads);
|
|
};
|
|
|
|
private handleSelectDownload = (event: IpcMainInvokeEvent, startFrom: string) => {
|
|
return this.selectDefaultDownloadDirectory(
|
|
startFrom,
|
|
localizeMessage('main.downloadsManager.specifyDownloadsFolder', 'Specify the folder where files will download'),
|
|
);
|
|
}
|
|
|
|
private selectDefaultDownloadDirectory = async (startFrom: string, message: string) => {
|
|
log.debug('handleSelectDownload', startFrom);
|
|
|
|
const result = await dialog.showOpenDialog({defaultPath: startFrom || Config.downloadLocation,
|
|
message,
|
|
properties:
|
|
['openDirectory', 'createDirectory', 'dontAddToRecent', 'promptToCreate']});
|
|
return result.filePaths[0];
|
|
}
|
|
|
|
private verifyMacAppStoreDownloadFolder = async (fileName: string) => {
|
|
let downloadLocation = Config.downloadLocation;
|
|
|
|
// eslint-disable-next-line no-undef
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
if (__IS_MAC_APP_STORE__ && downloadLocation) {
|
|
try {
|
|
const savePath = this.getSavePath(downloadLocation, fileName);
|
|
fs.writeFileSync(savePath, '');
|
|
fs.unlinkSync(savePath);
|
|
} catch (e) {
|
|
downloadLocation = await this.selectDefaultDownloadDirectory(
|
|
downloadLocation,
|
|
localizeMessage('main.downloadsManager.resetDownloadsFolder', 'Please reset the folder where files will download'),
|
|
);
|
|
Config.set('downloadLocation', downloadLocation);
|
|
}
|
|
}
|
|
|
|
return downloadLocation;
|
|
}
|
|
|
|
private markFileAsDeleted = (item: DownloadedItem) => {
|
|
const fileId = this.getDownloadedFileId(item);
|
|
const file = this.downloads[fileId];
|
|
file.state = 'deleted';
|
|
this.save(fileId, file);
|
|
};
|
|
|
|
private toggleAppMenuDownloadsEnabled = (value: boolean) => {
|
|
const appMenuDownloads = Menu.getApplicationMenu()?.getMenuItemById('app-menu-downloads');
|
|
if (appMenuDownloads) {
|
|
appMenuDownloads.enabled = value;
|
|
}
|
|
};
|
|
|
|
private saveAll = (downloads: DownloadedItems): void => {
|
|
log.debug('saveAll');
|
|
|
|
this.downloads = downloads;
|
|
this.setJson(downloads);
|
|
ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN, true, this.downloads);
|
|
MainWindow.sendToRenderer(UPDATE_DOWNLOADS_DROPDOWN, this.downloads);
|
|
};
|
|
|
|
private save = (key: string, item: DownloadedItem) => {
|
|
log.debug('save');
|
|
this.downloads[key] = item;
|
|
this.setValue(key, item);
|
|
ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN, true, this.downloads);
|
|
MainWindow.sendToRenderer(UPDATE_DOWNLOADS_DROPDOWN, this.downloads);
|
|
};
|
|
|
|
private handleDownloadItemEvents = (item: DownloadItem, webContents: WebContents) => {
|
|
item.on('updated', (updateEvent, state) => {
|
|
this.updatedEventController(updateEvent, state, item);
|
|
});
|
|
item.once('done', (doneEvent, state) => {
|
|
this.doneEventController(doneEvent, state, item, webContents);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* This function return true if "downloadLocation" is undefined
|
|
*/
|
|
private shouldShowSaveDialog = (item: DownloadItem, downloadLocation?: string) => {
|
|
log.debug('shouldShowSaveDialog', {downloadLocation});
|
|
return !item.hasUserGesture() || !downloadLocation;
|
|
};
|
|
|
|
private showSaveDialog = (item: DownloadItem) => {
|
|
const filename = item.getFilename();
|
|
const fileElements = filename.split('.');
|
|
const filters = this.getFileFilters(fileElements.slice(fileElements.length - 1));
|
|
|
|
return dialog.showSaveDialog({
|
|
title: filename,
|
|
defaultPath: Config.downloadLocation ? path.join(Config.downloadLocation, filename) : filename,
|
|
filters,
|
|
securityScopedBookmarks: true,
|
|
});
|
|
};
|
|
|
|
private closeDownloadsDropdown = () => {
|
|
log.debug('closeDownloadsDropdown');
|
|
this.open = false;
|
|
ipcMain.emit(CLOSE_DOWNLOADS_DROPDOWN);
|
|
ipcMain.emit(CLOSE_DOWNLOADS_DROPDOWN_MENU);
|
|
this.clearAutoCloseTimeout();
|
|
};
|
|
|
|
private clearAutoCloseTimeout = () => {
|
|
if (this.autoCloseTimeout) {
|
|
clearTimeout(this.autoCloseTimeout);
|
|
this.autoCloseTimeout = null;
|
|
}
|
|
};
|
|
|
|
private upsertFileToDownloads = (item: DownloadItem, state: DownloadItemState, overridePath?: string) => {
|
|
const fileId = this.getFileId(item);
|
|
log.debug('upsertFileToDownloads', {fileId});
|
|
const formattedItem = this.formatDownloadItem(item, state, overridePath);
|
|
this.save(fileId, formattedItem);
|
|
this.checkIfMaxFilesReached();
|
|
};
|
|
|
|
private checkIfMaxFilesReached = () => {
|
|
const downloads = this.downloads;
|
|
if (Object.keys(downloads).length > DOWNLOADS_DROPDOWN_MAX_ITEMS) {
|
|
const oldestFileId = Object.keys(downloads).reduce((prev, curr) => {
|
|
return downloads[prev]?.addedAt > downloads[curr]?.addedAt ? curr : prev;
|
|
});
|
|
delete downloads[oldestFileId];
|
|
this.saveAll(downloads);
|
|
}
|
|
};
|
|
|
|
private shouldAutoClose = () => {
|
|
// if some other file is being downloaded
|
|
if (Object.values(this.downloads).some((item) => item.state === 'progressing')) {
|
|
return;
|
|
}
|
|
if (this.autoCloseTimeout) {
|
|
this.autoCloseTimeout.refresh();
|
|
} else {
|
|
this.autoCloseTimeout = setTimeout(() => this.closeDownloadsDropdown(), DOWNLOADS_DROPDOWN_AUTOCLOSE_TIMEOUT);
|
|
}
|
|
};
|
|
|
|
private shouldShowBadge = () => {
|
|
log.debug('shouldShowBadge');
|
|
|
|
if (this.open === true) {
|
|
MainWindow.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE);
|
|
} else {
|
|
MainWindow.sendToRenderer(SHOW_DOWNLOADS_DROPDOWN_BUTTON_BADGE);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* DownloadItem event handlers
|
|
*/
|
|
private updatedEventController = (updatedEvent: Event, state: DownloadItemUpdatedEventState, item: DownloadItem) => {
|
|
log.debug('updatedEventController', {state});
|
|
|
|
this.upsertFileToDownloads(item, state);
|
|
|
|
if (state === 'interrupted') {
|
|
this.fileSizes.delete(item.getFilename());
|
|
this.progressingItems.delete(this.getFileId(item));
|
|
}
|
|
this.shouldShowBadge();
|
|
};
|
|
|
|
private doneEventController = (doneEvent: Event, state: DownloadItemDoneEventState, item: DownloadItem, webContents: WebContents) => {
|
|
log.debug('doneEventController', {state});
|
|
|
|
if (state === 'completed' && !this.open) {
|
|
displayDownloadCompleted(path.basename(item.savePath), item.savePath, ViewManager.getViewByWebContentsId(webContents.id)?.view.server.name ?? '');
|
|
}
|
|
|
|
const bookmark = this.bookmarks.get(this.getFileId(item));
|
|
if (bookmark) {
|
|
const func = app.startAccessingSecurityScopedResource(bookmark?.bookmark);
|
|
fs.copyFileSync(path.resolve(app.getPath('temp'), path.basename(bookmark.originalPath)), bookmark.originalPath);
|
|
func();
|
|
}
|
|
|
|
this.upsertFileToDownloads(item, state, bookmark?.originalPath);
|
|
this.fileSizes.delete(item.getFilename());
|
|
this.progressingItems.delete(this.getFileId(item));
|
|
this.shouldAutoClose();
|
|
this.shouldShowBadge();
|
|
};
|
|
|
|
/**
|
|
* Related to application updates
|
|
*/
|
|
private onUpdateAvailable = (event: Event, version = 'unknown') => {
|
|
this.save(APP_UPDATE_KEY, {
|
|
...UPDATE_DOWNLOAD_ITEM,
|
|
filename: version,
|
|
state: 'available',
|
|
});
|
|
this.openDownloadsDropdown();
|
|
};
|
|
private onUpdateDownloaded = (event: Event, info: UpdateInfo) => {
|
|
log.debug('onUpdateDownloaded', {info});
|
|
|
|
const {version} = info;
|
|
const update = this.downloads[APP_UPDATE_KEY];
|
|
update.state = 'completed';
|
|
update.progress = 100;
|
|
update.filename = version;
|
|
|
|
this.save(APP_UPDATE_KEY, update);
|
|
this.openDownloadsDropdown();
|
|
};
|
|
private onUpdateProgress = (event: Event, progress: ProgressInfo) => {
|
|
log.debug('onUpdateProgress', {progress});
|
|
const {total, transferred, percent} = progress;
|
|
const update = this.downloads[APP_UPDATE_KEY] || {...UPDATE_DOWNLOAD_ITEM};
|
|
if (typeof update.addedAt !== 'number' || update.addedAt === 0) {
|
|
update.addedAt = Date.now();
|
|
}
|
|
update.state = 'progressing';
|
|
update.totalBytes = total;
|
|
update.receivedBytes = transferred;
|
|
update.progress = Math.round(percent);
|
|
this.save(APP_UPDATE_KEY, update);
|
|
this.shouldShowBadge();
|
|
};
|
|
private noUpdateAvailable = () => {
|
|
const downloads = this.downloads;
|
|
delete downloads[APP_UPDATE_KEY];
|
|
this.saveAll(downloads);
|
|
|
|
if (!this.hasDownloads()) {
|
|
this.closeDownloadsDropdown();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Internal utils
|
|
*/
|
|
private formatDownloadItem = (item: DownloadItem, state: DownloadItemState, overridePath?: string): DownloadedItem => {
|
|
const totalBytes = this.getFileSize(item);
|
|
const receivedBytes = item.getReceivedBytes();
|
|
const progress = getPercentage(receivedBytes, totalBytes);
|
|
|
|
return {
|
|
addedAt: doubleSecToMs(item.getStartTime()),
|
|
filename: this.getFileId(item),
|
|
mimeType: item.getMimeType(),
|
|
location: overridePath ?? item.getSavePath(),
|
|
progress,
|
|
receivedBytes,
|
|
state,
|
|
totalBytes,
|
|
type: DownloadItemTypeEnum.FILE,
|
|
bookmark: this.getBookmark(item),
|
|
};
|
|
};
|
|
|
|
private getBookmark = (item: DownloadItem) => {
|
|
return this.bookmarks.get(this.getFileId(item))?.bookmark;
|
|
}
|
|
|
|
private getFileSize = (item: DownloadItem) => {
|
|
const itemTotalBytes = item.getTotalBytes();
|
|
if (!itemTotalBytes) {
|
|
return parseInt(this.fileSizes.get(item.getFilename()) || '0', 10);
|
|
}
|
|
return itemTotalBytes;
|
|
};
|
|
|
|
private getSavePath = (downloadLocation: string, filename?: string) => {
|
|
const name = isStringWithLength(filename) ? `${filename}` : 'file';
|
|
return path.join(downloadLocation, name);
|
|
};
|
|
|
|
private getFileFilters = (fileElements: string[]): FileFilter[] => {
|
|
const filters = fileElements.map((element) => ({
|
|
name: `${element.toUpperCase()} (*.${element})`,
|
|
extensions: [element],
|
|
}));
|
|
|
|
filters.push({
|
|
name: localizeMessage('main.app.initialize.downloadBox.allFiles', 'All files'),
|
|
extensions: ['*'],
|
|
});
|
|
|
|
return filters;
|
|
};
|
|
|
|
private createFilename = (item: DownloadItem): string => {
|
|
const defaultFilename = item.getFilename();
|
|
const incrementedFilenameIfExists = shouldIncrementFilename(path.join(`${Config.downloadLocation}`, defaultFilename));
|
|
return incrementedFilenameIfExists;
|
|
};
|
|
|
|
private readFilenameFromPath = (savePath: string) => {
|
|
const pathObj = path.parse(savePath);
|
|
return pathObj.base;
|
|
};
|
|
|
|
private getFileId = (item: DownloadItem) => {
|
|
const fileNameFromPath = this.readFilenameFromPath(item.savePath);
|
|
const itemFilename = item.getFilename();
|
|
return fileNameFromPath && fileNameFromPath !== itemFilename ? fileNameFromPath : itemFilename;
|
|
};
|
|
|
|
private getDownloadedFileId = (item: DownloadedItem) => {
|
|
if (item.type === DownloadItemTypeEnum.UPDATE) {
|
|
return APP_UPDATE_KEY;
|
|
}
|
|
const fileNameFromPath = this.readFilenameFromPath(item.location);
|
|
const itemFilename = item.filename;
|
|
return fileNameFromPath && fileNameFromPath !== itemFilename ? fileNameFromPath : itemFilename;
|
|
};
|
|
|
|
private isAppUpdate = (item: DownloadedItem): boolean => {
|
|
return item.type === DownloadItemTypeEnum.UPDATE;
|
|
};
|
|
|
|
private isInvalidFile(file: DownloadedItem) {
|
|
return (typeof file !== 'object') ||
|
|
!file.filename ||
|
|
!file.state ||
|
|
!file.type;
|
|
}
|
|
}
|
|
|
|
let downloadsManager = new DownloadsManager(downloadsJson);
|
|
|
|
ipcMain.on(UPDATE_PATHS, () => {
|
|
downloadsManager = new DownloadsManager(downloadsJson);
|
|
});
|
|
|
|
export default downloadsManager;
|