fix(desktop): plugin installation

This commit is contained in:
Mo
2022-10-14 09:35:37 -05:00
parent a3a8acdd55
commit dd55c63955
3 changed files with 49 additions and 15 deletions

View File

@@ -10,8 +10,9 @@ import {
deleteDir, deleteDir,
deleteDirContents, deleteDirContents,
ensureDirectoryExists, ensureDirectoryExists,
extractNestedZip, extractZip,
FileDoesNotExist, FileDoesNotExist,
moveDirContents,
readJSONFile, readJSONFile,
} from '../Utils/FileUtils' } from '../Utils/FileUtils'
import { timeout } from '../Utils/Utils' 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( async function installComponent(
webContents: Electron.WebContents, webContents: Electron.WebContents,
mapping: MappingFileHandler, mapping: MappingFileHandler,
@@ -312,7 +337,12 @@ async function installComponent(
]) ])
logMessage('Extracting', paths.downloadPath, 'to', paths.absolutePath) 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' let main = 'index.html'
try { try {

View File

@@ -169,31 +169,37 @@ export async function moveDirContents(srcDir: string, destDir: string): Promise<
) )
} }
export async function extractNestedZip(source: string, dest: string): Promise<void> { export async function extractZip(source: string, dest: string): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
yauzl.open(source, { lazyEntries: true, autoClose: true }, (err, zipFile) => { yauzl.open(source, { lazyEntries: true, autoClose: true }, (err, zipFile) => {
let cancelled = false let cancelled = false
const tryReject = (err: Error) => { const tryReject = (err: Error) => {
if (!cancelled) { if (!cancelled) {
cancelled = true cancelled = true
reject(err) reject(err)
} }
} }
if (err) { if (err) {
return tryReject(err) return tryReject(err)
} }
if (!zipFile) { if (!zipFile) {
return tryReject(new Error('zipFile === undefined')) return tryReject(new Error('zipFile === undefined'))
} }
zipFile.readEntry() zipFile.readEntry()
zipFile.on('close', resolve) zipFile.on('close', resolve)
zipFile.on('entry', (entry) => { zipFile.on('entry', (entry) => {
if (cancelled) { if (cancelled) {
return return
} }
if (entry.fileName.endsWith('/')) {
/** entry is a directory, skip and read next entry */ const isEntryDirectory = entry.fileName.endsWith('/')
if (isEntryDirectory) {
zipFile.readEntry() zipFile.readEntry()
return return
} }
@@ -202,21 +208,19 @@ export async function extractNestedZip(source: string, dest: string): Promise<vo
if (cancelled) { if (cancelled) {
return return
} }
if (err) { if (err) {
return tryReject(err) return tryReject(err)
} }
if (!stream) { if (!stream) {
return tryReject(new Error('stream === undefined')) return tryReject(new Error('stream === undefined'))
} }
stream.on('error', tryReject) stream.on('error', tryReject)
const filepath = path.join(
dest, const filepath = path.join(dest, entry.fileName)
/**
* Remove the first element of the entry's path, which is the base
* directory we want to ignore
*/
entry.fileName.substring(entry.fileName.indexOf('/') + 1),
)
try { try {
await ensureDirectoryExists(path.dirname(filepath)) await ensureDirectoryExists(path.dirname(filepath))
} catch (error: any) { } catch (error: any) {

View File

@@ -4,7 +4,7 @@ import path from 'path'
import { import {
deleteDir, deleteDir,
ensureDirectoryExists, ensureDirectoryExists,
extractNestedZip, extractZip,
FileDoesNotExist, FileDoesNotExist,
moveDirContents, moveDirContents,
readJSONFile, readJSONFile,
@@ -25,7 +25,7 @@ test.afterEach(async () => {
}) })
test('extracts a zip and unnests the folders by one level', async (t) => { 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']) t.deepEqual(await fs.readdir(zipFileDestination), ['package.json', 'test-file.txt'])
}) })