diff --git a/build/locales/en/translations.json b/build/locales/en/translations.json index 41ccc6c3..62603f97 100644 --- a/build/locales/en/translations.json +++ b/build/locales/en/translations.json @@ -165,6 +165,10 @@ }, "modalDialog": { "title": "Database import" + }, + "loadingWindow": { + "description": "Importing DB ...", + "unzippedBytes": "DB size {{prettyUnzippedBytes}}" } } }, diff --git a/build/locales/ru/translations.json b/build/locales/ru/translations.json index 8c808737..687ba602 100644 --- a/build/locales/ru/translations.json +++ b/build/locales/ru/translations.json @@ -165,6 +165,10 @@ }, "modalDialog": { "title": "Импорт базы данных" + }, + "loadingWindow": { + "description": "Импортирование БД ...", + "unzippedBytes": "размер БД {{prettyUnzippedBytes}}" } } }, diff --git a/package.json b/package.json index 264e498b..a07ec864 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "new-github-issue-url": "0.2.1", "showdown": "2.0.3", "truncate-utf8-bytes": "1.0.2", - "yauzl": "2.10.0" + "yauzl": "3.2.0" }, "devDependencies": { "@mapbox/node-pre-gyp": "1.0.11", diff --git a/src/archiver.js b/src/archiver.js index 6a4c86b0..eccbffc2 100644 --- a/src/archiver.js +++ b/src/archiver.js @@ -140,15 +140,72 @@ const zip = async ( const unzip = ( zipPath, folderPath, - params = {} + params ) => { - const { extractFiles } = { ...params } - const extractedfileNames = [] - let isClosedByError = false + const { + extractFiles, + progressHandler + } = params ?? {} + return new Promise((_resolve, _reject) => { + const entryStates = [] + let totalUncompressedSize = 0 + let unzippedBytes = 0 + let lastProgressEventMts = Date.now() + + const asyncProgressHandler = async () => { + try { + if (typeof progressHandler !== 'function') { + return + } + + if ( + !Number.isFinite(totalUncompressedSize) || + totalUncompressedSize === 0 || + !Number.isFinite(unzippedBytes) + ) { + return + } + + const progress = unzippedBytes / totalUncompressedSize + const prettyUnzippedBytes = bytesToSize(unzippedBytes) + + await progressHandler({ + progress, + unzippedBytes, + prettyUnzippedBytes + }) + } catch (err) { + console.debug(err) + } + } + const resolve = (entryState) => { + if (entryState) { + entryState.isClosedSuccessfully = true + } + if ( + entryStates.some((state) => state?.isClosedWithError) || + entryStates.some((state) => !state?.isClosedSuccessfully) + ) { + return + } + + asyncProgressHandler() + + return _resolve(entryStates.map((state) => state?.entry?.fileName)) + } + const reject = (err, zipfile, entryState) => { + if (entryState) { + entryState.isClosedWithError = true + } + if (zipfile) { + zipfile.close() + } + + return _reject(err) + } - return new Promise((resolve, reject) => { try { - yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => { + yauzl.open(zipPath, { lazyEntries: false }, (err, zipfile) => { if (err) { reject(err) @@ -156,67 +213,67 @@ const unzip = ( } zipfile.on('error', reject) - zipfile.on('end', () => { - if (isClosedByError) { - return - } - - resolve(extractedfileNames) - }) - zipfile.readEntry() - + zipfile.on('end', () => resolve()) zipfile.on('entry', (entry) => { const { fileName } = entry const filePath = path.join(folderPath, fileName) const errorMessage = yauzl.validateFileName(fileName) if (/\/$/.test(fileName)) { - zipfile.readEntry() - return } if ( Array.isArray(extractFiles) && extractFiles.every(file => file !== fileName) ) { - zipfile.readEntry() - return } + + const entryState = { + isClosedWithError: false, + isClosedSuccessfully: false, + entry + } + totalUncompressedSize += entry?.uncompressedSize ?? 0 + entryStates.push(entryState) + if (errorMessage) { - isClosedByError = true - zipfile.close() - reject(new InvalidFileNameInArchiveError(errorMessage)) + reject( + new InvalidFileNameInArchiveError(errorMessage), + zipfile, + entryState + ) return } zipfile.openReadStream(entry, (err, readStream) => { if (err) { - isClosedByError = true - zipfile.close() - reject(err) + reject(err, zipfile, entryState) return } const output = fs.createWriteStream(filePath) - output.on('close', () => { - extractedfileNames.push(fileName) - - zipfile.readEntry() - }) + output.on('close', () => resolve(entryState)) output.on('error', (err) => { - isClosedByError = true - zipfile.close() - reject(err) + reject(err, zipfile, entryState) }) readStream.on('error', (err) => { - isClosedByError = true - zipfile.close() - reject(err) + reject(err, zipfile, entryState) + }) + readStream.on('data', (chunk) => { + unzippedBytes += chunk.length + const currMts = Date.now() + + if (currMts - lastProgressEventMts < 500) { + return + } + + lastProgressEventMts = currMts + asyncProgressHandler() }) readStream.pipe(output) diff --git a/src/import-db.js b/src/import-db.js index 58e3e646..a940a2f0 100644 --- a/src/import-db.js +++ b/src/import-db.js @@ -13,6 +13,9 @@ const pauseApp = require('./pause-app') const relaunch = require('./relaunch') const { rm, isMainWinAvailable } = require('./helpers') const wins = require('./window-creators/windows') +const { + setLoadingDescription +} = require('./window-creators/change-loading-win-visibility-state') const { DB_FILE_NAME, DB_SHM_FILE_NAME, @@ -78,7 +81,33 @@ module.exports = ({ throw new InvalidFilePathError() } - await pauseApp() + const progressHandler = async (args) => { + const { + progress, + prettyUnzippedBytes + } = args ?? {} + + const _description = i18next + .t('common.importDB.loadingWindow.description') + const _unzipped = i18next.t( + 'common.importDB.loadingWindow.unzippedBytes', + { prettyUnzippedBytes } + ) + + const unzipped = prettyUnzippedBytes + ? `
${_unzipped}` + : '' + const description = `${_description}${unzipped}` + + await setLoadingDescription({ progress, description }) + } + + await pauseApp({ + loadingWinParams: { + description: i18next + .t('common.importDB.loadingWindow.description') + } + }) await _rmDbExcludeMain(pathToUserData, DB_FILE_NAME) const extractedfileNames = await unzip( filePaths[0], @@ -89,7 +118,8 @@ module.exports = ({ DB_SHM_FILE_NAME, DB_WAL_FILE_NAME, SECRET_KEY_FILE_NAME - ] + ], + progressHandler } ) @@ -100,8 +130,11 @@ module.exports = ({ relaunch() } catch (err) { try { + const _win = isMainWinAvailable(wins.loadingWindow) + ? wins.loadingWindow + : win await showErrorModalDialog( - win, + _win, i18next.t('common.importDB.modalDialog.title'), err ) diff --git a/src/pause-app.js b/src/pause-app.js index 3883dd78..96d513b2 100644 --- a/src/pause-app.js +++ b/src/pause-app.js @@ -28,12 +28,16 @@ const _closeServer = () => { }) } -module.exports = async (opts = {}) => { +module.exports = async (opts) => { const { - beforeClosingServHook = () => {} - } = opts + beforeClosingServHook = () => {}, + loadingWinParams + } = opts ?? {} - await showLoadingWindow({ isRequiredToCloseAllWins: true }) + await showLoadingWindow({ + isRequiredToCloseAllWins: true, + ...loadingWinParams + }) await beforeClosingServHook()