@renovatebot/osv-offline 3.0.12 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,15 @@
4
4
  [![Build status](https://img.shields.io/github/actions/workflow/status/renovatebot/osv-offline/build.yml?branch=main&style=for-the-badge)](https://github.com/renovatebot/osv-offline/actions/workflows/build.yml)
5
5
  [![MIT license](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](./LICENSE)
6
6
 
7
+ ## Environment variables
8
+
9
+ | Name | Description | Default |
10
+ | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
11
+ | `OSV_OFFLINE_ROOT_DIR` | Directory of the downloaded databases | `<tmpdir>/osv-offline` |
12
+ | `OSV_OFFLINE_DATABASE_URL` | URL of the database zip | `https://github.com/renovatebot/osv-offline/releases/latest/download/osv-offline.zip` |
13
+ | `OSV_OFFLINE_DISABLE_DOWNLOAD` | Set to `true` to use the databases in `OSV_OFFLINE_ROOT_DIR` without downloading them | - |
14
+ | `OSV_OFFLINE_MAX_AGE` | How old the downloaded databases may be before they are downloaded again, as a [`ms`](https://github.com/vercel/ms) duration, e.g. `4h` | `1d` |
15
+
7
16
  ## License
8
17
 
9
18
  This package is licensed under [the MIT license](https://opensource.org/licenses/MIT).
@@ -4,17 +4,35 @@ import { pipeline } from 'node:stream/promises';
4
4
  import { OsvOfflineDb } from '@renovatebot/osv-offline-db';
5
5
  import path from 'path';
6
6
  import { DateTime } from 'luxon';
7
- import AdmZip from 'adm-zip';
7
+ import ms from 'ms';
8
8
  import { failure, success } from "./types.js";
9
+ import { extractZip } from "./unzip.js";
9
10
  import debug from 'debug';
10
11
  const logger = debug('osv-offline:download');
12
+ const defaultMaxAge = '1d';
13
+ /**
14
+ * Returns how many milliseconds old the local databases may be before they are downloaded again: `OSV_OFFLINE_MAX_AGE` (e.g. `4h`), or one day.
15
+ */
16
+ function getMaxAge() {
17
+ const value = process.env.OSV_OFFLINE_MAX_AGE;
18
+ if (!value) {
19
+ return ms(defaultMaxAge);
20
+ }
21
+ // returns `undefined` for an invalid value, despite the type
22
+ const maxAge = ms(value);
23
+ if (maxAge === undefined) {
24
+ logger(`Invalid OSV_OFFLINE_MAX_AGE '${value}', using ${defaultMaxAge}.`);
25
+ return ms(defaultMaxAge);
26
+ }
27
+ return maxAge;
28
+ }
11
29
  export async function tryDownloadDb() {
12
30
  await fs.ensureDir(OsvOfflineDb.rootDirectory);
13
31
  if (process.env.OSV_OFFLINE_DISABLE_DOWNLOAD?.toLowerCase() === 'true') {
14
32
  logger('Skipping database download.');
15
33
  return success();
16
34
  }
17
- // if local database exists and is less than a day old, don't do any network requests
35
+ // if local database exists and is younger than the maximum age, don't do any network requests
18
36
  let stats;
19
37
  try {
20
38
  stats = await fs.stat(path.join(OsvOfflineDb.rootDirectory, `osv-offline.zip`));
@@ -23,27 +41,40 @@ export async function tryDownloadDb() {
23
41
  // ignored
24
42
  }
25
43
  if (stats !== undefined &&
26
- DateTime.utc().diff(DateTime.fromJSDate(stats.mtime)).as('days') < 1) {
44
+ DateTime.utc().diff(DateTime.fromJSDate(stats.mtime)).toMillis() <
45
+ getMaxAge()) {
27
46
  logger('Skipping download, databases are up-to-date.');
28
47
  return success();
29
48
  }
30
49
  // only download databases if local databases are missing or remote is newer
50
+ // download and extract into a temporary directory first, so a failed attempt leaves the current databases untouched
51
+ const tmpDir = await fs.mkdtemp(path.join(OsvOfflineDb.rootDirectory, '.download-'));
31
52
  try {
32
53
  logger('Downloading databases ...');
33
54
  const databaseUrl = process.env.OSV_OFFLINE_DATABASE_URL ??
34
55
  'https://github.com/renovatebot/osv-offline/releases/latest/download/osv-offline.zip';
35
56
  const stream = got.stream(databaseUrl);
36
- const zipPath = path.join(OsvOfflineDb.rootDirectory, 'osv-offline.zip');
37
- const writeStream = fs.createWriteStream(zipPath);
57
+ const tmpZipPath = path.join(tmpDir, 'osv-offline.zip');
58
+ const writeStream = fs.createWriteStream(tmpZipPath);
38
59
  await pipeline(stream, writeStream);
39
60
  logger('Downloading databases done.');
40
61
  logger('Extracting databases ...');
41
- const zip = new AdmZip(zipPath);
42
- zip.extractAllTo(OsvOfflineDb.rootDirectory);
62
+ const extractDir = path.join(tmpDir, 'extracted');
63
+ await fs.ensureDir(extractDir);
64
+ const files = await extractZip(tmpZipPath, extractDir);
65
+ // renaming replaces existing databases, while readers which already opened them keep reading the old files
66
+ for (const file of files) {
67
+ await fs.rename(path.join(extractDir, file), path.join(OsvOfflineDb.rootDirectory, file));
68
+ }
69
+ // the zip's modification time marks the databases as up-to-date, so it's moved last
70
+ await fs.rename(tmpZipPath, path.join(OsvOfflineDb.rootDirectory, 'osv-offline.zip'));
43
71
  logger('Extracting databases done.');
44
72
  }
45
73
  catch (err) {
46
74
  return failure(err);
47
75
  }
76
+ finally {
77
+ await fs.remove(tmpDir);
78
+ }
48
79
  return success();
49
80
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Extracts the files of a zip archive into `targetDir`, streaming each one to disk and verifying its CRC-32.
3
+ *
4
+ * Only supports files in the root of the archive.
5
+ *
6
+ * @returns the names of the extracted files
7
+ */
8
+ export declare function extractZip(zipPath: string, targetDir: string): Promise<string[]>;
@@ -0,0 +1,63 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Transform } from 'node:stream';
4
+ import { pipeline } from 'node:stream/promises';
5
+ import { crc32 } from 'node:zlib';
6
+ import { openPromise } from 'yauzl';
7
+ /**
8
+ * Extracts the files of a zip archive into `targetDir`, streaming each one to disk and verifying its CRC-32.
9
+ *
10
+ * Only supports files in the root of the archive.
11
+ *
12
+ * @returns the names of the extracted files
13
+ */
14
+ export async function extractZip(zipPath, targetDir) {
15
+ const zipFile = await openPromise(zipPath, {
16
+ lazyEntries: true,
17
+ autoClose: false,
18
+ });
19
+ try {
20
+ const files = [];
21
+ for await (const entry of zipFile.eachEntry()) {
22
+ if (entry.fileName.endsWith('/')) {
23
+ // directory entry
24
+ continue;
25
+ }
26
+ if (entry.fileName.includes('/')) {
27
+ throw new Error(`Unsupported zip entry name: ${entry.fileName}`);
28
+ }
29
+ await pipeline(await zipFile.openReadStreamPromise(entry), new Crc32Verifier(entry), createWriteStream(path.join(targetDir, entry.fileName)));
30
+ files.push(entry.fileName);
31
+ }
32
+ return files;
33
+ }
34
+ finally {
35
+ zipFile.close();
36
+ }
37
+ }
38
+ /**
39
+ * Passes the data through, and fails when its CRC-32 differs from the zip entry's.
40
+ *
41
+ * yauzl only verifies the size of an entry.
42
+ *
43
+ * @see https://github.com/thejoshwolfe/yauzl/issues/49
44
+ */
45
+ class Crc32Verifier extends Transform {
46
+ entry;
47
+ crc = 0;
48
+ constructor(entry) {
49
+ super();
50
+ this.entry = entry;
51
+ }
52
+ _transform(chunk, _encoding, callback) {
53
+ this.crc = crc32(chunk, this.crc);
54
+ callback(null, chunk);
55
+ }
56
+ _flush(callback) {
57
+ if (this.crc !== this.entry.crc32) {
58
+ callback(new Error(`CRC-32 mismatch for ${this.entry.fileName}`));
59
+ return;
60
+ }
61
+ callback();
62
+ }
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@renovatebot/osv-offline",
3
- "version": "3.0.12",
3
+ "version": "3.1.0",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,19 +12,23 @@
12
12
  "url": "https://github.com/renovatebot/osv-offline.git"
13
13
  },
14
14
  "dependencies": {
15
- "@renovatebot/osv-offline-db": "3.0.12",
16
- "adm-zip": "~0.6.1",
15
+ "@renovatebot/osv-offline-db": "3.1.0",
17
16
  "debug": "^4.4.3",
18
17
  "fs-extra": "^11.4.1",
19
18
  "got": "^15.1.0",
20
- "luxon": "^3.7.2"
19
+ "luxon": "^3.7.2",
20
+ "ms": "^2.1.3",
21
+ "yauzl": "^3.4.0"
21
22
  },
22
23
  "devDependencies": {
23
- "@types/adm-zip": "0.5.8",
24
+ "@types/adm-zip": "0.6.0",
24
25
  "@types/debug": "4.1.13",
25
26
  "@types/fs-extra": "11.0.4",
26
- "@types/luxon": "3.7.5",
27
- "@types/node": "22.20.4"
27
+ "@types/luxon": "3.7.6",
28
+ "@types/ms": "2.1.0",
29
+ "@types/node": "22.20.5",
30
+ "@types/yauzl": "3.4.0",
31
+ "adm-zip": "0.6.1"
28
32
  },
29
33
  "engines": {
30
34
  "node": ">=22.12.0",