Skip to content

Commit e0cd041

Browse files
authored
Clean databases folder on startup (#675)
Cleans orphan databases on startup. This commit also bumps the fs-extra dependency to get readdir with dirent objects. Adds the `asyncFilter` to filter arrays asynchronously.
1 parent 4f76e9d commit e0cd041

11 files changed

Lines changed: 218 additions & 47 deletions

File tree

extensions/ql-vscode/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Alter structure of the _Test Explorer_ tree. It now follows the structure of the filesystem instead of the QL Packs. [#624](https://github.com/github/vscode-codeql/pull/624)
88
- Alter structure of the _Test Explorer_ tree. It now follows the structure of the filesystem instead of the QL Packs. [#624](https://github.com/github/vscode-codeql/pull/624)
99
- Add more structured output for tests. [#626](https://github.com/github/vscode-codeql/pull/626)
10+
- Whenever the extension restarts, orphaned databases will be cleaned up. These are databases whose files are located inside of the extension's storage area, but are not imported into the workspace.
1011

1112
## 1.3.6 - 4 November 2020
1213

extensions/ql-vscode/package-lock.json

Lines changed: 32 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/ql-vscode/package.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@
197197
"dark": "media/dark/folder-opened-plus.svg"
198198
}
199199
},
200+
{
201+
"command": "codeQLDatabases.removeOrphanedDatabases",
202+
"title": "Delete unused databases"
203+
},
200204
{
201205
"command": "codeQLDatabases.chooseDatabaseArchive",
202206
"title": "Choose Database from Archive",
@@ -573,6 +577,10 @@
573577
"command": "codeQLDatabases.chooseDatabaseArchive",
574578
"when": "false"
575579
},
580+
{
581+
"command": "codeQLDatabases.removeOrphanedDatabases",
582+
"when": "false"
583+
},
576584
{
577585
"command": "codeQLDatabases.chooseDatabaseInternet",
578586
"when": "false"
@@ -704,7 +712,7 @@
704712
"dependencies": {
705713
"child-process-promise": "^2.2.1",
706714
"classnames": "~2.2.6",
707-
"fs-extra": "^8.1.0",
715+
"fs-extra": "^9.0.1",
708716
"glob-promise": "^3.4.0",
709717
"js-yaml": "^3.14.0",
710718
"minimist": "~1.2.5",
@@ -727,15 +735,15 @@
727735
"@types/chai-as-promised": "~7.1.2",
728736
"@types/child-process-promise": "^2.2.1",
729737
"@types/classnames": "~2.2.9",
730-
"@types/fs-extra": "^8.0.0",
738+
"@types/fs-extra": "^9.0.3",
731739
"@types/glob": "^7.1.1",
732740
"@types/google-protobuf": "^3.2.7",
733741
"@types/gulp": "^4.0.6",
734742
"@types/gulp-sourcemaps": "0.0.32",
735743
"@types/js-yaml": "^3.12.5",
736744
"@types/jszip": "~3.1.6",
737745
"@types/mocha": "~8.0.3",
738-
"@types/node": "^12.0.8",
746+
"@types/node": "^12.14.1",
739747
"@types/node-fetch": "~2.5.2",
740748
"@types/proxyquire": "~1.3.28",
741749
"@types/react": "^16.8.17",

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

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
TreeItem,
99
Uri,
1010
window,
11-
env
11+
env,
1212
} from 'vscode';
1313
import * as fs from 'fs-extra';
1414

@@ -18,6 +18,8 @@ import {
1818
DatabaseItem,
1919
DatabaseManager,
2020
getUpgradesDirectories,
21+
isLikelyDatabaseRoot,
22+
isLikelyDbLanguageFolder,
2123
} from './databases';
2224
import {
2325
commandRunner,
@@ -36,6 +38,7 @@ import {
3638
promptImportLgtmDatabase,
3739
} from './databaseFetcher';
3840
import { CancellationToken } from 'vscode-jsonrpc';
41+
import { asyncFilter } from './pure/helpers-pure';
3942

4043
type ThemableIconPath = { light: string; dark: string } | string;
4144

@@ -229,7 +232,9 @@ export class DatabaseUI extends DisposableObject {
229232
canSelectMany: true,
230233
})
231234
);
235+
}
232236

237+
init() {
233238
logger.log('Registering database panel commands.');
234239
this.push(
235240
commandRunnerWithProgress(
@@ -340,6 +345,12 @@ export class DatabaseUI extends DisposableObject {
340345
this.handleOpenFolder
341346
)
342347
);
348+
this.push(
349+
commandRunner(
350+
'codeQLDatabases.removeOrphanedDatabases',
351+
this.handleRemoveOrphanedDatabases
352+
)
353+
);
343354
}
344355

345356
private handleMakeCurrentDatabase = async (
@@ -360,6 +371,53 @@ export class DatabaseUI extends DisposableObject {
360371
}
361372
};
362373

374+
handleRemoveOrphanedDatabases = async (): Promise<void> => {
375+
logger.log('Removing orphaned databases from workspace storage.');
376+
let dbDirs =
377+
// read directory
378+
(await fs.readdir(this.storagePath, { withFileTypes: true }))
379+
// remove non-directories
380+
.filter(dirent => dirent.isDirectory())
381+
// get the full path
382+
.map(dirent => path.join(this.storagePath, dirent.name))
383+
// remove databases still in workspace
384+
.filter(dbDir => {
385+
const dbUri = Uri.file(dbDir);
386+
return this.databaseManager.databaseItems.every(item => item.databaseUri.fsPath !== dbUri.fsPath);
387+
});
388+
389+
// remove non-databases
390+
dbDirs = await asyncFilter(dbDirs, isLikelyDatabaseRoot);
391+
392+
if (!dbDirs.length) {
393+
logger.log('No orphaned databases found.');
394+
return;
395+
}
396+
397+
// delete
398+
const failures = [] as string[];
399+
await Promise.all(
400+
dbDirs.map(async dbDir => {
401+
try {
402+
logger.log(`Deleting orphaned database '${dbDir}'.`);
403+
await fs.rmdir(dbDir, { recursive: true } as any); // typings doesn't recognize the options argument
404+
} catch (e) {
405+
failures.push(`${path.basename(dbDir)}`);
406+
}
407+
})
408+
);
409+
410+
if (failures.length) {
411+
const dirname = path.dirname(failures[0]);
412+
showAndLogErrorMessage(
413+
`Failed to delete unused databases:\n ${
414+
failures.join('\n ')
415+
}\n. To delete unused databases, please remove them manually from the storage folder ${dirname}.`
416+
);
417+
}
418+
};
419+
420+
363421
handleChooseDatabaseArchive = async (
364422
progress: ProgressCallback,
365423
token: CancellationToken
@@ -653,7 +711,7 @@ export class DatabaseUI extends DisposableObject {
653711
dbPath = path.dirname(dbPath);
654712
}
655713

656-
if (isLikelyDbFolder(dbPath)) {
714+
if (isLikelyDbLanguageFolder(dbPath)) {
657715
dbPath = path.dirname(dbPath);
658716
}
659717
return Uri.file(dbPath);
@@ -668,9 +726,3 @@ export class DatabaseUI extends DisposableObject {
668726
}
669727
}
670728
}
671-
672-
// TODO: Get the list of supported languages from a list that will be auto-updated.
673-
const dbRegeEx = /^db-(javascript|go|cpp|java|python|csharp)$/;
674-
function isLikelyDbFolder(dbPath: string) {
675-
return path.basename(dbPath).match(dbRegeEx);
676-
}

extensions/ql-vscode/src/databases.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,7 @@ export class DatabaseItemImpl implements DatabaseItem {
397397
* Holds if the database item refers to an exported snapshot
398398
*/
399399
public async hasMetadataFile(): Promise<boolean> {
400-
return (await Promise.all([
401-
fs.pathExists(path.join(this.databaseUri.fsPath, '.dbinfo')),
402-
fs.pathExists(path.join(this.databaseUri.fsPath, 'codeql-database.yml'))
403-
])).some(x => x);
400+
return await isLikelyDatabaseRoot(this.databaseUri.fsPath);
404401
}
405402

406403
/**
@@ -730,3 +727,23 @@ export function getUpgradesDirectories(scripts: string[]): vscode.Uri[] {
730727
const uniqueParentDirs = new Set(parentDirs);
731728
return Array.from(uniqueParentDirs).map(filePath => vscode.Uri.file(filePath));
732729
}
730+
731+
732+
// TODO: Get the list of supported languages from a list that will be auto-updated.
733+
734+
export async function isLikelyDatabaseRoot(fsPath: string) {
735+
const [a, b, c] = (await Promise.all([
736+
// databases can have either .dbinfo or codeql-database.yml.
737+
fs.pathExists(path.join(fsPath, '.dbinfo')),
738+
fs.pathExists(path.join(fsPath, 'codeql-database.yml')),
739+
740+
// they *must* have a db-language folder
741+
(await fs.readdir(fsPath)).some(isLikelyDbLanguageFolder)
742+
]));
743+
744+
return (a || b) && c;
745+
}
746+
747+
export function isLikelyDbLanguageFolder(dbPath: string) {
748+
return !!path.basename(dbPath).startsWith('db-');
749+
}

extensions/ql-vscode/src/extension.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ async function activateWithInstalledDistribution(
349349
getContextStoragePath(ctx),
350350
ctx.extensionPath
351351
);
352+
databaseUI.init();
352353
ctx.subscriptions.push(databaseUI);
353354

354355
logger.log('Initializing query history manager.');
@@ -643,6 +644,8 @@ async function activateWithInstalledDistribution(
643644
title: 'Calculate AST'
644645
}));
645646

647+
commands.executeCommand('codeQLDatabases.removeOrphanedDatabases');
648+
646649
logger.log('Successfully finished extension initialization.');
647650
}
648651

extensions/ql-vscode/src/pure/helpers-pure.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ class ExhaustivityCheckingError extends Error {
2121
export function assertNever(value: never): never {
2222
throw new ExhaustivityCheckingError(value);
2323
}
24+
25+
/**
26+
* Use to perform array filters where the predicate is asynchronous.
27+
*/
28+
export const asyncFilter = async function <T>(arr: T[], predicate: (arg0: T) => Promise<boolean>) {
29+
const results = await Promise.all(arr.map(predicate));
30+
return arr.filter((_, index) => results[index]);
31+
};

0 commit comments

Comments
 (0)