Skip to content

Commit 9aa9d6b

Browse files
authored
feat(downloads): accept downloads in persistent, allow specifying the downloadsPath (#2503)
1 parent ee3379a commit 9aa9d6b

14 files changed

+177
-50
lines changed

browsers.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
{
88
"name": "firefox",
9-
"revision": "1103"
9+
"revision": "1106"
1010
},
1111
{
1212
"name": "webkit",

docs/api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4002,6 +4002,7 @@ This methods attaches Playwright to an existing browser instance.
40024002
- `bypass` <[string]> Optional coma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`.
40034003
- `username` <[string]> Optional username to use if HTTP proxy requires authentication.
40044004
- `password` <[string]> Optional password to use if HTTP proxy requires authentication.
4005+
- `downloadsPath` <[string]> If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
40054006
- `firefoxUserPrefs` <[Object]> Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox).
40064007
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
40074008
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`.
@@ -4041,6 +4042,8 @@ const browser = await chromium.launch({ // Or 'firefox' or 'webkit'.
40414042
- `bypass` <[string]> Optional coma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`.
40424043
- `username` <[string]> Optional username to use if HTTP proxy requires authentication.
40434044
- `password` <[string]> Optional password to use if HTTP proxy requires authentication.
4045+
- `acceptDownloads` <[boolean]> Whether to automatically download all the attachments. Defaults to `false` where all the downloads are canceled.
4046+
- `downloadsPath` <[string]> If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
40444047
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
40454048
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`.
40464049
- `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`.
@@ -4088,6 +4091,7 @@ Launches browser that uses persistent storage located at `userDataDir` and retur
40884091
- `bypass` <[string]> Optional coma-separated domains to bypass proxy, for example `".com, chromium.org, .domain.com"`.
40894092
- `username` <[string]> Optional username to use if HTTP proxy requires authentication.
40904093
- `password` <[string]> Optional password to use if HTTP proxy requires authentication.
4094+
- `downloadsPath` <[string]> If specified, accepted downloads are downloaded into this folder. Otherwise, temporary folder is created and is deleted when browser is closed.
40914095
- `firefoxUserPrefs` <[Object]> Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox).
40924096
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
40934097
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`.

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/browserContext.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { Log, InnerLogger, Logger, RootLogger } from './logger';
2828
import { FunctionWithSource } from './frames';
2929
import * as debugSupport from './debug/debugSupport';
3030

31-
export type PersistentContextOptions = {
31+
type CommonContextOptions = {
3232
viewport?: types.Size | null,
3333
ignoreHTTPSErrors?: boolean,
3434
javaScriptEnabled?: boolean,
@@ -45,10 +45,11 @@ export type PersistentContextOptions = {
4545
isMobile?: boolean,
4646
hasTouch?: boolean,
4747
colorScheme?: types.ColorScheme,
48+
acceptDownloads?: boolean,
4849
};
4950

50-
export type BrowserContextOptions = PersistentContextOptions & {
51-
acceptDownloads?: boolean,
51+
export type PersistentContextOptions = CommonContextOptions;
52+
export type BrowserContextOptions = CommonContextOptions & {
5253
logger?: Logger,
5354
};
5455

@@ -278,12 +279,6 @@ export function validateBrowserContextOptions(options: BrowserContextOptions): B
278279
return result;
279280
}
280281

281-
export function validatePersistentContextOptions(options: PersistentContextOptions): PersistentContextOptions {
282-
if ((options as any).acceptDownloads !== undefined)
283-
throw new Error(`Option "acceptDownloads" is not supported for persistent context`);
284-
return validateBrowserContextOptions(options);
285-
}
286-
287282
export function verifyGeolocation(geolocation: types.Geolocation): types.Geolocation {
288283
const result = { ...geolocation };
289284
result.accuracy = result.accuracy || 0;

src/chromium/crBrowser.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ export class CRBrowser extends BrowserBase {
5454
await session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
5555
return browser;
5656
}
57-
5857
browser._defaultContext = new CRBrowserContext(browser, null, options.persistent);
5958

6059
const existingTargetAttachPromises: Promise<any>[] = [];

src/helper.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
import * as crypto from 'crypto';
1919
import { EventEmitter } from 'events';
2020
import * as fs from 'fs';
21+
import * as removeFolder from 'rimraf';
2122
import * as util from 'util';
2223
import * as types from './types';
24+
const removeFolderAsync = util.promisify(removeFolder);
2325

2426
export type RegisteredListener = {
2527
emitter: EventEmitter;
@@ -270,6 +272,12 @@ class Helper {
270272
return { width, height };
271273
return null;
272274
}
275+
276+
static async removeFolders(dirs: string[]) {
277+
await Promise.all(dirs.map(dir => {
278+
return removeFolderAsync(dir).catch((err: Error) => console.error(err));
279+
}));
280+
}
273281
}
274282

275283
export function assert(value: any, message?: string): asserts value {

src/server/browserType.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import * as fs from 'fs';
1818
import * as os from 'os';
1919
import * as path from 'path';
2020
import * as util from 'util';
21-
import { BrowserContext, PersistentContextOptions, validatePersistentContextOptions, verifyProxySettings } from '../browserContext';
21+
import { BrowserContext, PersistentContextOptions, verifyProxySettings, validateBrowserContextOptions } from '../browserContext';
2222
import { BrowserServer, WebSocketWrapper } from './browserServer';
2323
import * as browserPaths from '../install/browserPaths';
2424
import { Logger, RootLogger, InnerLogger } from '../logger';
@@ -32,26 +32,24 @@ import { Progress, runAbortableTask } from '../progress';
3232
import { ProxySettings } from '../types';
3333
import { TimeoutSettings } from '../timeoutSettings';
3434

35-
export type BrowserArgOptions = {
36-
headless?: boolean,
37-
args?: string[],
38-
devtools?: boolean,
39-
proxy?: ProxySettings,
40-
};
41-
4235
export type FirefoxUserPrefsOptions = {
4336
firefoxUserPrefs?: { [key: string]: string | number | boolean },
4437
};
4538

46-
type LaunchOptionsBase = BrowserArgOptions & {
39+
export type LaunchOptionsBase = {
4740
executablePath?: string,
41+
args?: string[],
4842
ignoreDefaultArgs?: boolean | string[],
4943
handleSIGINT?: boolean,
5044
handleSIGTERM?: boolean,
5145
handleSIGHUP?: boolean,
5246
timeout?: number,
5347
logger?: Logger,
5448
env?: Env,
49+
headless?: boolean,
50+
devtools?: boolean,
51+
proxy?: ProxySettings,
52+
downloadsPath?: string,
5553
};
5654

5755
export function processBrowserArgOptions(options: LaunchOptionsBase): { devtools: boolean, headless: boolean } {
@@ -77,6 +75,7 @@ export interface BrowserType {
7775
connect(options: ConnectOptions): Promise<Browser>;
7876
}
7977

78+
const mkdirAsync = util.promisify(fs.mkdir);
8079
const mkdtempAsync = util.promisify(fs.mkdtemp);
8180
const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-');
8281

@@ -116,7 +115,7 @@ export abstract class BrowserTypeBase implements BrowserType {
116115

117116
async launchPersistentContext(userDataDir: string, options: LaunchOptions & PersistentContextOptions = {}): Promise<BrowserContext> {
118117
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
119-
const persistent = validatePersistentContextOptions(options);
118+
const persistent = validateBrowserContextOptions(options);
120119
const logger = new RootLogger(options.logger);
121120
const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, logger, persistent, userDataDir), logger, TimeoutSettings.timeout(options));
122121
return browser._defaultContext!;
@@ -179,8 +178,16 @@ export abstract class BrowserTypeBase implements BrowserType {
179178
handleSIGHUP = true,
180179
} = options;
181180

182-
const downloadsPath = await mkdtempAsync(DOWNLOADS_FOLDER);
183-
const tempDirectories = [downloadsPath];
181+
const tempDirectories = [];
182+
let downloadsPath: string;
183+
if (options.downloadsPath) {
184+
downloadsPath = options.downloadsPath;
185+
await mkdirAsync(options.downloadsPath, { recursive: true });
186+
} else {
187+
downloadsPath = await mkdtempAsync(DOWNLOADS_FOLDER);
188+
tempDirectories.push(downloadsPath);
189+
}
190+
184191
if (!userDataDir) {
185192
userDataDir = await mkdtempAsync(path.join(os.tmpdir(), `playwright_${this._name}dev_profile-`));
186193
tempDirectories.push(userDataDir);
@@ -239,7 +246,7 @@ export abstract class BrowserTypeBase implements BrowserType {
239246
return { browserServer, downloadsPath, transport };
240247
}
241248

242-
abstract _defaultArgs(options: BrowserArgOptions, isPersistent: boolean, userDataDir: string): string[];
249+
abstract _defaultArgs(options: LaunchOptionsBase, isPersistent: boolean, userDataDir: string): string[];
243250
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>;
244251
abstract _wrapTransportWithWebSocket(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketWrapper;
245252
abstract _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env;

src/server/chromium.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { CRBrowser } from '../chromium/crBrowser';
2121
import * as ws from 'ws';
2222
import { Env } from './processLauncher';
2323
import { kBrowserCloseMessageId } from '../chromium/crConnection';
24-
import { BrowserArgOptions, BrowserTypeBase, processBrowserArgOptions } from './browserType';
24+
import { LaunchOptionsBase, BrowserTypeBase, processBrowserArgOptions } from './browserType';
2525
import { WebSocketWrapper } from './browserServer';
2626
import { ConnectionTransport, ProtocolRequest } from '../transport';
2727
import { InnerLogger, logError } from '../logger';
@@ -77,7 +77,7 @@ export class Chromium extends BrowserTypeBase {
7777
return wrapTransportWithWebSocket(transport, logger, port);
7878
}
7979

80-
_defaultArgs(options: BrowserArgOptions, isPersistent: boolean, userDataDir: string): string[] {
80+
_defaultArgs(options: LaunchOptionsBase, isPersistent: boolean, userDataDir: string): string[] {
8181
const { devtools, headless } = processBrowserArgOptions(options);
8282
const { args = [], proxy } = options;
8383
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));

src/server/firefox.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { FFBrowser } from '../firefox/ffBrowser';
2323
import { kBrowserCloseMessageId } from '../firefox/ffConnection';
2424
import { helper } from '../helper';
2525
import { WebSocketWrapper } from './browserServer';
26-
import { BrowserArgOptions, BrowserTypeBase, processBrowserArgOptions, FirefoxUserPrefsOptions } from './browserType';
26+
import { LaunchOptionsBase, BrowserTypeBase, processBrowserArgOptions, FirefoxUserPrefsOptions } from './browserType';
2727
import { Env } from './processLauncher';
2828
import { ConnectionTransport, SequenceNumberMixer } from '../transport';
2929
import { InnerLogger, logError } from '../logger';
@@ -57,7 +57,7 @@ export class Firefox extends BrowserTypeBase {
5757
return wrapTransportWithWebSocket(transport, logger, port);
5858
}
5959

60-
_defaultArgs(options: BrowserArgOptions & FirefoxUserPrefsOptions, isPersistent: boolean, userDataDir: string): string[] {
60+
_defaultArgs(options: LaunchOptionsBase & FirefoxUserPrefsOptions, isPersistent: boolean, userDataDir: string): string[] {
6161
const { devtools, headless } = processBrowserArgOptions(options);
6262
const { args = [], proxy } = options;
6363
if (devtools)

src/server/processLauncher.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,9 @@ import { Log } from '../logger';
2020
import * as readline from 'readline';
2121
import * as removeFolder from 'rimraf';
2222
import * as stream from 'stream';
23-
import * as util from 'util';
2423
import { helper } from '../helper';
2524
import { Progress } from '../progress';
2625

27-
const removeFolderAsync = util.promisify(removeFolder);
28-
2926
export const browserLog: Log = {
3027
name: 'browser',
3128
};
@@ -67,11 +64,7 @@ type LaunchResult = {
6764
};
6865

6966
export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> {
70-
const cleanup = async () => {
71-
await Promise.all(options.tempDirectories.map(dir => {
72-
return removeFolderAsync(dir).catch((err: Error) => console.error(err));
73-
}));
74-
};
67+
const cleanup = () => helper.removeFolders(options.tempDirectories);
7568

7669
const progress = options.progress;
7770
const stdio: ('ignore' | 'pipe')[] = options.pipe ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'];

0 commit comments

Comments
 (0)