diff --git a/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts b/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts index 11265b932..e2da9c5a8 100644 --- a/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts +++ b/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts @@ -10,8 +10,9 @@ import { deleteDir, deleteDirContents, ensureDirectoryExists, - extractNestedZip, + extractZip, FileDoesNotExist, + moveDirContents, readJSONFile, } from '../Utils/FileUtils' import { timeout } from '../Utils/Utils' @@ -267,6 +268,30 @@ async function checkForUpdate(webContents: Electron.WebContents, mapping: Mappin } } +/** + * Some plugin zips may be served directly from GitHub's Releases feature, which automatically generates a zip file. + * However, the actual assets end up being nested inside a root level folder within that zip. + * We can detect if we have a legacy nested structure if after unzipping the zip we end up with + * only 1 entry and that entry is a folder. + */ +async function usesLegacyNestedFolderStructure(dir: string) { + const fileNames = await fs.promises.readdir(dir) + if (fileNames.length > 1) { + return false + } + + const stat = fs.lstatSync(path.join(dir, fileNames[0])) + return stat.isDirectory() +} + +async function unnestLegacyStructure(dir: string) { + const fileNames = await fs.promises.readdir(dir) + const sourceDir = path.join(dir, fileNames[0]) + const destDir = dir + + await moveDirContents(sourceDir, destDir) +} + async function installComponent( webContents: Electron.WebContents, mapping: MappingFileHandler, @@ -312,7 +337,12 @@ async function installComponent( ]) logMessage('Extracting', paths.downloadPath, 'to', paths.absolutePath) - await extractNestedZip(paths.downloadPath, paths.absolutePath) + await extractZip(paths.downloadPath, paths.absolutePath) + + const legacyStructure = await usesLegacyNestedFolderStructure(paths.absolutePath) + if (legacyStructure) { + await unnestLegacyStructure(paths.absolutePath) + } let main = 'index.html' try { diff --git a/packages/desktop/app/javascripts/Main/Utils/FileUtils.ts b/packages/desktop/app/javascripts/Main/Utils/FileUtils.ts index b2917f150..e0e2cec9d 100644 --- a/packages/desktop/app/javascripts/Main/Utils/FileUtils.ts +++ b/packages/desktop/app/javascripts/Main/Utils/FileUtils.ts @@ -169,31 +169,37 @@ export async function moveDirContents(srcDir: string, destDir: string): Promise< ) } -export async function extractNestedZip(source: string, dest: string): Promise { +export async function extractZip(source: string, dest: string): Promise { return new Promise((resolve, reject) => { yauzl.open(source, { lazyEntries: true, autoClose: true }, (err, zipFile) => { let cancelled = false + const tryReject = (err: Error) => { if (!cancelled) { cancelled = true reject(err) } } + if (err) { return tryReject(err) } + if (!zipFile) { return tryReject(new Error('zipFile === undefined')) } zipFile.readEntry() + zipFile.on('close', resolve) + zipFile.on('entry', (entry) => { if (cancelled) { return } - if (entry.fileName.endsWith('/')) { - /** entry is a directory, skip and read next entry */ + + const isEntryDirectory = entry.fileName.endsWith('/') + if (isEntryDirectory) { zipFile.readEntry() return } @@ -202,21 +208,19 @@ export async function extractNestedZip(source: string, dest: string): Promise { }) test('extracts a zip and unnests the folders by one level', async (t) => { - await extractNestedZip(path.join(dataPath, 'zip-file.zip'), zipFileDestination) + await extractZip(path.join(dataPath, 'zip-file.zip'), zipFileDestination) t.deepEqual(await fs.readdir(zipFileDestination), ['package.json', 'test-file.txt']) })