@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 +9 -0
- package/dist/lib/download.js +38 -7
- package/dist/lib/unzip.d.ts +8 -0
- package/dist/lib/unzip.js +63 -0
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
[](https://github.com/renovatebot/osv-offline/actions/workflows/build.yml)
|
|
5
5
|
[](./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).
|
package/dist/lib/download.js
CHANGED
|
@@ -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
|
|
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
|
|
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)).
|
|
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
|
|
37
|
-
const writeStream = fs.createWriteStream(
|
|
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
|
|
42
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
27
|
-
"@types/
|
|
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",
|