@@ -10,9 +10,11 @@ import {
1010 pathExists ,
1111 createWriteStream ,
1212 remove ,
13+ readdir ,
1314} from "fs-extra" ;
1415import { basename , join } from "path" ;
1516import type { Octokit } from "@octokit/rest" ;
17+ import { nanoid } from "nanoid" ;
1618
1719import type { DatabaseManager , DatabaseItem } from "./local-databases" ;
1820import { tmpDir } from "../tmp-dir" ;
@@ -36,6 +38,7 @@ import { AppOctokit } from "../common/octokit";
3638import type { DatabaseOrigin } from "./local-databases/database-origin" ;
3739import { createTimeoutSignal } from "../common/fetch-stream" ;
3840import type { App } from "../common/app" ;
41+ import { createFilenameFromString } from "../common/filenames" ;
3942import { findDirWithFile } from "../common/files" ;
4043import { convertGithubNwoToDatabaseUrl } from "./github-databases/api" ;
4144
@@ -364,7 +367,11 @@ async function databaseArchiveFetcher(
364367 throw new Error ( "No storage path specified." ) ;
365368 }
366369 await ensureDir ( storagePath ) ;
367- const unzipPath = await getStorageFolder ( storagePath , databaseUrl ) ;
370+ const unzipPath = await getStorageFolder (
371+ storagePath ,
372+ databaseUrl ,
373+ nameOverride ,
374+ ) ;
368375
369376 if ( isFile ( databaseUrl ) ) {
370377 await readAndUnzip ( databaseUrl , unzipPath , cli , progress ) ;
@@ -408,31 +415,60 @@ async function databaseArchiveFetcher(
408415 }
409416}
410417
411- async function getStorageFolder ( storagePath : string , urlStr : string ) {
412- // we need to generate a folder name for the unzipped archive,
413- // this needs to be human readable since we may use this name as the initial
414- // name for the database
415- const url = Uri . parse ( urlStr ) ;
416- // MacOS has a max filename length of 255
417- // and remove a few extra chars in case we need to add a counter at the end.
418- let lastName = basename ( url . path ) . substring ( 0 , 250 ) ;
419- if ( lastName . endsWith ( ".zip" ) ) {
420- lastName = lastName . substring ( 0 , lastName . length - 4 ) ;
418+ // The number of tries to use when generating a unique filename before
419+ // giving up and using a nanoid.
420+ const DUPLICATE_FILENAMES_TRIES = 10_000 ;
421+
422+ async function getStorageFolder (
423+ storagePath : string ,
424+ urlStr : string ,
425+ nameOverrride ?: string ,
426+ ) {
427+ let lastName : string ;
428+
429+ if ( nameOverrride ) {
430+ lastName = createFilenameFromString ( nameOverrride ) ;
431+ } else {
432+ // we need to generate a folder name for the unzipped archive,
433+ // this needs to be human readable since we may use this name as the initial
434+ // name for the database
435+ const url = Uri . parse ( urlStr ) ;
436+ // MacOS has a max filename length of 255
437+ // and remove a few extra chars in case we need to add a counter at the end.
438+ lastName = basename ( url . path ) . substring ( 0 , 250 ) ;
439+ if ( lastName . endsWith ( ".zip" ) ) {
440+ lastName = lastName . substring ( 0 , lastName . length - 4 ) ;
441+ }
421442 }
422443
423444 const realpath = await fs_realpath ( storagePath ) ;
424- let folderName = join ( realpath , lastName ) ;
445+ let folderName = lastName ;
446+
447+ // get all existing files instead of calling pathExists on every
448+ // single combination of realpath and folderName
449+ const existingFiles = await readdir ( realpath ) ;
425450
426451 // avoid overwriting existing folders
427452 let counter = 0 ;
428- while ( await pathExists ( folderName ) ) {
453+ while ( existingFiles . includes ( basename ( folderName ) ) ) {
429454 counter ++ ;
430- folderName = join ( realpath , `${ lastName } -${ counter } ` ) ;
431- if ( counter > 100 ) {
432- throw new Error ( "Could not find a unique name for downloaded database." ) ;
455+
456+ if ( counter <= DUPLICATE_FILENAMES_TRIES ) {
457+ // First try to use a counter to make the name unique.
458+ folderName = `${ lastName } -${ counter } ` ;
459+ } else if ( counter <= DUPLICATE_FILENAMES_TRIES + 5 ) {
460+ // If there are more than 10,000 similarly named databases,
461+ // give up on using a counter and use a random string instead.
462+ folderName = `${ lastName } -${ nanoid ( ) } ` ;
463+ } else {
464+ // This should almost never happen, but just in case, we don't want to
465+ // get stuck in an infinite loop.
466+ throw new Error (
467+ "Could not find a unique name for downloaded database. Please remove some databases and try again." ,
468+ ) ;
433469 }
434470 }
435- return folderName ;
471+ return join ( realpath , folderName ) ;
436472}
437473
438474function validateUrl ( databaseUrl : string ) {
0 commit comments