Skip to content

Commit 926ab92

Browse files
committed
Add command to download, unzip, and open databases
New command that requests a URL and allows a user to install a database from that url. Closes #357
1 parent 36484fc commit 926ab92

File tree

6 files changed

+152
-5
lines changed

6 files changed

+152
-5
lines changed

extensions/ql-vscode/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"onCommand:codeQL.checkForUpdatesToCLI",
3030
"onCommand:codeQL.chooseDatabase",
3131
"onCommand:codeQL.setCurrentDatabase",
32+
"onCommand:codeQL.downloadDatabase",
3233
"onCommand:codeQLDatabases.chooseDatabase",
3334
"onCommand:codeQLDatabases.setCurrentDatabase",
3435
"onCommand:codeQL.quickQuery",
@@ -211,6 +212,10 @@
211212
"command": "codeQLDatabases.openDatabaseFolder",
212213
"title": "Show Database Directory"
213214
},
215+
{
216+
"command": "codeQL.downloadDatabase",
217+
"title": "CodeQL: Download database"
218+
},
214219
{
215220
"command": "codeQLDatabases.sortByName",
216221
"title": "Sort by Name",
@@ -373,6 +378,10 @@
373378
"command": "codeQL.runQuery",
374379
"when": "resourceLangId == ql && resourceExtname == .ql"
375380
},
381+
{
382+
"command": "codeQL.downloadDatabase",
383+
"when": "true"
384+
},
376385
{
377386
"command": "codeQL.quickEval",
378387
"when": "editorLangId == ql"
@@ -385,6 +394,14 @@
385394
"command": "codeQLDatabases.setCurrentDatabase",
386395
"when": "false"
387396
},
397+
{
398+
"command": "codeQLDatabases.renameDatabase",
399+
"when": "false"
400+
},
401+
{
402+
"command": "codeQLDatabases.openDatabaseFolder",
403+
"when": "false"
404+
},
388405
{
389406
"command": "codeQLDatabases.sortByName",
390407
"when": "false"
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import * as fetch from "node-fetch";
2+
import * as unzipper from "unzipper";
3+
import { ExtensionContext, Uri, ProgressOptions, ProgressLocation, commands, window } from "vscode";
4+
import * as fs from "fs-extra";
5+
import * as path from "path";
6+
import { DatabaseManager } from "./databases";
7+
import { ProgressCallback, showAndLogErrorMessage, withProgress } from "./helpers";
8+
9+
export default async function promptFetchDatabase(dbm: DatabaseManager, ctx: ExtensionContext) {
10+
try {
11+
const databaseUrl = await window.showInputBox({
12+
prompt: 'Enter URL of zipfile of database to download'
13+
});
14+
15+
if (databaseUrl) {
16+
validateUrl(databaseUrl);
17+
18+
const progressOptions: ProgressOptions = {
19+
location: ProgressLocation.Notification,
20+
title: 'Adding database from URL',
21+
cancellable: false,
22+
};
23+
await withProgress(progressOptions, async progress => await databaseFetcher(databaseUrl, dbm, ctx, progress));
24+
commands.executeCommand('codeQLDatabases.focus');
25+
}
26+
} catch (e) {
27+
showAndLogErrorMessage(e.message);
28+
}
29+
}
30+
31+
async function databaseFetcher(
32+
databaseUrl: string,
33+
databasesManager: DatabaseManager,
34+
ctx: ExtensionContext,
35+
progressCallback: ProgressCallback
36+
): Promise<void> {
37+
progressCallback({
38+
maxStep: 3,
39+
message: 'Downloading database',
40+
step: 1
41+
});
42+
const storagePath = ctx.storagePath || ctx.globalStoragePath;
43+
if (!storagePath) {
44+
throw new Error("No storage path specified.");
45+
}
46+
const unzipPath = await getStorageFolder(storagePath, databaseUrl);
47+
48+
const response = await fetch.default(databaseUrl);
49+
const unzipStream = unzipper.Extract({
50+
path: unzipPath
51+
});
52+
progressCallback({
53+
maxStep: 3,
54+
message: 'Unzipping database',
55+
step: 2
56+
});
57+
await new Promise((resolve, reject) => {
58+
response.body.on('error', reject);
59+
unzipStream.on('error', reject);
60+
unzipStream.on('close', resolve);
61+
response.body.pipe(unzipStream);
62+
});
63+
progressCallback({
64+
maxStep: 3,
65+
message: 'Opening database',
66+
step: 3
67+
});
68+
69+
// if there is a single directory inside, then assume that's what we want to import
70+
const dirs = await fs.readdir(unzipPath);
71+
const dbPath = dirs?.length === 1 && (await fs.stat(path.join(unzipPath, dirs[0]))).isDirectory
72+
? path.join(unzipPath, dirs[0])
73+
: unzipPath;
74+
75+
// might need to upgrade before importing...
76+
const item = await databasesManager.openDatabase(Uri.parse(dbPath));
77+
databasesManager.setCurrentDatabaseItem(item);
78+
}
79+
80+
async function getStorageFolder(storagePath: string, urlStr: string) {
81+
const url = Uri.parse(urlStr);
82+
let lastName = path.basename(url.path).substring(0, 255);
83+
if (lastName.endsWith(".zip")) {
84+
lastName = lastName.substring(0, lastName.length - 4);
85+
}
86+
87+
const realpath = await fs.realpath(storagePath);
88+
let folderName = path.join(realpath, lastName);
89+
let counter = 0;
90+
while (await fs.pathExists(folderName)) {
91+
counter++;
92+
folderName = path.join(realpath, `${lastName}-${counter}`);
93+
if (counter > 100) {
94+
throw new Error("Could not find a unique name for downloaded database.");
95+
}
96+
}
97+
return folderName;
98+
}
99+
100+
101+
function validateUrl(databaseUrl: string) {
102+
let uri;
103+
try {
104+
uri = Uri.parse(databaseUrl, true);
105+
} catch (e) {
106+
throw new Error(`Invalid url: ${databaseUrl}`);
107+
}
108+
109+
if (uri.scheme !== 'https') {
110+
throw new Error('Must use https for downloading a database.');
111+
}
112+
}

extensions/ql-vscode/src/databases.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,14 @@ export class DatabaseManager extends DisposableObject {
669669
vscode.workspace.updateWorkspaceFolders(folderIndex, 1);
670670
}
671671

672+
// Delete folder from file system only if it is controlled by the extension
673+
if (this.isExtensionControlledLocation(item.databaseUri)) {
674+
logger.log(`Deleting database from filesystem.`);
675+
fs.remove(item.databaseUri.path).then(
676+
() => logger.log(`Deleted '${item.databaseUri.path}'`),
677+
e => logger.log(`Failed to delete '${item.databaseUri.path}'. Reason: ${e.message}`));
678+
}
679+
672680
this._onDidChangeDatabaseItem.fire(undefined);
673681
}
674682

@@ -680,6 +688,11 @@ export class DatabaseManager extends DisposableObject {
680688
private updatePersistedDatabaseList(): void {
681689
this.ctx.workspaceState.update(DB_LIST, this._databaseItems.map(item => item.getPersistedState()));
682690
}
691+
692+
private isExtensionControlledLocation(uri: vscode.Uri) {
693+
const storagePath = this.ctx.storagePath || this.ctx.globalStoragePath;
694+
return uri.path.startsWith(storagePath);
695+
}
683696
}
684697

685698
/**

extensions/ql-vscode/src/distribution.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as unzipper from "unzipper";
66
import * as url from "url";
77
import { ExtensionContext, Event } from "vscode";
88
import { DistributionConfig } from "./config";
9-
import { InvocationRateLimiter, InvocationRateLimiterResultKind, ProgressUpdate, showAndLogErrorMessage } from "./helpers";
9+
import { InvocationRateLimiter, InvocationRateLimiterResultKind, showAndLogErrorMessage } from "./helpers";
1010
import { logger } from "./logging";
1111
import * as helpers from "./helpers";
1212
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
@@ -171,7 +171,7 @@ export class DistributionManager implements DistributionProvider {
171171
* Returns a failed promise if an unexpected error occurs during installation.
172172
*/
173173
public installExtensionManagedDistributionRelease(release: Release,
174-
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
174+
progressCallback?: helpers.ProgressCallback): Promise<void> {
175175
return this._extensionSpecificDistributionManager.installDistributionRelease(release, progressCallback);
176176
}
177177

@@ -253,14 +253,14 @@ class ExtensionSpecificDistributionManager {
253253
* Returns a failed promise if an unexpected error occurs during installation.
254254
*/
255255
public async installDistributionRelease(release: Release,
256-
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
256+
progressCallback?: helpers.ProgressCallback): Promise<void> {
257257
await this.downloadDistribution(release, progressCallback);
258258
// Store the installed release within the global extension state.
259259
this.storeInstalledRelease(release);
260260
}
261261

262262
private async downloadDistribution(release: Release,
263-
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
263+
progressCallback?: helpers.ProgressCallback): Promise<void> {
264264
try {
265265
await this.removeDistribution();
266266
} catch (e) {

extensions/ql-vscode/src/extension.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { displayQuickQuery } from './quick-query';
2020
import { compileAndRunQueryAgainstDatabase, tmpDirDisposal, UserCancellationException } from './run-queries';
2121
import { QLTestAdapterFactory } from './test-adapter';
2222
import { TestUIService } from './test-ui';
23+
import promptFetchDatabase from './databaseFetcher';
2324

2425
/**
2526
* extension.ts
@@ -60,8 +61,9 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command:
6061

6162
const extensionId = 'GitHub.vscode-codeql'; // TODO: Is there a better way of obtaining this?
6263
const extension = extensions.getExtension(extensionId);
63-
if (extension === undefined)
64+
if (extension === undefined) {
6465
throw new Error(`Can't find extension ${extensionId}`);
66+
}
6567

6668
const stubbedCommands: string[]
6769
= extension.packageJSON.contributes.commands.map((entry: { command: string }) => entry.command);
@@ -333,6 +335,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
333335
await qs.restartQueryServer();
334336
helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', { outputLogger: queryServerLogger });
335337
}));
338+
ctx.subscriptions.push(commands.registerCommand('codeQL.downloadDatabase', () => promptFetchDatabase(dbm, ctx)));
336339

337340
ctx.subscriptions.push(client.start());
338341

extensions/ql-vscode/src/helpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export interface ProgressUpdate {
2222
message: string;
2323
}
2424

25+
export type ProgressCallback = (p: ProgressUpdate) => void;
26+
2527
/**
2628
* This mediates between the kind of progress callbacks we want to
2729
* write (where we *set* current progress position and give

0 commit comments

Comments
 (0)