diff --git a/build/locales/en/translations.json b/build/locales/en/translations.json index b392f3ba..41ccc6c3 100644 --- a/build/locales/en/translations.json +++ b/build/locales/en/translations.json @@ -152,6 +152,10 @@ "confirmButtonText": "OK", "title": "Database export", "message": "Exported successfully" + }, + "loadingWindow": { + "description": "Exporting DB ...", + "archiveSize": "archive size {{prettyArchiveSize}}" } }, "importDB": { diff --git a/build/locales/ru/translations.json b/build/locales/ru/translations.json index e96ab7c1..8c808737 100644 --- a/build/locales/ru/translations.json +++ b/build/locales/ru/translations.json @@ -152,6 +152,10 @@ "confirmButtonText": "OK", "title": "Экспорт базы данных", "message": "Экспортирована успешно" + }, + "loadingWindow": { + "description": "Экспортирование БД ...", + "archiveSize": "размер архива {{prettyArchiveSize}}" } }, "importDB": { diff --git a/package.json b/package.json index 42c82aff..264e498b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ }, "license": "Apache-2.0", "dependencies": { - "archiver": "5.3.0", + "archiver": "7.0.1", "bfx-svc-test-helper": "git+https://github.com/bitfinexcom/bfx-svc-test-helper.git", "bittorrent-dht": "10.0.2", "changelog-parser": "3.0.1", diff --git a/src/archiver.js b/src/archiver.js index b4bb460a..6a4c86b0 100644 --- a/src/archiver.js +++ b/src/archiver.js @@ -9,17 +9,66 @@ const { InvalidFileNameInArchiveError } = require('./errors') -const zip = ( +const getTotalFilesStats = async (filePaths) => { + const promises = filePaths.map((filePath) => { + return fs.promises.stat(filePath) + }) + const stats = await Promise.all(promises) + const size = stats.reduce((size, stat) => { + return Number.isFinite(stat?.size) + ? size + stat.size + : size + }, 0) + + return { + size, + stats + } +} + +const bytesToSize = (bytes) => { + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] + + if (bytes <= 0) { + return '0 Byte' + } + + const i = Number.parseInt(Math.floor(Math.log(bytes) / Math.log(1024))) + const val = Math.round(bytes / Math.pow(1024, i), 2) + const size = sizes[i] + + return `${val} ${size}` +} + +const zip = async ( zipPath, - filePaths = [], - params = {} + filePaths, + params ) => { - return new Promise((resolve, reject) => { + const _filePaths = Array.isArray(filePaths) + ? filePaths + : [filePaths] + const { + size, + stats + } = await getTotalFilesStats(_filePaths) + + return new Promise((_resolve, _reject) => { + let interval = null + const resolve = (...args) => { + clearInterval(interval) + return _resolve(...args) + } + const reject = (err) => { + clearInterval(interval) + return _reject(err) + } + try { - const _filePaths = Array.isArray(filePaths) - ? filePaths - : [filePaths] - const { zlib } = { ...params } + const { + zlib, + progressHandler + } = params ?? {} const _params = { ...params, zlib: { @@ -36,16 +85,50 @@ const zip = ( archive.on('error', reject) archive.on('warning', reject) + if (typeof progressHandler === 'function') { + let processedBytes = 0 + + const asyncProgressHandler = async () => { + try { + if ( + !Number.isFinite(size) || + size === 0 || + !Number.isFinite(processedBytes) + ) { + return + } + + const progress = processedBytes / size + const archiveBytes = archive.pointer() + const prettyArchiveSize = bytesToSize(archiveBytes) + + await progressHandler({ + progress, + archiveBytes, + prettyArchiveSize + }) + } catch (err) { + console.debug(err) + } + } + + archive.on('progress', async (e) => { + processedBytes = e.fs.processedBytes ?? 0 + await asyncProgressHandler() + }) + interval = setInterval(asyncProgressHandler, 3000) + } + archive.pipe(output) - _filePaths.forEach((file) => { - const readStream = fs.createReadStream(file) - const name = path.basename(file) + for (const [i, filePath] of _filePaths.entries()) { + const readStream = fs.createReadStream(filePath) + const name = path.basename(filePath) readStream.on('error', reject) - archive.append(readStream, { name }) - }) + archive.append(readStream, { name, stats: stats[i] }) + } archive.finalize() } catch (err) { diff --git a/src/export-db.js b/src/export-db.js index 8ebc5e15..5cbbfc43 100644 --- a/src/export-db.js +++ b/src/export-db.js @@ -10,7 +10,8 @@ const showErrorModalDialog = require('./show-error-modal-dialog') const showMessageModalDialog = require('./show-message-modal-dialog') const { showLoadingWindow, - hideLoadingWindow + hideLoadingWindow, + setLoadingDescription } = require('./window-creators/change-loading-win-visibility-state') const wins = require('./window-creators/windows') const isMainWinAvailable = require('./helpers/is-main-win-available') @@ -67,13 +68,37 @@ module.exports = ({ throw new InvalidFilePathError() } - await showLoadingWindow() + await showLoadingWindow({ + description: i18next + .t('common.exportDB.loadingWindow.description') + }) + + const progressHandler = async (args) => { + const { + progress, + prettyArchiveSize + } = args ?? {} + + const _description = i18next.t('common.exportDB.loadingWindow.description') + const _archived = i18next.t( + 'common.exportDB.loadingWindow.archiveSize', + { prettyArchiveSize } + ) + + const archived = prettyArchiveSize + ? `
${_archived}` + : '' + const description = `${_description}${archived}` + + await setLoadingDescription({ progress, description }) + } + await zip(filePath, [ dbPath, dbShmPath, dbWalPath, secretKeyPath - ]) + ], { progressHandler }) await hideLoadingWindow() await showMessageModalDialog(win, { diff --git a/src/show-error-modal-dialog.js b/src/show-error-modal-dialog.js index a2d55ac1..abc10392 100644 --- a/src/show-error-modal-dialog.js +++ b/src/show-error-modal-dialog.js @@ -77,7 +77,7 @@ module.exports = async (win, title = 'Error', err) => { return _showErrorBox(win, title, message) } - const message = i18next.t('common.showErrorModalDialog.syncFrequencyChangingErrorMessage') + const message = i18next.t('common.showErrorModalDialog.unexpectedExceptionMessage') return _showErrorBox(win, title, message) } diff --git a/src/window-creators/change-loading-win-visibility-state.js b/src/window-creators/change-loading-win-visibility-state.js index 158a47ab..3962dd31 100644 --- a/src/window-creators/change-loading-win-visibility-state.js +++ b/src/window-creators/change-loading-win-visibility-state.js @@ -59,11 +59,12 @@ const _setParentWindow = (noParent) => { wins.loadingWindow.setParentWindow(win) } -const _runProgressLoader = (opts = {}) => { +const _runProgressLoader = (opts) => { const { win = wins.loadingWindow, - isIndeterminateMode = false - } = { ...opts } + isIndeterminateMode = false, + progress + } = opts ?? {} if ( !win || @@ -72,6 +73,20 @@ const _runProgressLoader = (opts = {}) => { ) { return } + if (Number.isFinite(progress)) { + if ( + progress >= 1 && + !isIndeterminateMode + ) { + win.setProgressBar(-0.1) + + return + } + + win.setProgressBar(progress) + + return + } if (isIndeterminateMode) { // Change to indeterminate mode when progress > 1 win.setProgressBar(1.1) @@ -83,14 +98,14 @@ const _runProgressLoader = (opts = {}) => { const duration = 3000 // ms const interval = duration / fps // ms const step = 1 / (duration / interval) - let progress = 0 + let _progress = 0 intervalMarker = setInterval(() => { - if (progress >= 1) { - progress = 0 + if (_progress >= 1) { + _progress = 0 } - progress += step + _progress += step if ( !win || @@ -102,7 +117,7 @@ const _runProgressLoader = (opts = {}) => { return } - win.setProgressBar(progress) + win.setProgressBar(_progress) }, interval).unref() } @@ -123,8 +138,14 @@ const _stopProgressLoader = ( win.setProgressBar(-0.1) } -const _setLoadingDescription = async (win, description) => { +const setLoadingDescription = async (params) => { try { + const { + win = wins.loadingWindow, + progress, + description = '' + } = params ?? {} + if ( !win || typeof win !== 'object' || @@ -134,11 +155,31 @@ const _setLoadingDescription = async (win, description) => { return } + const _progressPerc = ( + Number.isFinite(progress) && + progress > 0 + ) + ? Math.floor(progress * 100) + : null + const progressPerc = ( + Number.isFinite(_progressPerc) && + _progressPerc > 100 + ) + ? 100 + : _progressPerc + const descriptionChunk = description + ? `

${description}

` + : '

' + const progressChunk = Number.isFinite(progressPerc) + ? `

${progressPerc} %

` + : '

' + const _description = `${progressChunk}${descriptionChunk}` + const loadingDescReadyPromise = GeneralIpcChannelHandlers .onLoadingDescriptionReady() GeneralIpcChannelHandlers - .sendLoadingDescription(win, { description }) + .sendLoadingDescription(win, { description: _description }) const loadingRes = await loadingDescReadyPromise @@ -152,6 +193,7 @@ const _setLoadingDescription = async (win, description) => { const showLoadingWindow = async (opts) => { const { + progress, description = '', isRequiredToCloseAllWins = false, isNotRunProgressLoaderRequired = false, @@ -170,14 +212,18 @@ const showLoadingWindow = async (opts) => { _setParentWindow(isRequiredToCloseAllWins || noParent) - if (!isNotRunProgressLoaderRequired) { - _runProgressLoader({ isIndeterminateMode }) + const _progress = Number.isFinite(progress) + ? Math.floor(progress * 100) / 100 + : progress + + if ( + !isNotRunProgressLoaderRequired || + Number.isFinite(progress) + ) { + _runProgressLoader({ progress: _progress, isIndeterminateMode }) } - await _setLoadingDescription( - wins.loadingWindow, - description - ) + await setLoadingDescription({ progress: _progress, description }) if (!wins.loadingWindow.isVisible()) { centerWindow(wins.loadingWindow) @@ -197,10 +243,7 @@ const hideLoadingWindow = async (opts) => { } = opts ?? {} // need to empty description - await _setLoadingDescription( - wins.loadingWindow, - '' - ) + await setLoadingDescription({ description: '' }) _stopProgressLoader() if (isRequiredToShowMainWin) { @@ -224,5 +267,6 @@ const hideLoadingWindow = async (opts) => { module.exports = { showLoadingWindow, - hideLoadingWindow + hideLoadingWindow, + setLoadingDescription }