-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun.go
More file actions
733 lines (670 loc) · 22.1 KB
/
Copy pathrun.go
File metadata and controls
733 lines (670 loc) · 22.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
package patchright
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
const (
patchrightCliVersion = "1.61.1"
// nodeVersion is the Node.js runtime downloaded alongside the driver when no
// PATCHRIGHT_NODEJS_PATH is provided. It is kept in line with the Node.js
// version upstream Playwright bundles in its own driver.
nodeVersion = "24.18.0"
// defaultNpmRegistry serves the platform-independent patchright package.
// Override with the PATCHRIGHT_NPM_REGISTRY environment variable.
defaultNpmRegistry = "https://registry.npmjs.org"
// defaultNodejsDistHost serves the per-platform Node.js binaries.
// Override with the NODE_MIRROR environment variable (nvm/n convention).
defaultNodejsDistHost = "https://nodejs.org/dist"
)
var logger = slog.Default()
// PatchrightDriver wraps the Patchright CLI (patched Playwright).
//
// It's required for patchright-go to work.
type PatchrightDriver struct {
Version string
options *RunOptions
}
func NewDriver(options ...*RunOptions) (*PatchrightDriver, error) {
transformed, err := transformRunOptions(options...) // get default values
if err != nil {
return nil, err
}
return &PatchrightDriver{
options: transformed,
Version: transformed.Version,
}, nil
}
func getDefaultCacheDirectory() (string, error) {
userHomeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("could not get user home directory: %w", err)
}
switch runtime.GOOS {
case "windows":
return filepath.Join(userHomeDir, "AppData", "Local"), nil
case "darwin":
return filepath.Join(userHomeDir, "Library", "Caches"), nil
case "linux":
return filepath.Join(userHomeDir, ".cache"), nil
}
return "", errors.New("could not determine cache directory")
}
func (d *PatchrightDriver) isUpToDateDriver() (bool, error) {
if _, err := os.Stat(d.options.DriverDirectory); os.IsNotExist(err) {
if err := os.MkdirAll(d.options.DriverDirectory, 0o777); err != nil {
return false, fmt.Errorf("could not create driver directory: %w", err)
}
}
if _, err := os.Stat(getDriverCliJs(d.options)); os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("could not check if driver is up2date: %w", err)
}
cmd := d.Command("--version")
output, err := cmd.Output()
if err != nil {
return false, fmt.Errorf("could not run driver: %w", err)
}
if bytes.Contains(output, []byte(d.Version)) {
return true, nil
}
// avoid triggering downloads and accidentally overwriting files
return false, fmt.Errorf("driver exists but version not %s in : %s", d.Version, d.options.DriverDirectory)
}
// Command returns an exec.Cmd for the driver.
func (d *PatchrightDriver) Command(arg ...string) *exec.Cmd {
cmd := exec.Command(getNodeExecutable(d.options), append([]string{getDriverCliJs(d.options)}, arg...)...)
cmd.SysProcAttr = defaultSysProcAttr
if d.options.BrowsersPath != "" {
cmd.Env = append(os.Environ(), "PLAYWRIGHT_BROWSERS_PATH="+d.options.BrowsersPath)
}
return cmd
}
// Install downloads the driver and the browsers depending on [RunOptions].
func (d *PatchrightDriver) Install() error {
if err := d.DownloadDriver(); err != nil {
return fmt.Errorf("could not install driver: %w", err)
}
if d.options.SkipInstallBrowsers {
return nil
}
d.log("Downloading browsers...")
if err := d.installBrowsers(); err != nil {
return fmt.Errorf("could not install browsers: %w", err)
}
d.log("Downloaded browsers successfully")
return nil
}
// Uninstall removes the driver and the browsers.
func (d *PatchrightDriver) Uninstall() error {
d.log("Removing browsers...")
if err := d.uninstallBrowsers(); err != nil {
return fmt.Errorf("could not uninstall browsers: %w", err)
}
d.log("Removing driver...")
if err := os.RemoveAll(d.options.DriverDirectory); err != nil {
return fmt.Errorf("could not remove driver directory: %w", err)
}
d.log("Uninstall driver successfully")
return nil
}
// DownloadDriver downloads the driver only.
//
// The driver is assembled from two upstream sources:
// - the platform-independent patchright package from the npm registry,
// extracted into <DriverDirectory>/package (this contains cli.js); and
// - the matching per-platform Node.js binary from nodejs.org, placed at
// <DriverDirectory>/node[.exe].
//
// When PATCHRIGHT_NODEJS_PATH is set the Node.js download is skipped and the
// preinstalled Node.js is used instead, which also covers platforms for which
// nodejs.org has no prebuilt binary (e.g. linux/arm).
func (d *PatchrightDriver) DownloadDriver() error {
up2Date, err := d.isUpToDateDriver()
if err != nil {
return err
}
if up2Date {
return d.patchDriverBundle()
}
d.log("Downloading driver", "path", d.options.DriverDirectory)
if err := d.downloadPatchrightPackage(); err != nil {
return err
}
if err := d.downloadPatchrightCore(); err != nil {
return err
}
if err := d.downloadNode(); err != nil {
return err
}
d.log("Downloaded driver successfully")
return d.patchDriverBundle()
}
// downloadPatchrightPackage downloads the platform-independent patchright
// package from the npm registry and extracts its "package/" contents into the
// driver directory, so that <DriverDirectory>/package/cli.js exists.
func (d *PatchrightDriver) downloadPatchrightPackage() error {
url := fmt.Sprintf("%s/patchright/-/patchright-%s.tgz", npmRegistry(d.options), d.Version)
body, err := downloadWithRetry(url)
if err != nil {
return fmt.Errorf("could not download patchright: %w", err)
}
gzReader, err := gzip.NewReader(bytes.NewReader(body))
if err != nil {
return fmt.Errorf("could not read patchright archive: %w", err)
}
defer gzReader.Close() //nolint:errcheck
tarReader := tar.NewReader(gzReader)
extracted := false
for {
header, err := tarReader.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return fmt.Errorf("could not read patchright archive: %w", err)
}
// npm tarballs nest everything under a top-level "package/" directory,
// which is exactly the layout the driver expects on disk.
if header.Typeflag != tar.TypeReg || !strings.HasPrefix(header.Name, "package/") {
continue
}
diskPath, err := safeJoin(d.options.DriverDirectory, header.Name)
if err != nil {
return err
}
if err := writeFileFromReader(diskPath, tarReader, header.FileInfo().Mode()); err != nil {
return err
}
extracted = true
}
if !extracted {
return fmt.Errorf("no files extracted from patchright %s", d.Version)
}
return nil
}
// downloadPatchrightCore downloads patchright-core, the actual driver
// implementation, and installs it as a node_module within the patchright
// package so that require('patchright-core') resolves correctly.
func (d *PatchrightDriver) downloadPatchrightCore() error {
url := fmt.Sprintf("%s/patchright-core/-/patchright-core-%s.tgz", npmRegistry(d.options), d.Version)
body, err := downloadWithRetry(url)
if err != nil {
return fmt.Errorf("could not download patchright-core: %w", err)
}
gzReader, err := gzip.NewReader(bytes.NewReader(body))
if err != nil {
return fmt.Errorf("could not read patchright-core archive: %w", err)
}
defer gzReader.Close() //nolint:errcheck
// Extract into <DriverDirectory>/package/node_modules/patchright-core/
// so that require('patchright-core') from the patchright package resolves.
nodeModulesDir := filepath.Join(d.options.DriverDirectory, "package", "node_modules", "patchright-core")
tarReader := tar.NewReader(gzReader)
extracted := false
for {
header, err := tarReader.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return fmt.Errorf("could not read patchright-core archive: %w", err)
}
if header.Typeflag != tar.TypeReg || !strings.HasPrefix(header.Name, "package/") {
continue
}
relativePath := strings.TrimPrefix(header.Name, "package/")
diskPath, err := safeJoin(nodeModulesDir, relativePath)
if err != nil {
return err
}
if err := writeFileFromReader(diskPath, tarReader, header.FileInfo().Mode()); err != nil {
return err
}
extracted = true
}
if !extracted {
return fmt.Errorf("no files extracted from patchright-core %s", d.Version)
}
return nil
}
// downloadNode downloads the per-platform Node.js binary. It is a no-op when
// NodeJSPath is set or PATCHRIGHT_NODEJS_PATH env var is set.
func (d *PatchrightDriver) downloadNode() error {
if d.options.NodeJSPath != "" {
d.log("Skipping Node.js download, using provided NodeJSPath")
return nil
}
if os.Getenv("PATCHRIGHT_NODEJS_PATH") != "" {
d.log("Skipping Node.js download, using PATCHRIGHT_NODEJS_PATH")
return nil
}
suffix, err := nodePlatformSuffix()
if err != nil {
return err
}
archiveDir := fmt.Sprintf("node-v%s-%s", nodeVersion, suffix)
isWindows := runtime.GOOS == "windows"
ext := "tar.gz"
if isWindows {
ext = "zip"
}
url := fmt.Sprintf("%s/v%s/%s.%s", nodejsMirror(d.options), nodeVersion, archiveDir, ext)
body, err := downloadWithRetry(url)
if err != nil {
return fmt.Errorf("could not download Node.js: %w", err)
}
node := "node"
if isWindows {
node = "node.exe"
}
nodeDiskPath := filepath.Join(d.options.DriverDirectory, node)
if isWindows {
return extractZipEntry(body, archiveDir+"/node.exe", nodeDiskPath)
}
return extractTarGzEntry(body, archiveDir+"/bin/node", nodeDiskPath)
}
func (d *PatchrightDriver) patchDriverBundle() error {
coreBundlePath := filepath.Join(d.options.DriverDirectory, "package", "lib", "coreBundle.js")
data, err := os.ReadFile(coreBundlePath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("could not read driver bundle: %w", err)
}
replacements := map[string]string{
"pageError.location.url": `pageError.location?.url || ""`,
"pageError.location.lineNumber": "pageError.location?.lineNumber || 0",
"pageError.location.columnNumber": "pageError.location?.columnNumber || 0",
}
changed := false
for original, patched := range replacements {
originalBytes := []byte(original)
patchedBytes := []byte(patched)
if bytes.Contains(data, originalBytes) {
data = bytes.ReplaceAll(data, originalBytes, patchedBytes)
changed = true
}
}
if !changed {
alreadyPatched := true
for _, patched := range replacements {
if !bytes.Contains(data, []byte(patched)) {
alreadyPatched = false
break
}
}
if alreadyPatched {
return nil
}
return fmt.Errorf("could not patch driver bundle: pageError location pattern not found")
}
if err := os.WriteFile(coreBundlePath, data, 0o644); err != nil {
return fmt.Errorf("could not write patched driver bundle: %w", err)
}
return nil
}
func (d *PatchrightDriver) log(msg string, args ...any) {
if d.options.Verbose {
d.options.Logger.Info(msg, args...)
}
}
func (d *PatchrightDriver) run() (*connection, error) {
transport, err := newPipeTransport(d, d.options.Stderr)
if err != nil {
return nil, err
}
connection := newConnection(transport)
return connection, nil
}
func (d *PatchrightDriver) installBrowsers() error {
additionalArgs := []string{"install"}
if d.options.Browsers != nil {
additionalArgs = append(additionalArgs, d.options.Browsers...)
}
if d.options.OnlyInstallShell {
additionalArgs = append(additionalArgs, "--only-shell")
}
if d.options.NoInstallShell {
additionalArgs = append(additionalArgs, "--no-shell")
}
if d.options.DryRun {
additionalArgs = append(additionalArgs, "--dry-run")
}
if d.options.WithDeps {
additionalArgs = append(additionalArgs, "--with-deps")
}
cmd := d.Command(additionalArgs...)
cmd.Stdout = d.options.Stdout
cmd.Stderr = d.options.Stderr
return cmd.Run()
}
func (d *PatchrightDriver) uninstallBrowsers() error {
cmd := d.Command("uninstall")
cmd.Stdout = d.options.Stdout
cmd.Stderr = d.options.Stderr
return cmd.Run()
}
// RunOptions are custom options to run the driver
type RunOptions struct {
// DriverDirectory is the path to the patchright driver directory.
// Falls back to PATCHRIGHT_DRIVER_PATH env var, then <cwd>/bin/patchright-driver.
DriverDirectory string
// NodeJSPath overrides the Node.js binary path. Falls back to
// PATCHRIGHT_NODEJS_PATH env var. Required on platforms without a prebuilt
// Node.js binary (e.g. linux/arm).
NodeJSPath string
// CLIPath overrides the cli.js path. Falls back to PATCHRIGHT_CLI_PATH
// env var. Useful for custom driver layouts.
CLIPath string
// Version overrides the patchright driver version to download and use.
// When empty, the library's built-in default version is used.
Version string
// NpmRegistry overrides the npm registry URL for downloading the patchright
// package. Falls back to PATCHRIGHT_NPM_REGISTRY env var. Default:
// https://registry.npmjs.org
NpmRegistry string
// NodeMirror overrides the Node.js distribution mirror URL. Falls back to
// NODE_MIRROR env var. Default: https://nodejs.org/dist
NodeMirror string
// BrowsersPath overrides where browsers are downloaded to.
// Falls back to PLAYWRIGHT_BROWSERS_PATH env var, then the Playwright
// default (~/.cache/ms-playwright on Linux).
BrowsersPath string
// OnlyInstallShell only downloads the headless shell. (For chromium browsers only)
OnlyInstallShell bool
// NoInstallShell does not install chromium headless shell. (For chromium browsers only)
NoInstallShell bool
SkipInstallBrowsers bool
// Browsers to install. Patchright only supports chromium-based browsers.
// If not set and SkipInstallBrowsers is false, will download chromium.
Browsers []string
// install system dependencies for browsers
WithDeps bool
Verbose bool // default true
Stdout io.Writer
Stderr io.Writer
Logger *slog.Logger
// DryRun does not install browser/dependencies. It will only print information.
DryRun bool
}
// Install downloads the driver and the browsers depending on [RunOptions].
//
// It is idempotent: if the driver and browsers are already present and
// up-to-date, the call is a fast no-op.
func Install(options ...*RunOptions) error {
driver, err := NewDriver(options...)
if err != nil {
return fmt.Errorf("could not get driver instance: %w", err)
}
if err := driver.Install(); err != nil {
return fmt.Errorf("could not install driver: %w", err)
}
return nil
}
// Run starts a Patchright instance.
//
// If the driver (or browsers) are not yet installed in [RunOptions.DriverDirectory],
// they are downloaded automatically. Subsequent calls with an up-to-date
// driver skip the download.
func Run(options ...*RunOptions) (*Patchright, error) {
driver, err := NewDriver(options...)
if err != nil {
return nil, fmt.Errorf("could not get driver instance: %w", err)
}
if err := driver.Install(); err != nil {
return nil, fmt.Errorf("could not install driver: %w", err)
}
connection, err := driver.run()
if err != nil {
return nil, err
}
playwright, err := connection.Start()
return playwright, err
}
func transformRunOptions(options ...*RunOptions) (*RunOptions, error) {
option := RunOptions{
Verbose: true,
}
if len(options) == 1 {
option = *options[0]
}
if option.OnlyInstallShell && option.NoInstallShell {
return nil, fmt.Errorf("OnlyInstallShell and NoInstallShell cannot be set at the same time")
}
if option.Version == "" {
option.Version = patchrightCliVersion
}
if option.DriverDirectory == "" {
option.DriverDirectory = os.Getenv("PATCHRIGHT_DRIVER_PATH")
}
if option.DriverDirectory == "" {
cwd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("could not get working directory: %w", err)
}
option.DriverDirectory = filepath.Join(cwd, "bin", "patchright-driver")
}
if option.Stdout == nil {
option.Stdout = os.Stdout
}
if option.Stderr == nil {
option.Stderr = os.Stderr
}
if option.Logger == nil {
option.Logger = slog.New(slog.NewTextHandler(option.Stderr, nil))
}
return &option, nil
}
func getNodeExecutable(options *RunOptions) string {
if options.NodeJSPath != "" {
return options.NodeJSPath
}
if envPath := os.Getenv("PATCHRIGHT_NODEJS_PATH"); envPath != "" {
return envPath
}
node := "node"
if runtime.GOOS == "windows" {
node = "node.exe"
}
return filepath.Join(options.DriverDirectory, node)
}
func getDriverCliJs(options *RunOptions) string {
if options.CLIPath != "" {
return options.CLIPath
}
if envPath := os.Getenv("PATCHRIGHT_CLI_PATH"); envPath != "" {
return envPath
}
return filepath.Join(options.DriverDirectory, "package", "cli.js")
}
func npmRegistry(options *RunOptions) string {
if options.NpmRegistry != "" {
return strings.TrimRight(options.NpmRegistry, "/")
}
if host := os.Getenv("PATCHRIGHT_NPM_REGISTRY"); host != "" {
return strings.TrimRight(host, "/")
}
return defaultNpmRegistry
}
func nodejsMirror(options *RunOptions) string {
if options.NodeMirror != "" {
return strings.TrimRight(options.NodeMirror, "/")
}
if host := os.Getenv("NODE_MIRROR"); host != "" {
return strings.TrimRight(host, "/")
}
return defaultNodejsDistHost
}
// nodePlatformSuffix maps the current GOOS/GOARCH to the suffix nodejs.org uses
// in its release archive names (e.g. "linux-x64", "darwin-arm64", "win-x64").
// Platforms without a prebuilt Node.js binary (such as linux/arm, 32-bit ARM)
// return an actionable error pointing at PATCHRIGHT_NODEJS_PATH.
func nodePlatformSuffix() (string, error) {
var os_ string
switch runtime.GOOS {
case "windows":
os_ = "win"
case "darwin":
os_ = "darwin"
case "linux":
os_ = "linux"
default:
return "", unsupportedNodePlatformError()
}
var arch string
switch runtime.GOARCH {
case "amd64":
arch = "x64"
case "arm64":
arch = "arm64"
default:
// Notably linux/arm (32-bit, e.g. Raspberry Pi armv7l): nodejs.org no
// longer ships a prebuilt binary, so we cannot download one.
return "", unsupportedNodePlatformError()
}
return fmt.Sprintf("%s-%s", os_, arch), nil
}
func unsupportedNodePlatformError() error {
return fmt.Errorf("no prebuilt Node.js %s is available for %s/%s; "+
"install Node.js yourself and set PATCHRIGHT_NODEJS_PATH to its path",
nodeVersion, runtime.GOOS, runtime.GOARCH)
}
// safeJoin joins an archive entry name onto root, guarding against path
// traversal ("zip slip"/"tar slip"): the resulting path must stay within root.
// This matters because PATCHRIGHT_NPM_REGISTRY / NODE_MIRROR allow arbitrary
// mirrors, so archive contents are not fully trusted.
func safeJoin(root, name string) (string, error) {
diskPath := filepath.Join(root, filepath.FromSlash(name))
prefix := filepath.Clean(root) + string(os.PathSeparator)
if diskPath != filepath.Clean(root) && !strings.HasPrefix(diskPath, prefix) {
return "", fmt.Errorf("invalid path in archive: %s", name)
}
return diskPath, nil
}
// writeFileFromReader writes the contents of r to diskPath, creating parent
// directories as needed and preserving the executable bit on non-Windows hosts.
func writeFileFromReader(diskPath string, r io.Reader, mode os.FileMode) error {
if err := os.MkdirAll(filepath.Dir(diskPath), 0o777); err != nil {
return fmt.Errorf("could not create directory: %w", err)
}
outFile, err := os.Create(diskPath)
if err != nil {
return fmt.Errorf("could not create file: %w", err)
}
if _, err := io.Copy(outFile, r); err != nil {
outFile.Close() //nolint:errcheck
return fmt.Errorf("could not write file: %w", err)
}
if err := outFile.Close(); err != nil {
return fmt.Errorf("could not close file: %w", err)
}
if mode.Perm()&0o100 != 0 && runtime.GOOS != "windows" {
if err := makeFileExecutable(diskPath); err != nil {
return err
}
}
return nil
}
// extractTarGzEntry extracts a single named entry from a gzipped tar archive to
// diskPath and marks it executable.
func extractTarGzEntry(archive []byte, entryName, diskPath string) error {
gzReader, err := gzip.NewReader(bytes.NewReader(archive))
if err != nil {
return fmt.Errorf("could not read archive: %w", err)
}
defer gzReader.Close() //nolint:errcheck
tarReader := tar.NewReader(gzReader)
for {
header, err := tarReader.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return fmt.Errorf("could not read archive: %w", err)
}
if header.Name != entryName {
continue
}
// Force the executable bit: the node binary must be runnable.
return writeFileFromReader(diskPath, tarReader, header.FileInfo().Mode()|0o100)
}
return fmt.Errorf("could not find %s in archive", entryName)
}
// extractZipEntry extracts a single named entry from a zip archive to diskPath.
func extractZipEntry(archive []byte, entryName, diskPath string) error {
zipReader, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive)))
if err != nil {
return fmt.Errorf("could not read archive: %w", err)
}
for _, file := range zipReader.File {
if file.Name != entryName {
continue
}
rc, err := file.Open()
if err != nil {
return fmt.Errorf("could not open zip entry: %w", err)
}
err = writeFileFromReader(diskPath, rc, file.Mode())
rc.Close() //nolint:errcheck
return err
}
return fmt.Errorf("could not find %s in archive", entryName)
}
func makeFileExecutable(path string) error {
stats, err := os.Stat(path)
if err != nil {
return fmt.Errorf("could not stat driver: %w", err)
}
if err := os.Chmod(path, stats.Mode()|0x40); err != nil {
return fmt.Errorf("could not set permissions: %w", err)
}
return nil
}
// downloadWithRetry downloads url, retrying a few times on transient failures.
// It does not retry client errors (4xx), which are not transient.
func downloadWithRetry(url string) ([]byte, error) {
var lastErr error
for attempt := 1; attempt <= 3; attempt++ {
body, retryable, err := download(url)
if err == nil {
return body, nil
}
lastErr = err
if !retryable {
break
}
}
return nil, lastErr
}
// download fetches url. The returned bool reports whether a failure is worth
// retrying (network errors and 5xx are; 4xx are not).
func download(url string) ([]byte, bool, error) {
resp, err := http.Get(url)
if err != nil {
return nil, true, fmt.Errorf("could not download from %s: %w", url, err)
}
defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode != http.StatusOK {
retryable := resp.StatusCode >= 500
return nil, retryable, fmt.Errorf("got non 200 status code: %d (%s) from %s", resp.StatusCode, resp.Status, url)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, true, fmt.Errorf("could not read response body: %w", err)
}
return body, false, nil
}