-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #37131 from wildan-m/wildan/fix/35189-multi-downlo…
…ads-external-link Fix multiple download issues on Mobile Safari and desktop versions.
- Loading branch information
Showing
12 changed files
with
325 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import type {BrowserWindow} from 'electron'; | ||
import {app} from 'electron'; | ||
import * as path from 'path'; | ||
import createQueue from '@libs/Queue/Queue'; | ||
import CONST from '@src/CONST'; | ||
import ELECTRON_EVENTS from './ELECTRON_EVENTS'; | ||
import type Options from './electronDownloadManagerType'; | ||
|
||
type DownloadItem = { | ||
// The window where the download will be initiated | ||
win: BrowserWindow; | ||
|
||
// The URL of the file to be downloaded | ||
url: string; | ||
|
||
// The options for the download, such as save path, file name, etc. | ||
options: Options; | ||
}; | ||
|
||
/** | ||
* Returns the filename with extension based on the given name and MIME type. | ||
* @param name - The name of the file. | ||
* @param mime - The MIME type of the file. | ||
* @returns The filename with extension. | ||
*/ | ||
const getFilenameFromMime = (name: string, mime: string): string => { | ||
const extensions = mime.split('/').pop(); | ||
return `${name}.${extensions}`; | ||
}; | ||
|
||
const createDownloadQueue = () => { | ||
const downloadItemProcessor = (item: DownloadItem): Promise<void> => | ||
new Promise((resolve, reject) => { | ||
let downloadTimeout: NodeJS.Timeout; | ||
let downloadListener: (event: Electron.Event, electronDownloadItem: Electron.DownloadItem) => void; | ||
|
||
const timeoutFunction = () => { | ||
item.win.webContents.session.removeListener('will-download', downloadListener); | ||
resolve(); | ||
}; | ||
|
||
const listenerFunction = (event: Electron.Event, electronDownloadItem: Electron.DownloadItem) => { | ||
clearTimeout(downloadTimeout); | ||
|
||
const options = item.options; | ||
const cleanup = () => item.win.webContents.session.removeListener('will-download', listenerFunction); | ||
const errorMessage = `The download of ${electronDownloadItem.getFilename()} was interrupted`; | ||
|
||
if (options.directory && !path.isAbsolute(options.directory)) { | ||
throw new Error('The `directory` option must be an absolute path'); | ||
} | ||
|
||
const directory = options.directory ?? app.getPath('downloads'); | ||
|
||
let filePath: string; | ||
if (options.filename) { | ||
filePath = path.join(directory, options.filename); | ||
} else { | ||
const filename = electronDownloadItem.getFilename(); | ||
const name = path.extname(filename) ? filename : getFilenameFromMime(filename, electronDownloadItem.getMimeType()); | ||
|
||
filePath = options.overwrite ? path.join(directory, name) : path.join(directory, name); | ||
} | ||
|
||
if (options.saveAs) { | ||
electronDownloadItem.setSaveDialogOptions({defaultPath: filePath, ...options.dialogOptions}); | ||
} else { | ||
electronDownloadItem.setSavePath(filePath); | ||
} | ||
|
||
electronDownloadItem.on('updated', (_, state) => { | ||
if (state !== 'interrupted') { | ||
return; | ||
} | ||
|
||
item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_CANCELED, {url: item.url}); | ||
cleanup(); | ||
reject(new Error(errorMessage)); | ||
electronDownloadItem.cancel(); | ||
}); | ||
|
||
electronDownloadItem.on('done', (_, state) => { | ||
cleanup(); | ||
if (state === 'cancelled') { | ||
item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_CANCELED, {url: item.url}); | ||
resolve(); | ||
} else if (state === 'interrupted') { | ||
item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_FAILED, {url: item.url}); | ||
reject(new Error(errorMessage)); | ||
} else if (state === 'completed') { | ||
if (process.platform === 'darwin') { | ||
const savePath = electronDownloadItem.getSavePath(); | ||
app.dock.downloadFinished(savePath); | ||
} | ||
item.win.webContents.send(ELECTRON_EVENTS.DOWNLOAD_COMPLETED, {url: item.url}); | ||
resolve(); | ||
} | ||
}); | ||
}; | ||
|
||
downloadTimeout = setTimeout(timeoutFunction, CONST.DOWNLOADS_TIMEOUT); | ||
downloadListener = listenerFunction; | ||
|
||
item.win.webContents.downloadURL(item.url); | ||
item.win.webContents.session.on('will-download', downloadListener); | ||
}); | ||
|
||
const queue = createQueue<DownloadItem>(downloadItemProcessor); | ||
|
||
const enqueueDownloadItem = (item: DownloadItem): void => { | ||
queue.enqueue(item); | ||
}; | ||
return {enqueueDownloadItem, dequeueDownloadItem: queue.dequeue}; | ||
}; | ||
|
||
export default createDownloadQueue; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import type {SaveDialogOptions} from 'electron'; | ||
|
||
type Options = { | ||
/** | ||
Show a `Save As…` dialog instead of downloading immediately. | ||
Note: Only use this option when strictly necessary. Downloading directly without a prompt is a much better user experience. | ||
@default false | ||
*/ | ||
readonly saveAs?: boolean; | ||
|
||
/** | ||
The directory to save the file in. | ||
Must be an absolute path. | ||
Default: [User's downloads directory](https://electronjs.org/docs/api/app/#appgetpathname) | ||
*/ | ||
readonly directory?: string; | ||
|
||
/** | ||
Name of the saved file. | ||
This option only makes sense for `electronDownloadManager.download()`. | ||
Default: [`downloadItem.getFilename()`](https://electronjs.org/docs/api/download-item/#downloaditemgetfilename) | ||
*/ | ||
readonly filename?: string; | ||
|
||
/** | ||
Allow downloaded files to overwrite files with the same name in the directory they are saved to. | ||
The default behavior is to append a number to the filename. | ||
@default false | ||
*/ | ||
readonly overwrite?: boolean; | ||
|
||
/** | ||
Customize the save dialog. | ||
If `defaultPath` is not explicity defined, a default value is assigned based on the file path. | ||
@default {} | ||
*/ | ||
readonly dialogOptions?: SaveDialogOptions; | ||
}; | ||
|
||
export default Options; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import Log from '@libs/Log'; | ||
import CONST from '@src/CONST'; | ||
import type Queue from './QueueType'; | ||
|
||
// Function to create a new queue | ||
function createQueue<T>(processItem: (item: T) => Promise<void>): Queue<T> { | ||
// Array to hold the elements of the queue | ||
const elements: T[] = []; | ||
let isProcessing = false; | ||
|
||
// Function to remove an item from the front of the queue | ||
function dequeue(): T | undefined { | ||
return elements.shift(); | ||
} | ||
|
||
// Function to check if the queue is empty | ||
function isEmpty(): boolean { | ||
return elements.length === 0; | ||
} | ||
|
||
// Function to process the next item in the queue | ||
function processNext(): Promise<void> { | ||
return new Promise((resolve) => { | ||
if (!isEmpty()) { | ||
const nextItem = dequeue(); | ||
if (nextItem) { | ||
processItem(nextItem) | ||
.catch((error: Error) => { | ||
const errorMessage = error.message ?? CONST.ERROR.UNKNOWN_ERROR; | ||
|
||
Log.hmmm('Queue error:', {errorMessage}); | ||
}) | ||
.finally(() => { | ||
processNext().then(resolve); | ||
}); | ||
} | ||
} else { | ||
isProcessing = false; | ||
resolve(); | ||
} | ||
}); | ||
} | ||
|
||
// Initiates the processing of items in the queue. | ||
// Continues to dequeue and process items as long as the queue is not empty. | ||
// Sets the `isProcessing` flag to true at the start and resets it to false once all items have been processed. | ||
function run(): Promise<void> { | ||
isProcessing = true; | ||
return processNext(); | ||
} | ||
|
||
// Adds an item to the queue and initiates processing if not already in progress | ||
function enqueue(item: T): void { | ||
elements.push(item); | ||
if (!isProcessing) { | ||
run(); | ||
} | ||
} | ||
|
||
// Function to get the item at the front of the queue without removing it | ||
function peek(): T | undefined { | ||
return elements.length > 0 ? elements[0] : undefined; | ||
} | ||
|
||
// Function to get the number of items in the queue | ||
function size(): number { | ||
return elements.length; | ||
} | ||
|
||
// Return an object with the queue operations | ||
return { | ||
run, | ||
enqueue, | ||
dequeue, | ||
isEmpty, | ||
peek, | ||
size, | ||
}; | ||
} | ||
|
||
export default createQueue; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
type Queue<T> = { | ||
run(): Promise<void>; | ||
enqueue: (item: T) => void; | ||
dequeue: () => T | undefined; | ||
isEmpty: () => boolean; | ||
peek: () => T | undefined; | ||
size: () => number; | ||
}; | ||
|
||
export default Queue; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import ELECTRON_EVENTS from '@desktop/ELECTRON_EVENTS'; | ||
import type Options from '@desktop/electronDownloadManagerType'; | ||
import CONST from '@src/CONST'; | ||
import type {FileDownload} from './types'; | ||
|
||
/** | ||
* The function downloads an attachment on desktop platforms. | ||
*/ | ||
const fileDownload: FileDownload = (url, fileName) => { | ||
const options: Options = { | ||
filename: fileName, | ||
saveAs: true, | ||
}; | ||
window.electron.send(ELECTRON_EVENTS.DOWNLOAD, {url, options}); | ||
return new Promise((resolve) => { | ||
// This sets a timeout that will resolve the promise after 5 seconds to prevent indefinite hanging | ||
const downloadTimeout = setTimeout(() => { | ||
resolve(); | ||
}, CONST.DOWNLOADS_TIMEOUT); | ||
|
||
const handleDownloadStatus = (...args: unknown[]) => { | ||
const arg = Array.isArray(args) ? args[0] : null; | ||
const eventUrl = arg && typeof arg === 'object' && 'url' in arg ? arg.url : null; | ||
|
||
if (eventUrl === url) { | ||
clearTimeout(downloadTimeout); | ||
resolve(); | ||
} | ||
}; | ||
|
||
window.electron.on(ELECTRON_EVENTS.DOWNLOAD_COMPLETED, handleDownloadStatus); | ||
window.electron.on(ELECTRON_EVENTS.DOWNLOAD_FAILED, handleDownloadStatus); | ||
window.electron.on(ELECTRON_EVENTS.DOWNLOAD_CANCELED, handleDownloadStatus); | ||
}); | ||
}; | ||
|
||
export default fileDownload; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.