1
1
import * as os from 'os' ;
2
2
import * as uniqueFilename from 'unique-filename' ;
3
3
import * as puppeteer from 'puppeteer' ;
4
+ import * as chokidar from 'chokidar' ;
5
+ import * as path from 'path' ;
6
+ import * as fs from 'fs' ;
4
7
import { Logger } from '../logger' ;
5
8
import { RenderingConfig } from '../config' ;
6
9
@@ -23,10 +26,26 @@ export interface RenderOptions {
23
26
headers ?: HTTPHeaders ;
24
27
}
25
28
29
+ export interface RenderCSVOptions {
30
+ url : string ;
31
+ filePath : string ;
32
+ timeout : string | number ;
33
+ renderKey : string ;
34
+ domain : string ;
35
+ timezone ?: string ;
36
+ encoding ?: string ;
37
+ headers ?: HTTPHeaders ;
38
+ }
39
+
26
40
export interface RenderResponse {
27
41
filePath : string ;
28
42
}
29
43
44
+ export interface RenderCSVResponse {
45
+ filePath : string ;
46
+ fileName ?: string ;
47
+ }
48
+
30
49
export class Browser {
31
50
constructor ( protected config : RenderingConfig , protected log : Logger ) {
32
51
this . log . debug ( 'Browser initialized' , 'config' , this . config ) ;
@@ -48,15 +67,31 @@ export class Browser {
48
67
49
68
async start ( ) : Promise < void > { }
50
69
51
- validateOptions ( options : RenderOptions ) {
70
+ validateRenderOptions ( options : RenderOptions | RenderCSVOptions ) {
52
71
if ( options . url . startsWith ( `socket://` ) ) {
53
72
// Puppeteer doesn't support socket:// URLs
54
73
throw new Error ( `Image rendering in socket mode is not supported` ) ;
55
74
}
56
75
76
+ options . headers = options . headers || { } ;
77
+ const headers = { } ;
78
+
79
+ if ( options . headers [ 'Accept-Language' ] ) {
80
+ headers [ 'Accept-Language' ] = options . headers [ 'Accept-Language' ] ;
81
+ } else if ( this . config . acceptLanguage ) {
82
+ headers [ 'Accept-Language' ] = this . config . acceptLanguage ;
83
+ }
84
+
85
+ options . headers = headers ;
86
+
87
+ options . timeout = parseInt ( options . timeout as string , 10 ) || 30 ;
88
+ }
89
+
90
+ validateImageOptions ( options : RenderOptions ) {
91
+ this . validateRenderOptions ( options ) ;
92
+
57
93
options . width = parseInt ( options . width as string , 10 ) || this . config . width ;
58
94
options . height = parseInt ( options . height as string , 10 ) || this . config . height ;
59
- options . timeout = parseInt ( options . timeout as string , 10 ) || 30 ;
60
95
61
96
if ( options . width < 10 ) {
62
97
options . width = this . config . width ;
@@ -79,17 +114,6 @@ export class Browser {
79
114
if ( options . deviceScaleFactor > this . config . maxDeviceScaleFactor ) {
80
115
options . deviceScaleFactor = this . config . deviceScaleFactor ;
81
116
}
82
-
83
- options . headers = options . headers || { } ;
84
- const headers = { } ;
85
-
86
- if ( options . headers [ 'Accept-Language' ] ) {
87
- headers [ 'Accept-Language' ] = options . headers [ 'Accept-Language' ] ;
88
- } else if ( this . config . acceptLanguage ) {
89
- headers [ 'Accept-Language' ] = this . config . acceptLanguage ;
90
- }
91
-
92
- options . headers = headers ;
93
117
}
94
118
95
119
getLauncherOptions ( options ) {
@@ -111,12 +135,28 @@ export class Browser {
111
135
return launcherOptions ;
112
136
}
113
137
138
+ async preparePage ( page : any , options : any ) {
139
+ if ( this . config . verboseLogging ) {
140
+ this . log . debug ( 'Setting cookie for page' , 'renderKey' , options . renderKey , 'domain' , options . domain ) ;
141
+ }
142
+ await page . setCookie ( {
143
+ name : 'renderKey' ,
144
+ value : options . renderKey ,
145
+ domain : options . domain ,
146
+ } ) ;
147
+
148
+ if ( options . headers && Object . keys ( options . headers ) . length > 0 ) {
149
+ this . log . debug ( `Setting extra HTTP headers for page` , 'headers' , options . headers ) ;
150
+ await page . setExtraHTTPHeaders ( options . headers ) ;
151
+ }
152
+ }
153
+
114
154
async render ( options : RenderOptions ) : Promise < RenderResponse > {
115
155
let browser ;
116
156
let page : any ;
117
157
118
158
try {
119
- this . validateOptions ( options ) ;
159
+ this . validateImageOptions ( options ) ;
120
160
const launcherOptions = this . getLauncherOptions ( options ) ;
121
161
browser = await puppeteer . launch ( launcherOptions ) ;
122
162
page = await browser . newPage ( ) ;
@@ -152,19 +192,7 @@ export class Browser {
152
192
deviceScaleFactor : options . deviceScaleFactor ,
153
193
} ) ;
154
194
155
- if ( this . config . verboseLogging ) {
156
- this . log . debug ( 'Setting cookie for page' , 'renderKey' , options . renderKey , 'domain' , options . domain ) ;
157
- }
158
- await page . setCookie ( {
159
- name : 'renderKey' ,
160
- value : options . renderKey ,
161
- domain : options . domain ,
162
- } ) ;
163
-
164
- if ( options . headers && Object . keys ( options . headers ) . length > 0 ) {
165
- this . log . debug ( `Setting extra HTTP headers for page` , 'headers' , options . headers ) ;
166
- await page . setExtraHTTPHeaders ( options . headers ) ;
167
- }
195
+ await this . preparePage ( page , options ) ;
168
196
169
197
if ( this . config . verboseLogging ) {
170
198
this . log . debug ( 'Moving mouse on page' , 'x' , options . width , 'y' , options . height ) ;
@@ -202,6 +230,78 @@ export class Browser {
202
230
return { filePath : options . filePath } ;
203
231
}
204
232
233
+ async renderCSV ( options : RenderCSVOptions ) : Promise < RenderCSVResponse > {
234
+ let browser ;
235
+ let page : any ;
236
+
237
+ try {
238
+ this . validateRenderOptions ( options ) ;
239
+ const launcherOptions = this . getLauncherOptions ( options ) ;
240
+ browser = await puppeteer . launch ( launcherOptions ) ;
241
+ page = await browser . newPage ( ) ;
242
+ this . addPageListeners ( page ) ;
243
+
244
+ return await this . exportCSV ( page , options ) ;
245
+ } finally {
246
+ if ( page ) {
247
+ this . removePageListeners ( page ) ;
248
+ await page . close ( ) ;
249
+ }
250
+ if ( browser ) {
251
+ await browser . close ( ) ;
252
+ }
253
+ }
254
+ }
255
+
256
+ async exportCSV ( page : any , options : any ) : Promise < RenderCSVResponse > {
257
+ await this . preparePage ( page , options ) ;
258
+
259
+ const downloadPath = uniqueFilename ( os . tmpdir ( ) ) ;
260
+ fs . mkdirSync ( downloadPath ) ;
261
+ const watcher = chokidar . watch ( downloadPath ) ;
262
+ let downloadFilePath = '' ;
263
+ watcher . on ( 'add' , file => {
264
+ if ( ! file . endsWith ( '.crdownload' ) ) {
265
+ downloadFilePath = file ;
266
+ }
267
+ } ) ;
268
+
269
+ await page . _client . send ( 'Page.setDownloadBehavior' , { behavior : 'allow' , downloadPath : downloadPath } ) ;
270
+
271
+ if ( this . config . verboseLogging ) {
272
+ this . log . debug ( 'Navigating and waiting for all network requests to finish' , 'url' , options . url ) ;
273
+ }
274
+
275
+ await page . goto ( options . url , { waitUntil : 'networkidle0' , timeout : options . timeout * 1000 } ) ;
276
+
277
+ if ( this . config . verboseLogging ) {
278
+ this . log . debug ( 'Waiting for download to end' ) ;
279
+ }
280
+
281
+ const startDate = Date . now ( ) ;
282
+ while ( Date . now ( ) - startDate <= options . timeout * 1000 ) {
283
+ if ( downloadFilePath !== '' ) {
284
+ break ;
285
+ }
286
+ await new Promise ( resolve => setTimeout ( resolve , 500 ) ) ;
287
+ }
288
+
289
+ if ( downloadFilePath === '' ) {
290
+ throw new Error ( `Timeout exceeded while waiting for download to end` ) ;
291
+ }
292
+
293
+ await watcher . close ( ) ;
294
+
295
+ let filePath = downloadFilePath ;
296
+ if ( options . filePath ) {
297
+ fs . renameSync ( downloadFilePath , options . filePath ) ;
298
+ filePath = options . filePath ;
299
+ fs . rmdirSync ( path . dirname ( downloadFilePath ) ) ;
300
+ }
301
+
302
+ return { filePath, fileName : path . basename ( downloadFilePath ) } ;
303
+ }
304
+
205
305
addPageListeners ( page : any ) {
206
306
page . on ( 'error' , this . logError ) ;
207
307
page . on ( 'pageerror' , this . logPageError ) ;
0 commit comments