Skip to content

Commit 8dd1b9f

Browse files
committed
Augments the add database command to handle zip files
The add database command can now add databases by zip file. When a file is selected, the zip file is attempted to be extracted into a directory managed by the extension. Once extracted, a database is searched for, by looking for a .dbinfo file. Crucially, we are using the same infrastructure to download a database as we are to add a database by zip file.
1 parent 2da70d7 commit 8dd1b9f

File tree

3 files changed

+104
-36
lines changed

3 files changed

+104
-36
lines changed

extensions/ql-vscode/src/databaseFetcher.ts

Lines changed: 86 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as unzipper from "unzipper";
33
import { Uri, ProgressOptions, ProgressLocation, commands, window } from "vscode";
44
import * as fs from "fs-extra";
55
import * as path from "path";
6-
import { DatabaseManager } from "./databases";
6+
import { DatabaseManager, DatabaseItem } from "./databases";
77
import { ProgressCallback, showAndLogErrorMessage, withProgress } from "./helpers";
88

99
export default async function promptFetchDatabase(dbm: DatabaseManager, storagePath: string) {
@@ -28,13 +28,13 @@ export default async function promptFetchDatabase(dbm: DatabaseManager, storageP
2828
}
2929
}
3030

31-
async function databaseFetcher(
31+
export async function databaseFetcher(
3232
databaseUrl: string,
3333
databasesManager: DatabaseManager,
3434
storagePath: string,
35-
progressCallback: ProgressCallback
36-
): Promise<void> {
37-
progressCallback({
35+
progressCallback?: ProgressCallback
36+
): Promise<DatabaseItem> {
37+
progressCallback?.({
3838
maxStep: 3,
3939
message: 'Downloading database',
4040
step: 1
@@ -45,36 +45,27 @@ async function databaseFetcher(
4545
await fs.ensureDir(storagePath);
4646
const unzipPath = await getStorageFolder(storagePath, databaseUrl);
4747

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({
48+
if (isFile(databaseUrl)) {
49+
await readAndUnzip(databaseUrl, unzipPath);
50+
} else {
51+
await fetchAndUnzip(databaseUrl, unzipPath, progressCallback);
52+
}
53+
54+
progressCallback?.({
6455
maxStep: 3,
6556
message: 'Opening database',
6657
step: 3
6758
});
6859

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);
60+
const dbPath = await findDirWithFile(unzipPath, '.dbinfo');
61+
if (dbPath) {
62+
// might need to upgrade before importing...
63+
const item = await databasesManager.openDatabase(Uri.parse(dbPath));
64+
databasesManager.setCurrentDatabaseItem(item);
65+
return item;
66+
} else {
67+
throw new Error('Database not found in archive.');
68+
}
7869
}
7970

8071
async function getStorageFolder(storagePath: string, urlStr: string) {
@@ -110,3 +101,68 @@ function validateUrl(databaseUrl: string) {
110101
throw new Error('Must use https for downloading a database.');
111102
}
112103
}
104+
105+
async function readAndUnzip(databaseUrl: string, unzipPath: string) {
106+
const unzipStream = unzipper.Extract({
107+
path: unzipPath,
108+
verbose: true
109+
});
110+
111+
await new Promise((resolve, reject) => {
112+
// we already know this is a file scheme
113+
const databaseFile = Uri.parse(databaseUrl).path;
114+
const stream = fs.createReadStream(databaseFile);
115+
stream.on('error', reject);
116+
unzipStream.on('error', reject);
117+
unzipStream.on('close', resolve);
118+
stream.pipe(unzipStream);
119+
});
120+
}
121+
122+
async function fetchAndUnzip(databaseUrl: string, unzipPath: string, progressCallback?: ProgressCallback) {
123+
const response = await fetch.default(databaseUrl);
124+
const unzipStream = unzipper.Extract({
125+
path: unzipPath
126+
});
127+
progressCallback?.({
128+
maxStep: 3,
129+
message: 'Unzipping database',
130+
step: 2
131+
});
132+
await new Promise((resolve, reject) => {
133+
response.body.on('error', reject);
134+
unzipStream.on('error', reject);
135+
unzipStream.on('close', resolve);
136+
response.body.pipe(unzipStream);
137+
});
138+
}
139+
140+
function isFile(databaseUrl: string) {
141+
return Uri.parse(databaseUrl).scheme === 'file';
142+
}
143+
144+
/**
145+
* Recursively looks for a file in a directory. If the file exists, then returns the directory containing the file.
146+
*
147+
* @param dir The directory to search
148+
* @param toFind The file to recursively look for in this directory
149+
*
150+
* @returns the directory containing the file, or undefined if not found.
151+
*/
152+
async function findDirWithFile(dir: string, toFind: string): Promise<string | undefined> {
153+
if (!(await fs.stat(dir)).isDirectory()) {
154+
return;
155+
}
156+
const files = await fs.readdir(dir);
157+
if (files.includes(toFind)) {
158+
return dir;
159+
}
160+
for (const file of files) {
161+
const newPath = path.join(dir, file);
162+
const result = await findDirWithFile(newPath, toFind);
163+
if (result) {
164+
return result;
165+
}
166+
}
167+
return;
168+
}

extensions/ql-vscode/src/databases-ui.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { logger } from './logging';
88
import { clearCacheInDatabase, UserCancellationException } from './run-queries';
99
import * as qsClient from './queryserver-client';
1010
import { upgradeDatabase } from './upgrades';
11+
import { databaseFetcher } from './databaseFetcher';
1112

1213
type ThemableIconPath = { light: string; dark: string } | string;
1314

@@ -149,10 +150,11 @@ function getFirst(list: Uri[] | undefined): Uri | undefined {
149150
*/
150151
async function chooseDatabaseDir(): Promise<Uri | undefined> {
151152
const chosen = await window.showOpenDialog({
152-
openLabel: 'Choose Database',
153+
openLabel: 'Choose Database folder or archive',
153154
canSelectFiles: true,
154155
canSelectFolders: true,
155-
canSelectMany: false
156+
canSelectMany: false,
157+
156158
});
157159
return getFirst(chosen);
158160
}
@@ -164,7 +166,8 @@ export class DatabaseUI extends DisposableObject {
164166
ctx: ExtensionContext,
165167
private cliserver: cli.CodeQLCliServer,
166168
private databaseManager: DatabaseManager,
167-
private readonly queryServer: qsClient.QueryServerClient | undefined
169+
private readonly queryServer: qsClient.QueryServerClient | undefined,
170+
private readonly storagePath: string
168171
) {
169172
super();
170173

@@ -189,7 +192,12 @@ export class DatabaseUI extends DisposableObject {
189192
}
190193

191194
private handleChooseDatabase = async (): Promise<DatabaseItem | undefined> => {
192-
return await this.chooseAndSetDatabase();
195+
try {
196+
return await this.chooseAndSetDatabase();
197+
} catch (e) {
198+
showAndLogErrorMessage(e.message);
199+
return undefined;
200+
}
193201
}
194202

195203
private handleSortByName = async () => {
@@ -326,7 +334,11 @@ export class DatabaseUI extends DisposableObject {
326334
*/
327335
private async chooseAndSetDatabase(): Promise<DatabaseItem | undefined> {
328336
const uri = await chooseDatabaseDir();
329-
if (uri !== undefined) {
337+
338+
if (uri?.path.endsWith('.zip')) {
339+
return await databaseFetcher(uri.toString(), this.databaseManager, this.storagePath);
340+
}
341+
else if (uri !== undefined) {
330342
return await this.setCurrentDatabase(uri);
331343
}
332344
else {

extensions/ql-vscode/src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
265265

266266
const dbm = new DatabaseManager(ctx, qlConfigurationListener, logger);
267267
ctx.subscriptions.push(dbm);
268-
const databaseUI = new DatabaseUI(ctx, cliServer, dbm, qs);
268+
const databaseUI = new DatabaseUI(ctx, cliServer, dbm, qs, getContextStoragePath(ctx));
269269
ctx.subscriptions.push(databaseUI);
270270

271271
const queryHistoryConfigurationListener = new QueryHistoryConfigListener();

0 commit comments

Comments
 (0)