tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Cache.ts (6673B)


      1 /**
      2 * @license
      3 * Copyright 2023 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import fs from 'node:fs';
      8 import os from 'node:os';
      9 import path from 'node:path';
     10 
     11 import debug from 'debug';
     12 
     13 import {
     14  Browser,
     15  type BrowserPlatform,
     16  executablePathByBrowser,
     17  getVersionComparator,
     18 } from './browser-data/browser-data.js';
     19 import {detectBrowserPlatform} from './detectPlatform.js';
     20 
     21 const debugCache = debug('puppeteer:browsers:cache');
     22 
     23 /**
     24 * @public
     25 */
     26 export class InstalledBrowser {
     27  browser: Browser;
     28  buildId: string;
     29  platform: BrowserPlatform;
     30  readonly executablePath: string;
     31 
     32  #cache: Cache;
     33 
     34  /**
     35   * @internal
     36   */
     37  constructor(
     38    cache: Cache,
     39    browser: Browser,
     40    buildId: string,
     41    platform: BrowserPlatform,
     42  ) {
     43    this.#cache = cache;
     44    this.browser = browser;
     45    this.buildId = buildId;
     46    this.platform = platform;
     47    this.executablePath = cache.computeExecutablePath({
     48      browser,
     49      buildId,
     50      platform,
     51    });
     52  }
     53 
     54  /**
     55   * Path to the root of the installation folder. Use
     56   * {@link computeExecutablePath} to get the path to the executable binary.
     57   */
     58  get path(): string {
     59    return this.#cache.installationDir(
     60      this.browser,
     61      this.platform,
     62      this.buildId,
     63    );
     64  }
     65 
     66  readMetadata(): Metadata {
     67    return this.#cache.readMetadata(this.browser);
     68  }
     69 
     70  writeMetadata(metadata: Metadata): void {
     71    this.#cache.writeMetadata(this.browser, metadata);
     72  }
     73 }
     74 
     75 /**
     76 * @internal
     77 */
     78 export interface ComputeExecutablePathOptions {
     79  /**
     80   * Determines which platform the browser will be suited for.
     81   *
     82   * @defaultValue **Auto-detected.**
     83   */
     84  platform?: BrowserPlatform;
     85  /**
     86   * Determines which browser to launch.
     87   */
     88  browser: Browser;
     89  /**
     90   * Determines which buildId to download. BuildId should uniquely identify
     91   * binaries and they are used for caching.
     92   */
     93  buildId: string;
     94 }
     95 
     96 export interface Metadata {
     97  // Maps an alias (canary/latest/dev/etc.) to a buildId.
     98  aliases: Record<string, string>;
     99 }
    100 
    101 /**
    102 * The cache used by Puppeteer relies on the following structure:
    103 *
    104 * - rootDir
    105 *   -- <browser1> | browserRoot(browser1)
    106 *   ---- <platform>-<buildId> | installationDir()
    107 *   ------ the browser-platform-buildId
    108 *   ------ specific structure.
    109 *   -- <browser2> | browserRoot(browser2)
    110 *   ---- <platform>-<buildId> | installationDir()
    111 *   ------ the browser-platform-buildId
    112 *   ------ specific structure.
    113 *   @internal
    114 */
    115 export class Cache {
    116  #rootDir: string;
    117 
    118  constructor(rootDir: string) {
    119    this.#rootDir = rootDir;
    120  }
    121 
    122  /**
    123   * @internal
    124   */
    125  get rootDir(): string {
    126    return this.#rootDir;
    127  }
    128 
    129  browserRoot(browser: Browser): string {
    130    return path.join(this.#rootDir, browser);
    131  }
    132 
    133  metadataFile(browser: Browser): string {
    134    return path.join(this.browserRoot(browser), '.metadata');
    135  }
    136 
    137  readMetadata(browser: Browser): Metadata {
    138    const metatadaPath = this.metadataFile(browser);
    139    if (!fs.existsSync(metatadaPath)) {
    140      return {aliases: {}};
    141    }
    142    // TODO: add type-safe parsing.
    143    const data = JSON.parse(fs.readFileSync(metatadaPath, 'utf8'));
    144    if (typeof data !== 'object') {
    145      throw new Error('.metadata is not an object');
    146    }
    147    return data;
    148  }
    149 
    150  writeMetadata(browser: Browser, metadata: Metadata): void {
    151    const metatadaPath = this.metadataFile(browser);
    152    fs.mkdirSync(path.dirname(metatadaPath), {recursive: true});
    153    fs.writeFileSync(metatadaPath, JSON.stringify(metadata, null, 2));
    154  }
    155 
    156  resolveAlias(browser: Browser, alias: string): string | undefined {
    157    const metadata = this.readMetadata(browser);
    158    if (alias === 'latest') {
    159      return Object.values(metadata.aliases || {})
    160        .sort(getVersionComparator(browser))
    161        .at(-1);
    162    }
    163    return metadata.aliases[alias];
    164  }
    165 
    166  installationDir(
    167    browser: Browser,
    168    platform: BrowserPlatform,
    169    buildId: string,
    170  ): string {
    171    return path.join(this.browserRoot(browser), `${platform}-${buildId}`);
    172  }
    173 
    174  clear(): void {
    175    fs.rmSync(this.#rootDir, {
    176      force: true,
    177      recursive: true,
    178      maxRetries: 10,
    179      retryDelay: 500,
    180    });
    181  }
    182 
    183  uninstall(
    184    browser: Browser,
    185    platform: BrowserPlatform,
    186    buildId: string,
    187  ): void {
    188    const metadata = this.readMetadata(browser);
    189    for (const alias of Object.keys(metadata.aliases)) {
    190      if (metadata.aliases[alias] === buildId) {
    191        delete metadata.aliases[alias];
    192      }
    193    }
    194    fs.rmSync(this.installationDir(browser, platform, buildId), {
    195      force: true,
    196      recursive: true,
    197      maxRetries: 10,
    198      retryDelay: 500,
    199    });
    200  }
    201 
    202  getInstalledBrowsers(): InstalledBrowser[] {
    203    if (!fs.existsSync(this.#rootDir)) {
    204      return [];
    205    }
    206    const types = fs.readdirSync(this.#rootDir);
    207    const browsers = types.filter((t): t is Browser => {
    208      return (Object.values(Browser) as string[]).includes(t);
    209    });
    210    return browsers.flatMap(browser => {
    211      const files = fs.readdirSync(this.browserRoot(browser));
    212      return files
    213        .map(file => {
    214          const result = parseFolderPath(
    215            path.join(this.browserRoot(browser), file),
    216          );
    217          if (!result) {
    218            return null;
    219          }
    220          return new InstalledBrowser(
    221            this,
    222            browser,
    223            result.buildId,
    224            result.platform as BrowserPlatform,
    225          );
    226        })
    227        .filter((item: InstalledBrowser | null): item is InstalledBrowser => {
    228          return item !== null;
    229        });
    230    });
    231  }
    232 
    233  computeExecutablePath(options: ComputeExecutablePathOptions): string {
    234    options.platform ??= detectBrowserPlatform();
    235    if (!options.platform) {
    236      throw new Error(
    237        `Cannot download a binary for the provided platform: ${os.platform()} (${os.arch()})`,
    238      );
    239    }
    240    try {
    241      options.buildId =
    242        this.resolveAlias(options.browser, options.buildId) ?? options.buildId;
    243    } catch {
    244      debugCache('could not read .metadata file for the browser');
    245    }
    246    const installationDir = this.installationDir(
    247      options.browser,
    248      options.platform,
    249      options.buildId,
    250    );
    251    return path.join(
    252      installationDir,
    253      executablePathByBrowser[options.browser](
    254        options.platform,
    255        options.buildId,
    256      ),
    257    );
    258  }
    259 }
    260 
    261 function parseFolderPath(
    262  folderPath: string,
    263 ): {platform: string; buildId: string} | undefined {
    264  const name = path.basename(folderPath);
    265  const splits = name.split('-');
    266  if (splits.length !== 2) {
    267    return;
    268  }
    269  const [platform, buildId] = splits;
    270  if (!buildId || !platform) {
    271    return;
    272  }
    273  return {platform, buildId};
    274 }