<commit_before>// 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} from 'electron';
import log from 'electron-log';
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,
    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 {APP_UPDATE_KEY, UPDATE_DOWNLOAD_ITEM} from 'common/constants';
import {DOWNLOADS_DROPDOWN_AUTOCLOSE_TIMEOUT, DOWNLOADS_DROPDOWN_MAX_ITEMS} from 'common/utils/constants';
import {localizeMessage} from 'main/i18nManager';
import {displayDownloadCompleted} from 'main/notifications';
import WindowManager from 'main/windows/windowManager';
import {doubleSecToMs, getPercentage, isStringWithLength, readFilenameFromContentDispositionHeader, shouldIncrementFilename} from 'main/utils';

import appVersionManager from './AppVersionManager';
import {downloadsJson} from './constants';
import * as Validator from './Validator';

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('DownloadsManager.init', {'this.json': this.json, validatedJSON});
        if (validatedJSON) {
            this.saveAll(validatedJSON);
        } else {
            this.saveAll({});
        }
        this.checkForDeletedFiles();
        this.reloadFilesForMAS();

        ipcMain.handle(REQUEST_HAS_DOWNLOADS, () => {
            return this.hasDownloads();
        });
        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('DownloadsManager.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 savePath = this.getSavePath(`${Config.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('DownloadsManager.checkForDeletedFiles');
        const downloads = this.downloads;
        let modified = false;

        for (const fileId in downloads) {
            if (Object.prototype.hasOwnProperty.call(downloads, fileId)) {
                // Remove update if app was updated and restarted
                if (fileId === APP_UPDATE_KEY) {
                    if (appVersionManager.lastAppVersion === downloads[APP_UPDATE_KEY].filename) {
                        delete downloads[APP_UPDATE_KEY];
                        modified = true;
                    } else {
                        continue;
                    }
                }
                const file = downloads[fileId];
                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('DownloadsManager.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('DownloadsDropdownView.showFileInFolder', {item});

        if (!item) {
            log.debug('DownloadsDropdownView.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('DownloadsDropdownView.showFileInFolder', 'NO_DOWNLOAD_LOCATION');
    };

    openFile = (item?: DownloadedItem) => {
        log.debug('DownloadsDropdownView.openFile', {item});

        if (!item) {
            log.debug('DownloadsDropdownView.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('DownloadsDropdownView.openFileError', {err});
                this.showFileInFolder(item);
            });
            func?.();
        } else {
            log.debug('DownloadsDropdownView.openFile', 'COULD_NOT_OPEN_FILE');
            this.markFileAsDeleted(item);
            this.showFileInFolder(item);
        }
    };

    clearFile = (item?: DownloadedItem) => {
        log.debug('DownloadsDropdownView.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('DownloadsDropdownView.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;
        WindowManager.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('DownloadsManager.hasDownloads');
        return (Object.keys(this.downloads)?.length || 0) > 0;
    };

    getDownloads = () => {
        return this.downloads;
    };

    openDownloadsDropdown = () => {
        log.debug('DownloadsManager.openDownloadsDropdown');
        this.open = true;
        ipcMain.emit(OPEN_DOWNLOADS_DROPDOWN);
        WindowManager.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 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('DownloadsManager.saveAll');

        this.downloads = downloads;
        this.setJson(downloads);
        ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN, true, this.downloads);
        WindowManager?.sendToRenderer(UPDATE_DOWNLOADS_DROPDOWN, this.downloads);
    };

    private save = (key: string, item: DownloadedItem) => {
        log.debug('DownloadsManager.save');
        this.downloads[key] = item;
        this.setValue(key, item);
        ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN, true, this.downloads);
        WindowManager?.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('DownloadsManager.shouldShowSaveDialog', {downloadLocation});
        return !item.hasUserGesture() || !downloadLocation;
    };

    private showSaveDialog = (item: DownloadItem) => {
        const filename = item.getFilename();
        const fileElements = filename.split('.');
        const filters = this.getFileFilters(fileElements.slice(1));

        return dialog.showSaveDialog({
            title: filename,
            defaultPath: Config.downloadLocation ? path.join(Config.downloadLocation, filename) : filename,
            filters,
            securityScopedBookmarks: true,
        });
    };

    private closeDownloadsDropdown = () => {
        log.debug('DownloadsManager.closeDownloadsDropdown');
        this.open = false;
        ipcMain.emit(CLOSE_DOWNLOADS_DROPDOWN);<commit_msg>[MM-48167] Only grab the last extension from a filename during Save As (#2357)<commit_after>// 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} from 'electron';
import log from 'electron-log';
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,
    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 {APP_UPDATE_KEY, UPDATE_DOWNLOAD_ITEM} from 'common/constants';
import {DOWNLOADS_DROPDOWN_AUTOCLOSE_TIMEOUT, DOWNLOADS_DROPDOWN_MAX_ITEMS} from 'common/utils/constants';
import {localizeMessage} from 'main/i18nManager';
import {displayDownloadCompleted} from 'main/notifications';
import WindowManager from 'main/windows/windowManager';
import {doubleSecToMs, getPercentage, isStringWithLength, readFilenameFromContentDispositionHeader, shouldIncrementFilename} from 'main/utils';

import appVersionManager from './AppVersionManager';
import {downloadsJson} from './constants';
import * as Validator from './Validator';

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('DownloadsManager.init', {'this.json': this.json, validatedJSON});
        if (validatedJSON) {
            this.saveAll(validatedJSON);
        } else {
            this.saveAll({});
        }
        this.checkForDeletedFiles();
        this.reloadFilesForMAS();

        ipcMain.handle(REQUEST_HAS_DOWNLOADS, () => {
            return this.hasDownloads();
        });
        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('DownloadsManager.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 savePath = this.getSavePath(`${Config.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('DownloadsManager.checkForDeletedFiles');
        const downloads = this.downloads;
        let modified = false;

        for (const fileId in downloads) {
            if (Object.prototype.hasOwnProperty.call(downloads, fileId)) {
                // Remove update if app was updated and restarted
                if (fileId === APP_UPDATE_KEY) {
                    if (appVersionManager.lastAppVersion === downloads[APP_UPDATE_KEY].filename) {
                        delete downloads[APP_UPDATE_KEY];
                        modified = true;
                    } else {
                        continue;
                    }
                }
                const file = downloads[fileId];
                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('DownloadsManager.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('DownloadsDropdownView.showFileInFolder', {item});

        if (!item) {
            log.debug('DownloadsDropdownView.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('DownloadsDropdownView.showFileInFolder', 'NO_DOWNLOAD_LOCATION');
    };

    openFile = (item?: DownloadedItem) => {
        log.debug('DownloadsDropdownView.openFile', {item});

        if (!item) {
            log.debug('DownloadsDropdownView.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('DownloadsDropdownView.openFileError', {err});
                this.showFileInFolder(item);
            });
            func?.();
        } else {
            log.debug('DownloadsDropdownView.openFile', 'COULD_NOT_OPEN_FILE');
            this.markFileAsDeleted(item);
            this.showFileInFolder(item);
        }
    };

    clearFile = (item?: DownloadedItem) => {
        log.debug('DownloadsDropdownView.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('DownloadsDropdownView.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;
        WindowManager.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('DownloadsManager.hasDownloads');
        return (Object.keys(this.downloads)?.length || 0) > 0;
    };

    getDownloads = () => {
        return this.downloads;
    };

    openDownloadsDropdown = () => {
        log.debug('DownloadsManager.openDownloadsDropdown');
        this.open = true;
        ipcMain.emit(OPEN_DOWNLOADS_DROPDOWN);
        WindowManager.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 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('DownloadsManager.saveAll');

        this.downloads = downloads;
        this.setJson(downloads);
        ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN, true, this.downloads);
        WindowManager?.sendToRenderer(UPDATE_DOWNLOADS_DROPDOWN, this.downloads);
    };

    private save = (key: string, item: DownloadedItem) => {
        log.debug('DownloadsManager.save');
        this.downloads[key] = item;
        this.setValue(key, item);
        ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN, true, this.downloads);
        WindowManager?.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('DownloadsManager.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('DownloadsManager.closeDownloadsDropdown');
        this.open = false;
        ipcMain.emit(CLOSE_DOWNLOADS_DROPDOWN);
{'fraction_non_alphanumeric': 0.11713751353479673, 'fraction_numerical': 0.0010171604816747056, 'mean_word_length': 2.4399548532731377, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 77, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 276, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4865293', 'n_tokens_mistral': 8715, 'n_tokens_neox': 8453, 'n_words': 1442}