diff --git a/.github/workflows/build-old.yml b/.github/workflows/build-old.yml new file mode 100644 index 000000000..af960ab7d --- /dev/null +++ b/.github/workflows/build-old.yml @@ -0,0 +1,57 @@ +name: Build/release-old + +on: + push: + tags: + - v* + +jobs: + release: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + + steps: + - name: Check out Git repository + uses: actions/checkout@v2 + with: + submodules: recursive + + - name: Install Node.js, NPM and Yarn + uses: actions/setup-node@v2.1.5 + with: + node-version: 16 + + - name: Prepare for app notarization + if: startsWith(matrix.os, 'macos') + # Import Apple API key for app notarization on macOS + run: | + mkdir -p ~/private_keys/ + echo '${{ secrets.api_key_old }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id_old }}.p8 + + - name: Install libarchive-tools for pacman build # Related https://github.com/electron-userland/electron-builder/issues/4181 + if: startsWith(matrix.os, 'ubuntu') + run: sudo apt-get install libarchive-tools + + - name: Electron Builder Action + uses: samuelmeuli/action-electron-builder@v1.6.0 + with: + # GitHub token, automatically provided to the action + # (No need to define this secret in the repo settings) + github_token: ${{ secrets.github_token }} + + # If the commit is tagged with a version (e.g. "v1.0.0"), + # release the app after building + release: ${{ startsWith(github.ref, 'refs/tags/v') }} + + mac_certs: ${{ secrets.mac_certs_old }} + mac_certs_password: ${{ secrets.mac_certs_password_old }} + env: + # macOS notarization API key + API_KEY_ID: ${{ secrets.api_key_id_old }} + API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id_old }} + # setry crash reporting token + SENTRY_AUTH_TOKEN: ${{secrets.sentry_auth_token}} + USE_HARD_LINKS: false diff --git a/.gitignore b/.gitignore index 6805edf2f..cd357cec5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ buildingSteps.md .DS_Store .idea/ build/.DS_Store +.env +.electron-symbols/ diff --git a/.gitmodules b/.gitmodules index cdc9c9559..f2834d8e6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "bada-frame"] path = ui url = https://github.com/ente-io/bada-frame - branch = demo + branch = release [submodule "thirdparty/next-electron-server"] path = thirdparty/next-electron-server url = https://github.com/ente-io/next-electron-server.git diff --git a/README.md b/README.md index d71fe5d8f..78b05261d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# bhari-frame (heavy-frame) +# ente Photos - Desktop Desktop app for [ente.io](https://ente.io) build with [electron](https://electronjs.org) and loads of ❤️. @@ -10,7 +10,7 @@ The goal of this app was to 1. provide a stable environment for customers to back up large amounts of data reliably 2. export uploaded data from our servers to their local hard drives. -Electron was the best way to reuse our battle tested code from [bada-frame](https://github.com/ente-io/bada-frame) that powers [web.ente.io](https://web.ente.io). +Electron was the best way to reuse our battle tested code from [photos-web](https://github.com/ente-io/photos-web) that powers [web.ente.io](https://web.ente.io). As an archival solution built by a small team, we are hopeful that this project will help us keep our stack lean, while ensuring a painfree life for our customers. @@ -18,12 +18,8 @@ If you are running into issues with this app, please drop a mail to [support@ent ## Download -- [Latest Release](https://github.com/ente-io/bhari-frame/releases/latest) +- [Latest Release](https://github.com/ente-io/photos-desktop/releases/latest) -*User contributed ports* - -- [AUR](https://aur.archlinux.org/packages/ente-desktop-appimage): - `yay -S ente-desktop-appimage` ## Building from source @@ -33,10 +29,10 @@ fetch and build from source. ```bash # Clone this repository -git clone https://github.com/ente-io/bhari-frame +git clone https://github.com/ente-io/photos-desktop # Go into the repository -cd bhari-frame +cd photos-desktop # Clone submodules (recursively) git submodule update --init --recursive diff --git a/build/image-magick b/build/image-magick new file mode 100755 index 000000000..d5d5062e2 Binary files /dev/null and b/build/image-magick differ diff --git a/build/taskbar-icon-Template.png b/build/taskbar-icon-Template.png new file mode 100644 index 000000000..9645cedca Binary files /dev/null and b/build/taskbar-icon-Template.png differ diff --git a/build/taskbar-icon-Template@2x.png b/build/taskbar-icon-Template@2x.png new file mode 100644 index 000000000..355cc03c1 Binary files /dev/null and b/build/taskbar-icon-Template@2x.png differ diff --git a/build/taskbar-icon-Template@3x.png b/build/taskbar-icon-Template@3x.png new file mode 100644 index 000000000..8046b49a4 Binary files /dev/null and b/build/taskbar-icon-Template@3x.png differ diff --git a/package.json b/package.json index 8234ab068..929fc09e3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ente", "productName": "ente", - "version": "1.7.0-alpha.7", + "version": "1.6.16", "private": true, "description": "Desktop client for ente.io", "main": "app/main.js", @@ -39,6 +39,11 @@ ] } ], + "asarUnpack": [ + "node_modules/ffmpeg-static/bin/${os}/${arch}/ffmpeg", + "node_modules/ffmpeg-static/index.js", + "node_modules/ffmpeg-static/package.json" + ], "files": [ "app/**/*", { @@ -63,17 +68,22 @@ "start": "concurrently \"yarn start-main\" \"yarn start-renderer\"", "build-renderer": "cd ui && yarn install && yarn build && cd ..", "build": "yarn build-renderer && yarn build-main", - "test-release": "yarn build && electron-builder" + "test-release": "yarn build && electron-builder --config.compression=store" }, "author": "ente ", "devDependencies": { "@sentry/cli": "^1.68.0", "@types/auto-launch": "^5.0.2", + "@types/ffmpeg-static": "^3.0.1", "@types/get-folder-size": "^2.0.0", + "@types/node": "^16.18.3", + "@types/node-fetch": "^2.6.2", + "@types/promise-fs": "^2.1.1", + "@types/semver-compare": "^1.0.1", "@typescript-eslint/eslint-plugin": "^5.28.0", "@typescript-eslint/parser": "^5.28.0", "concurrently": "^7.0.0", - "electron": "^15.3.0", + "electron": "^21.2.2", "electron-builder": "^23.0.3", "electron-builder-notarize": "^1.2.0", "electron-download": "^4.1.1", @@ -87,18 +97,20 @@ }, "dependencies": { "@sentry/electron": "^2.5.1", - "@types/node": "^14.14.37", - "@types/promise-fs": "^2.1.1", - "chokidar": "^3.5.3", + "any-shell-escape": "^0.1.1", "auto-launch": "^5.0.5", + "chokidar": "^3.5.3", "electron-log": "^4.3.5", "electron-reload": "^2.0.0-alpha.1", "electron-store": "^8.0.1", "electron-updater": "^4.3.8", + "ffmpeg-static": "^5.1.0", "get-folder-size": "^2.0.1", "next-electron-server": "file:./thirdparty/next-electron-server", + "node-fetch": "^2.6.7", "node-stream-zip": "^1.15.0", - "promise-fs": "^2.1.1" + "promise-fs": "^2.1.1", + "semver-compare": "^1.0.0" }, "standard": { "parser": "babel-eslint" @@ -109,4 +121,4 @@ "prettier --write --ignore-unknown" ] } -} +} \ No newline at end of file diff --git a/sentry-symbols.js b/sentry-symbols.js index 478d9caaa..955cda5f5 100644 --- a/sentry-symbols.js +++ b/sentry-symbols.js @@ -12,15 +12,13 @@ try { process.exit(1); } -const VERSION = /\bv?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?\b/i; const SYMBOL_CACHE_FOLDER = '.electron-symbols'; -const package = require('./package.json'); const sentryCli = new SentryCli('./sentry.properties'); async function main() { - let version = getElectronVersion(); + const version = getElectronVersion(); if (!version) { - console.error('Cannot detect electron version, check package.json'); + console.error('Cannot detect electron version, check that electron is installed'); return; } @@ -68,20 +66,11 @@ async function main() { } function getElectronVersion() { - if (!package) { - return false; + try { + return require('electron/package.json').version; + } catch (error) { + return undefined; } - - let electronVersion = - (package.dependencies && package.dependencies.electron) || - (package.devDependencies && package.devDependencies.electron); - - if (!electronVersion) { - return false; - } - - const matches = VERSION.exec(electronVersion); - return matches ? matches[0] : false; } async function downloadSymbols(options) { diff --git a/sentry.properties b/sentry.properties index a8d4fdb3e..2bbdc2cc8 100644 --- a/sentry.properties +++ b/sentry.properties @@ -1,4 +1,3 @@ defaults.url=https://sentry.ente.io/ defaults.org=ente defaults.project=bhari-frame -cli.executable=../../../../usr/local/lib/node_modules/@sentry/wizard/node_modules/@sentry/cli/bin/sentry-cli diff --git a/src/api/common.ts b/src/api/common.ts index 1e20bc1f1..376ecfdb7 100644 --- a/src/api/common.ts +++ b/src/api/common.ts @@ -1,5 +1,5 @@ import { ipcRenderer } from 'electron/renderer'; -import { logError } from '../utils/logging'; +import { logError } from '../services/logging'; export const selectRootDirectory = async (): Promise => { try { @@ -8,3 +8,18 @@ export const selectRootDirectory = async (): Promise => { logError(e, 'error while selecting root directory'); } }; + +export const getAppVersion = async (): Promise => { + try { + return await ipcRenderer.invoke('get-app-version'); + } catch (e) { + logError(e, 'failed to get release version'); + throw e; + } +}; + +export { + logToDisk, + openLogDirectory, + getSentryUserID, +} from '../services/logging'; diff --git a/src/api/electronStore.ts b/src/api/electronStore.ts index 720293186..39a3b9df2 100644 --- a/src/api/electronStore.ts +++ b/src/api/electronStore.ts @@ -1,7 +1,7 @@ import { keysStore } from '../stores/keys.store'; import { safeStorageStore } from '../stores/safeStorage.store'; import { uploadStatusStore } from '../stores/upload.store'; -import { logError } from '../utils/logging'; +import { logError } from '../services/logging'; export const clearElectronStore = () => { try { diff --git a/src/api/export.ts b/src/api/export.ts index 16d29d9b5..93e6b4f4d 100644 --- a/src/api/export.ts +++ b/src/api/export.ts @@ -1,6 +1,6 @@ import { readTextFile, writeStream } from './../services/fs'; import { ipcRenderer } from 'electron'; -import { logError } from '../utils/logging'; +import { logError } from '../services/logging'; import * as fs from 'promise-fs'; export const exists = (path: string) => { diff --git a/src/api/ffmpeg.ts b/src/api/ffmpeg.ts new file mode 100644 index 000000000..1b0f57068 --- /dev/null +++ b/src/api/ffmpeg.ts @@ -0,0 +1,41 @@ +import { ipcRenderer } from 'electron'; +import { existsSync } from 'fs'; +import { logError } from '../services/logging'; +import { ElectronFile } from '../types'; + +export async function runFFmpegCmd( + cmd: string[], + inputFile: File | ElectronFile, + outputFileName: string +) { + let inputFilePath = null; + let createdTempInputFile = null; + try { + if (!existsSync(inputFile.path)) { + const inputFileData = new Uint8Array(await inputFile.arrayBuffer()); + inputFilePath = await ipcRenderer.invoke( + 'write-temp-file', + inputFileData, + inputFile.name + ); + createdTempInputFile = true; + } else { + inputFilePath = inputFile.path; + } + const outputFileData = await ipcRenderer.invoke( + 'run-ffmpeg-cmd', + cmd, + inputFilePath, + outputFileName + ); + return new File([outputFileData], outputFileName); + } finally { + if (createdTempInputFile) { + try { + await ipcRenderer.invoke('remove-temp-file', inputFilePath); + } catch (e) { + logError(e, 'failed to deleteTempFile'); + } + } + } +} diff --git a/src/api/heicConvert.ts b/src/api/heicConvert.ts new file mode 100644 index 000000000..68909b678 --- /dev/null +++ b/src/api/heicConvert.ts @@ -0,0 +1,9 @@ +import { ipcRenderer } from 'electron/renderer'; + +export async function convertHEIC(fileData: Uint8Array): Promise { + const convertedFileData = await ipcRenderer.invoke( + 'convert-heic', + fileData + ); + return convertedFileData; +} diff --git a/src/api/safeStorage.ts b/src/api/safeStorage.ts index 35ce2478d..3f5bcf60e 100644 --- a/src/api/safeStorage.ts +++ b/src/api/safeStorage.ts @@ -1,6 +1,6 @@ import { ipcRenderer } from 'electron'; import { safeStorageStore } from '../stores/safeStorage.store'; -import { logError } from '../utils/logging'; +import { logError } from '../services/logging'; export async function setEncryptionKey(encryptionKey: string) { try { diff --git a/src/api/system.ts b/src/api/system.ts index 2267ac522..cba15edc7 100644 --- a/src/api/system.ts +++ b/src/api/system.ts @@ -1,4 +1,5 @@ import { ipcRenderer } from 'electron'; +import { AppUpdateInfo } from '../types'; export const sendNotification = (content: string) => { ipcRenderer.send('send-notification', content); @@ -9,3 +10,20 @@ export const showOnTray = (content: string) => { export const reloadWindow = () => { ipcRenderer.send('reload-window'); }; + +export const registerUpdateEventListener = ( + showUpdateDialog: (updateInfo: AppUpdateInfo) => void +) => { + ipcRenderer.removeAllListeners('show-update-dialog'); + ipcRenderer.on('show-update-dialog', (_, updateInfo: AppUpdateInfo) => { + showUpdateDialog(updateInfo); + }); +}; + +export const updateAndRestart = () => { + ipcRenderer.send('update-and-restart'); +}; + +export const skipAppVersion = (version: string) => { + ipcRenderer.send('skip-app-version', version); +}; diff --git a/src/api/upload.ts b/src/api/upload.ts index 83eb1a0b7..d6611763f 100644 --- a/src/api/upload.ts +++ b/src/api/upload.ts @@ -1,7 +1,7 @@ import { getElectronFile } from './../services/fs'; import { uploadStatusStore } from '../stores/upload.store'; import { ElectronFile, FILE_PATH_TYPE } from '../types'; -import { logError } from '../utils/logging'; +import { logError } from '../services/logging'; import { ipcRenderer } from 'electron'; import { getElectronFilesFromGoogleZip, @@ -18,7 +18,10 @@ export const getPendingUploads = async () => { if (zipPaths.length) { type = FILE_PATH_TYPE.ZIPS; for (const zipPath of zipPaths) { - files.push(...(await getElectronFilesFromGoogleZip(zipPath))); + files = [ + ...files, + ...(await getElectronFilesFromGoogleZip(zipPath)), + ]; } const pendingFilePaths = new Set(filePaths); files = files.filter((file) => pendingFilePaths.has(file.path)); @@ -62,10 +65,13 @@ export const showUploadZipDialog = async () => { const filePaths: string[] = await ipcRenderer.invoke( 'show-upload-zip-dialog' ); - const files: ElectronFile[] = []; + let files: ElectronFile[] = []; for (const filePath of filePaths) { - files.push(...(await getElectronFilesFromGoogleZip(filePath))); + files = [ + ...files, + ...(await getElectronFilesFromGoogleZip(filePath)), + ]; } return { diff --git a/src/config/index.ts b/src/config/index.ts index f427bb82f..6b2e8a1f4 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,6 +1,20 @@ const PROD_HOST_URL: string = 'ente://app'; const RENDERER_OUTPUT_DIR: string = './ui/out'; +const LOG_FILENAME = 'ente.log'; +const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024; -export { PROD_HOST_URL, RENDERER_OUTPUT_DIR, FILE_STREAM_CHUNK_SIZE }; +const SENTRY_DSN = 'https://e9268b784d1042a7a116f53c58ad2165@sentry.ente.io/5'; + +const RELEASE_VERSION = require('../../package.json').version; + +export { + PROD_HOST_URL, + RENDERER_OUTPUT_DIR, + FILE_STREAM_CHUNK_SIZE, + LOG_FILENAME, + MAX_LOG_SIZE, + SENTRY_DSN, + RELEASE_VERSION, +}; diff --git a/src/main.ts b/src/main.ts index c0ef1c868..b4bbca763 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,8 +13,12 @@ import { setupNextElectronServe, enableSharedArrayBufferSupport, handleDockIconHideOnAutoLaunch, + logSystemInfo, } from './utils/main'; import { initSentry } from './services/sentry'; +import { setupLogging } from './utils/logging'; +import { isDev } from './utils/common'; +import { setupMainProcessStatsLogger } from './utils/processStats'; let mainWindow: BrowserWindow; @@ -41,6 +45,8 @@ setupMainHotReload(); setupNextElectronServe(); +setupLogging(isDev); + const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); @@ -62,14 +68,16 @@ if (!gotTheLock) { // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.on('ready', async () => { + logSystemInfo(); + setupMainProcessStatsLogger(); + initSentry(); mainWindow = await createWindow(); const tray = setupTrayItem(mainWindow); const watcher = initWatcher(mainWindow); setupMacWindowOnDockIconClick(); - initSentry(); setupMainMenu(); setupIpcComs(tray, mainWindow, watcher); - handleUpdates(mainWindow, tray); + handleUpdates(mainWindow); handleDownloads(mainWindow); addAllowOriginHeader(mainWindow); }); diff --git a/src/preload.ts b/src/preload.ts index 8db7516e7..504a3016e 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,4 +1,11 @@ -import { reloadWindow, sendNotification, showOnTray } from './api/system'; +import { + registerUpdateEventListener, + reloadWindow, + sendNotification, + showOnTray, + updateAndRestart, + skipAppVersion, +} from './api/system'; import { showUploadDirsDialog, showUploadFilesDialog, @@ -32,11 +39,23 @@ import { setExportRecord, exists, } from './api/export'; -import { selectRootDirectory } from './api/common'; +import { + selectRootDirectory, + logToDisk, + openLogDirectory, + getSentryUserID, + getAppVersion, +} from './api/common'; import { fixHotReloadNext12 } from './utils/preload'; import { isFolder, getDirFiles } from './api/fs'; +import { convertHEIC } from './api/heicConvert'; +import { setupLogging } from './utils/logging'; +import { setupRendererProcessStatsLogger } from './utils/processStats'; +import { runFFmpegCmd } from './api/ffmpeg'; fixHotReloadNext12(); +setupLogging(); +setupRendererProcessStatsLogger(); const windowObject: any = window; @@ -76,4 +95,13 @@ windowObject['ElectronAPIs'] = { isFolder, updateWatchMappingSyncedFiles, updateWatchMappingIgnoredFiles, + logToDisk, + convertHEIC, + openLogDirectory, + registerUpdateEventListener, + updateAndRestart, + skipAppVersion, + getSentryUserID, + getAppVersion, + runFFmpegCmd, }; diff --git a/src/services/appUpdater.ts b/src/services/appUpdater.ts index 7279516e0..533b24fef 100644 --- a/src/services/appUpdater.ts +++ b/src/services/appUpdater.ts @@ -1,39 +1,117 @@ -import { BrowserWindow, dialog, Tray } from 'electron'; +import { app, BrowserWindow } from 'electron'; import { autoUpdater } from 'electron-updater'; import log from 'electron-log'; import { setIsAppQuitting, setIsUpdateAvailable } from '../main'; -import { buildContextMenu } from '../utils/menu'; +import semVerCmp from 'semver-compare'; +import { AppUpdateInfo, GetFeatureFlagResponse } from '../types'; +import { getSkipAppVersion, setSkipAppVersion } from './userPreference'; +import fetch from 'node-fetch'; +import { logErrorSentry } from './sentry'; +import ElectronLog from 'electron-log'; +import { isPlatform } from '../utils/main'; -class AppUpdater { - constructor() { - log.transports.file.level = 'debug'; - autoUpdater.logger = log; - } +const FIVE_MIN_IN_MICROSECOND = 5 * 60 * 1000; - async checkForUpdate(tray: Tray, mainWindow: BrowserWindow) { - await autoUpdater.checkForUpdatesAndNotify(); - autoUpdater.on('update-downloaded', () => { - showUpdateDialog(); - setIsUpdateAvailable(true); - tray.setContextMenu(buildContextMenu(mainWindow)); - }); +export function setupAutoUpdater() { + autoUpdater.logger = log; + autoUpdater.autoDownload = false; +} + +export async function checkForUpdateAndNotify(mainWindow: BrowserWindow) { + try { + log.debug('checkForUpdateAndNotify called'); + const updateCheckResult = await autoUpdater.checkForUpdates(); + log.debug('update version', updateCheckResult.updateInfo.version); + if ( + semVerCmp(updateCheckResult.updateInfo.version, app.getVersion()) <= + 0 + ) { + log.debug('already at latest version'); + return; + } + const skipAppVersion = getSkipAppVersion(); + if ( + skipAppVersion && + updateCheckResult.updateInfo.version === skipAppVersion + ) { + log.info( + 'user chose to skip version ', + updateCheckResult.updateInfo.version + ); + return; + } + const desktopCutoffVersion = await getDesktopCutoffVersion(); + if ( + desktopCutoffVersion && + isPlatform('mac') && + semVerCmp( + updateCheckResult.updateInfo.version, + desktopCutoffVersion + ) > 0 + ) { + log.debug('auto update not possible due to key change'); + showUpdateDialog(mainWindow, { + autoUpdatable: false, + version: updateCheckResult.updateInfo.version, + }); + } else { + let timeout: NodeJS.Timeout; + log.debug('attempting auto update'); + autoUpdater.downloadUpdate(); + autoUpdater.on('update-downloaded', () => { + timeout = setTimeout( + () => + showUpdateDialog(mainWindow, { + autoUpdatable: true, + version: updateCheckResult.updateInfo.version, + }), + FIVE_MIN_IN_MICROSECOND + ); + }); + autoUpdater.on('error', (error) => { + clearTimeout(timeout); + logErrorSentry(error, 'auto update failed'); + showUpdateDialog(mainWindow, { + autoUpdatable: false, + version: updateCheckResult.updateInfo.version, + }); + }); + } + setIsUpdateAvailable(true); + } catch (e) { + logErrorSentry(e, 'checkForUpdateAndNotify failed'); } } -export default new AppUpdater(); +export function updateAndRestart() { + ElectronLog.log('user quit the app'); + setIsAppQuitting(true); + autoUpdater.quitAndInstall(); +} -export const showUpdateDialog = (): void => { - dialog - .showMessageBox({ - type: 'info', - title: 'Install update', - message: 'Restart to update to the latest version of ente', - buttons: ['Later', 'Restart now'], - }) - .then((buttonIndex) => { - if (buttonIndex.response === 1) { - setIsAppQuitting(true); - autoUpdater.quitAndInstall(); - } - }); -}; +export function getAppVersion() { + return `v${app.getVersion()}`; +} + +export function skipAppVersion(version: string) { + setSkipAppVersion(version); +} + +async function getDesktopCutoffVersion() { + try { + const featureFlags = ( + await fetch('https://static.ente.io/feature_flags.json') + ).json() as GetFeatureFlagResponse; + return featureFlags.desktopCutoffVersion; + } catch (e) { + logErrorSentry(e, 'failed to get feature flags'); + return undefined; + } +} + +function showUpdateDialog( + mainWindow: BrowserWindow, + updateInfo: AppUpdateInfo +) { + mainWindow.webContents.send('show-update-dialog', updateInfo); +} diff --git a/src/services/autoLauncher.ts b/src/services/autoLauncher.ts index 80111ea46..c28eb177c 100644 --- a/src/services/autoLauncher.ts +++ b/src/services/autoLauncher.ts @@ -1,12 +1,12 @@ +import { isPlatform } from '../utils/main'; import { AutoLauncherClient } from '../types/autoLauncher'; -import { isPlatformWindows, isPlatformMac } from '../utils/main'; import linuxAutoLauncher from './autoLauncherClients/linuxAutoLauncher'; import macAndWindowsAutoLauncher from './autoLauncherClients/macAndWindowsAutoLauncher'; class AutoLauncher { private client: AutoLauncherClient; init() { - if (isPlatformMac() || isPlatformWindows()) { + if (isPlatform('mac') || isPlatform('windows')) { this.client = macAndWindowsAutoLauncher; } else { this.client = linuxAutoLauncher; diff --git a/src/services/chokidar.ts b/src/services/chokidar.ts index ac77fc5d2..4fea900d9 100644 --- a/src/services/chokidar.ts +++ b/src/services/chokidar.ts @@ -1,6 +1,6 @@ import chokidar from 'chokidar'; import { BrowserWindow } from 'electron'; -import { logError } from '../utils/logging'; +import { logError } from '../services/logging'; import { getWatchMappings } from '../api/watch'; export function initWatcher(mainWindow: BrowserWindow) { diff --git a/src/services/diskLRU.ts b/src/services/diskLRU.ts index 3a02b5483..5aa133a41 100644 --- a/src/services/diskLRU.ts +++ b/src/services/diskLRU.ts @@ -2,7 +2,7 @@ import path from 'path'; import { readdir, stat, unlink } from 'promise-fs'; import getFolderSize from 'get-folder-size'; import { utimes, close, open } from 'promise-fs'; -import { logError } from '../utils/logging'; +import { logError } from '../services/logging'; export interface LeastRecentlyUsedResult { atime: Date; diff --git a/src/services/ffmpeg.ts b/src/services/ffmpeg.ts new file mode 100644 index 000000000..dfd24737a --- /dev/null +++ b/src/services/ffmpeg.ts @@ -0,0 +1,75 @@ +import pathToFfmpeg from 'ffmpeg-static'; +const shellescape = require('any-shell-escape'); +import util from 'util'; +import log from 'electron-log'; +import { readFile, rmSync, writeFile } from 'promise-fs'; +import { logErrorSentry } from './sentry'; +import { generateTempFilePath, getTempDirPath } from '../utils/temp'; +import { existsSync } from 'fs'; + +const execAsync = util.promisify(require('child_process').exec); + +export const INPUT_PATH_PLACEHOLDER = 'INPUT'; +export const FFMPEG_PLACEHOLDER = 'FFMPEG'; +export const OUTPUT_PATH_PLACEHOLDER = 'OUTPUT'; + +function getFFmpegStaticPath() { + return pathToFfmpeg.replace('app.asar', 'app.asar.unpacked'); +} + +export async function runFFmpegCmd( + cmd: string[], + inputFilePath: string, + outputFileName: string +) { + let tempOutputFilePath: string; + try { + tempOutputFilePath = await generateTempFilePath(outputFileName); + + cmd = cmd.map((cmdPart) => { + if (cmdPart === FFMPEG_PLACEHOLDER) { + return getFFmpegStaticPath(); + } else if (cmdPart === INPUT_PATH_PLACEHOLDER) { + return inputFilePath; + } else if (cmdPart === OUTPUT_PATH_PLACEHOLDER) { + return tempOutputFilePath; + } else { + return cmdPart; + } + }); + cmd = shellescape(cmd); + log.info('cmd', cmd); + await execAsync(cmd); + if (!existsSync(tempOutputFilePath)) { + throw new Error('ffmpeg output file not found'); + } + const outputFile = await readFile(tempOutputFilePath); + return new Uint8Array(outputFile); + } catch (e) { + logErrorSentry(e, 'ffmpeg run command error'); + throw e; + } finally { + try { + rmSync(tempOutputFilePath, { force: true }); + } catch (e) { + logErrorSentry(e, 'failed to remove tempOutputFile'); + } + } +} + +export async function writeTempFile(fileStream: Uint8Array, fileName: string) { + const tempFilePath = await generateTempFilePath(fileName); + await writeFile(tempFilePath, fileStream); + return tempFilePath; +} + +export async function deleteTempFile(tempFilePath: string) { + const tempDirPath = await getTempDirPath(); + if (!tempFilePath.startsWith(tempDirPath)) { + logErrorSentry( + Error('not a temp file'), + 'tried to delete a non temp file' + ); + } + rmSync(tempFilePath, { force: true }); +} diff --git a/src/services/fs.ts b/src/services/fs.ts index 2d4a28ca2..ba22e39ff 100644 --- a/src/services/fs.ts +++ b/src/services/fs.ts @@ -4,6 +4,8 @@ import * as fs from 'promise-fs'; import { ElectronFile } from '../types'; import StreamZip from 'node-stream-zip'; import { Readable } from 'stream'; +import { logError } from './logging'; +import { existsSync } from 'fs'; // https://stackoverflow.com/a/63111390 export const getDirFilePaths = async (dirPath: string) => { @@ -16,7 +18,7 @@ export const getDirFilePaths = async (dirPath: string) => { for (const filePath of filePaths) { const absolute = path.join(dirPath, filePath); - files = files.concat(await getDirFilePaths(absolute)); + files = [...files, ...(await getDirFilePaths(absolute))]; } return files; @@ -48,6 +50,9 @@ export const getFileStream = async (filePath: string) => { await fs.close(file); } }, + async cancel() { + await fs.close(file); + }, }); return readableStream; }; @@ -60,13 +65,22 @@ export async function getElectronFile(filePath: string): Promise { size: fileStats.size, lastModified: fileStats.mtime.valueOf(), stream: async () => { + if (!existsSync(filePath)) { + throw new Error('electronFile does not exist'); + } return await getFileStream(filePath); }, blob: async () => { + if (!existsSync(filePath)) { + throw new Error('electronFile does not exist'); + } const blob = await fs.readFile(filePath); return new Blob([new Uint8Array(blob)]); }, arrayBuffer: async () => { + if (!existsSync(filePath)) { + throw new Error('electronFile does not exist'); + } const blob = await fs.readFile(filePath); return new Uint8Array(blob); }, @@ -94,31 +108,50 @@ export const getZipFileStream = async ( const done = { current: false, }; + const inProgress = { + current: false, + }; let resolveObj: (value?: any) => void = null; let rejectObj: (reason?: any) => void = null; stream.on('readable', () => { - if (resolveObj) { - const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer; - - if (chunk) { - resolveObj(new Uint8Array(chunk)); - resolveObj = null; + try { + if (resolveObj) { + inProgress.current = true; + const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer; + if (chunk) { + resolveObj(new Uint8Array(chunk)); + resolveObj = null; + } + inProgress.current = false; } + } catch (e) { + rejectObj(e); } }); stream.on('end', () => { - done.current = true; + try { + done.current = true; + if (resolveObj && !inProgress.current) { + resolveObj(null); + resolveObj = null; + } + } catch (e) { + rejectObj(e); + } }); stream.on('error', (e) => { - done.current = true; - - if (rejectObj) { + try { + done.current = true; + if (rejectObj) { + rejectObj(e); + rejectObj = null; + } + } catch (e) { rejectObj(e); - rejectObj = null; } }); - const readStreamData = () => { + const readStreamData = async () => { return new Promise((resolve, reject) => { const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer; @@ -142,6 +175,7 @@ export const getZipFileStream = async ( controller.close(); } } catch (e) { + logError(e, 'readableStream pull failed'); controller.close(); } }, @@ -183,5 +217,8 @@ export function writeStream(filePath: string, fileStream: any) { } export async function readTextFile(filePath: string) { + if (!existsSync(filePath)) { + throw new Error('File does not exist'); + } return await fs.readFile(filePath, 'utf-8'); } diff --git a/src/services/heicConverter.ts b/src/services/heicConverter.ts new file mode 100644 index 000000000..e1af0a27f --- /dev/null +++ b/src/services/heicConverter.ts @@ -0,0 +1,72 @@ +import util from 'util'; +import { exec } from 'child_process'; + +import { existsSync, rmSync } from 'fs'; +import { readFile, writeFile } from 'promise-fs'; +import { generateTempFilePath } from '../utils/temp'; +import { logErrorSentry } from './sentry'; +import { isPlatform } from '../utils/main'; +import { isDev } from '../utils/common'; +import path from 'path'; + +const asyncExec = util.promisify(exec); + +function getImageMagickStaticPath() { + return isDev + ? 'build/image-magick' + : path.join(process.resourcesPath, 'image-magick'); +} + +export async function convertHEIC( + heicFileData: Uint8Array +): Promise { + let tempInputFilePath: string; + let tempOutputFilePath: string; + try { + tempInputFilePath = await generateTempFilePath('.heic'); + tempOutputFilePath = await generateTempFilePath('.jpeg'); + + await writeFile(tempInputFilePath, heicFileData); + + await runConvertCommand(tempInputFilePath, tempOutputFilePath); + + if (!existsSync(tempOutputFilePath)) { + throw new Error('heic convert output file not found'); + } + const convertedFileData = new Uint8Array( + await readFile(tempOutputFilePath) + ); + return convertedFileData; + } catch (e) { + logErrorSentry(e, 'failed to convert heic'); + throw e; + } finally { + try { + rmSync(tempInputFilePath, { force: true }); + } catch (e) { + logErrorSentry(e, 'failed to remove tempInputFile'); + } + try { + rmSync(tempOutputFilePath, { force: true }); + } catch (e) { + logErrorSentry(e, 'failed to remove tempOutputFile'); + } + } +} + +async function runConvertCommand( + tempInputFilePath: string, + tempOutputFilePath: string +) { + if (isPlatform('mac')) { + await asyncExec( + `sips -s format jpeg ${tempInputFilePath} --out ${tempOutputFilePath}` + ); + } else if (isPlatform('linux')) { + await asyncExec( + `${getImageMagickStaticPath()} ${tempInputFilePath} -quality 100% ${tempOutputFilePath}` + ); + } else { + Error(`${process.platform} native heic convert not supported yet`); + } +} diff --git a/src/services/logging.ts b/src/services/logging.ts new file mode 100644 index 000000000..47ae79c2f --- /dev/null +++ b/src/services/logging.ts @@ -0,0 +1,18 @@ +import log from 'electron-log'; +import { ipcRenderer } from 'electron'; + +export function logToDisk(logLine: string) { + log.info(logLine); +} + +export function openLogDirectory() { + ipcRenderer.invoke('open-log-dir'); +} + +export function logError(error: Error, message: string, info?: string): void { + ipcRenderer.invoke('log-error', error, message, info); +} + +export function getSentryUserID(): Promise { + return ipcRenderer.invoke('get-sentry-id'); +} diff --git a/src/services/sentry.ts b/src/services/sentry.ts index 21be3b06c..30c2ac0d2 100644 --- a/src/services/sentry.ts +++ b/src/services/sentry.ts @@ -1,19 +1,22 @@ import * as Sentry from '@sentry/electron/dist/main'; +import { makeID } from '../utils/logging'; import { keysStore } from '../stores/keys.store'; - +import { SENTRY_DSN, RELEASE_VERSION } from '../config'; import { isDev } from '../utils/common'; +import { logToDisk } from './logging'; -const SENTRY_DSN = 'https://e9268b784d1042a7a116f53c58ad2165@sentry.ente.io/5'; +const ENV_DEVELOPMENT = 'development'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const version = require('../../package.json').version; +const isDEVSentryENV = () => + process.env.NEXT_PUBLIC_SENTRY_ENV === ENV_DEVELOPMENT; export function initSentry(): void { Sentry.init({ dsn: SENTRY_DSN, - release: version, + release: RELEASE_VERSION, environment: isDev ? 'development' : 'production', }); + Sentry.setUser({ id: getSentryUserID() }); } export function logErrorSentry( @@ -22,12 +25,17 @@ export function logErrorSentry( info?: Record ) { const err = errorWithContext(error, msg); - if (!process.env.NEXT_PUBLIC_SENTRY_ENV) { + logToDisk( + `error: ${error?.name} ${error?.message} ${ + error?.stack + } msg: ${msg} info: ${JSON.stringify(info)}` + ); + if (isDEVSentryENV()) { console.log(error, { msg, info }); } Sentry.captureException(err, { level: Sentry.Severity.Info, - user: { id: getUserAnonymizedID() }, + user: { id: getSentryUserID() }, contexts: { ...(info && { info: info, @@ -46,7 +54,7 @@ function errorWithContext(originalError: Error, context: string) { return errorWithContext; } -function getUserAnonymizedID() { +export function getSentryUserID() { let anonymizeUserID = keysStore.get('AnonymizeUserID')?.id; if (!anonymizeUserID) { anonymizeUserID = makeID(6); @@ -54,16 +62,3 @@ function getUserAnonymizedID() { } return anonymizeUserID; } - -function makeID(length: number) { - let result = ''; - const characters = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const charactersLength = characters.length; - for (let i = 0; i < length; i++) { - result += characters.charAt( - Math.floor(Math.random() * charactersLength) - ); - } - return result; -} diff --git a/src/services/upload.ts b/src/services/upload.ts index 8379df09d..7d5144121 100644 --- a/src/services/upload.ts +++ b/src/services/upload.ts @@ -15,11 +15,15 @@ export const getSavedFilePaths = (type: FILE_PATH_TYPE) => { }; export async function getZipEntryAsElectronFile( + zipName: string, zip: StreamZip.StreamZipAsync, entry: StreamZip.ZipEntry ): Promise { return { - path: entry.name, + path: path + .join(zipName, entry.name) + .split(path.sep) + .join(path.posix.sep), name: path.basename(entry.name), size: entry.size, lastModified: entry.time, @@ -58,6 +62,7 @@ export const getElectronFilesFromGoogleZip = async (filePath: string) => { const zip = new StreamZip.async({ file: filePath, }); + const zipName = path.basename(filePath, '.zip'); const entries = await zip.entries(); const files: ElectronFile[] = []; @@ -65,7 +70,7 @@ export const getElectronFilesFromGoogleZip = async (filePath: string) => { for (const entry of Object.values(entries)) { const basename = path.basename(entry.name); if (entry.isFile && basename.length > 0 && basename[0] !== '.') { - files.push(await getZipEntryAsElectronFile(zip, entry)); + files.push(await getZipEntryAsElectronFile(zipName, zip, entry)); } } diff --git a/src/services/userPreference.ts b/src/services/userPreference.ts index 494501acd..5746c2ae6 100644 --- a/src/services/userPreference.ts +++ b/src/services/userPreference.ts @@ -1,10 +1,16 @@ import { userPreferencesStore } from '../stores/userPreferences.store'; export function getHideDockIconPreference() { - const shouldHideDockIcon = userPreferencesStore.get('hideDockIcon'); - return shouldHideDockIcon; + return userPreferencesStore.get('hideDockIcon'); } export function setHideDockIconPreference(shouldHideDockIcon: boolean) { userPreferencesStore.set('hideDockIcon', shouldHideDockIcon); } + +export function getSkipAppVersion() { + return userPreferencesStore.get('skipAppVersion'); +} +export function setSkipAppVersion(version: string) { + userPreferencesStore.set('skipAppVersion', version); +} diff --git a/src/stores/userPreferences.store.ts b/src/stores/userPreferences.store.ts index b9f6a7781..09e7ce2fe 100644 --- a/src/stores/userPreferences.store.ts +++ b/src/stores/userPreferences.store.ts @@ -5,6 +5,9 @@ const userPreferencesSchema: Schema = { hideDockIcon: { type: 'boolean', }, + skipAppVersion: { + type: 'string', + }, }; export const userPreferencesStore = new Store({ diff --git a/src/types/index.ts b/src/types/index.ts index 0db566ce3..a7d26186e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -56,4 +56,14 @@ export interface SafeStorageStoreType { export interface UserPreferencesType { hideDockIcon: boolean; + skipAppVersion: string; +} + +export interface AppUpdateInfo { + autoUpdatable: boolean; + version: string; +} + +export interface GetFeatureFlagResponse { + desktopCutoffVersion?: string; } diff --git a/src/utils/createWindow.ts b/src/utils/createWindow.ts index 78ffa9786..db356d467 100644 --- a/src/utils/createWindow.ts +++ b/src/utils/createWindow.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { isDev } from './common'; import { isAppQuitting } from '../main'; import { PROD_HOST_URL } from '../config'; -import { isPlatformMac } from './main'; +import { isPlatform } from './main'; import { getHideDockIconPreference } from '../services/userPreference'; import autoLauncher from '../services/autoLauncher'; @@ -16,13 +16,13 @@ export async function createWindow(): Promise { const mainWindow = new BrowserWindow({ height: 600, width: 800, - backgroundColor: '#111111', webPreferences: { + sandbox: false, preload: path.join(__dirname, '../preload.js'), contextIsolation: false, }, icon: appIcon, - show: false, // don't show the main window on load + show: false, // don't show the main window on load, }); mainWindow.maximize(); const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); @@ -55,11 +55,25 @@ export async function createWindow(): Promise { ); }); mainWindow.once('ready-to-show', async () => { - splash.destroy(); - if (!wasAutoLaunched) { - mainWindow.show(); + try { + splash.destroy(); + if (!wasAutoLaunched) { + mainWindow.show(); + } + } catch (e) { + // ignore } }); + setTimeout(() => { + try { + splash.destroy(); + if (!wasAutoLaunched) { + mainWindow.show(); + } + } catch (e) { + // ignore + } + }, 2000); mainWindow.on('close', function (event) { if (!isAppQuitting()) { event.preventDefault(); @@ -69,12 +83,12 @@ export async function createWindow(): Promise { }); mainWindow.on('hide', () => { const shouldHideDockIcon = getHideDockIconPreference(); - if (isPlatformMac() && shouldHideDockIcon) { + if (isPlatform('mac') && shouldHideDockIcon) { app.dock.hide(); } }); mainWindow.on('show', () => { - if (isPlatformMac()) { + if (isPlatform('mac')) { app.dock.show(); } }); diff --git a/src/utils/ipcComms.ts b/src/utils/ipcComms.ts index 6235e7e5c..debec6273 100644 --- a/src/utils/ipcComms.ts +++ b/src/utils/ipcComms.ts @@ -6,13 +6,25 @@ import { Notification, safeStorage, app, + shell, } from 'electron'; import { createWindow } from './createWindow'; import { buildContextMenu } from './menu'; -import { logErrorSentry } from '../services/sentry'; +import { getSentryUserID, logErrorSentry } from '../services/sentry'; import chokidar from 'chokidar'; import path from 'path'; import { getDirFilePaths } from '../services/fs'; +import { convertHEIC } from '../services/heicConverter'; +import { + getAppVersion, + skipAppVersion, + updateAndRestart, +} from '../services/appUpdater'; +import { + deleteTempFile, + runFFmpegCmd, + writeTempFile, +} from '../services/ffmpeg'; export default function setupIpcComs( tray: Tray, @@ -67,7 +79,7 @@ export default function setupIpcComs( let files: string[] = []; for (const dirPath of dir.filePaths) { - files = files.concat(await getDirFilePaths(dirPath)); + files = [...files, ...(await getDirFilePaths(dirPath))]; } return files; @@ -96,4 +108,42 @@ export default function setupIpcComs( ipcMain.handle('get-path', (_, message) => { return app.getPath(message); }); + + ipcMain.handle('convert-heic', (_, fileData) => { + return convertHEIC(fileData); + }); + + ipcMain.handle('open-log-dir', () => { + shell.openPath(app.getPath('logs')); + }); + + ipcMain.on('update-and-restart', () => { + updateAndRestart(); + }); + ipcMain.on('skip-app-version', (_, version) => { + skipAppVersion(version); + }); + ipcMain.handle('get-sentry-id', () => { + return getSentryUserID(); + }); + + ipcMain.handle('get-app-version', () => { + return getAppVersion(); + }); + + ipcMain.handle( + 'run-ffmpeg-cmd', + (_, cmd, inputFilePath, outputFileName) => { + return runFFmpegCmd(cmd, inputFilePath, outputFileName); + } + ); + ipcMain.handle( + 'write-temp-file', + (_, fileStream: Uint8Array, fileName: string) => { + return writeTempFile(fileStream, fileName); + } + ); + ipcMain.handle('remove-temp-file', (_, tempFilePath: string) => { + return deleteTempFile(tempFilePath); + }); } diff --git a/src/utils/logging.ts b/src/utils/logging.ts index e50d533ff..664dea1d5 100644 --- a/src/utils/logging.ts +++ b/src/utils/logging.ts @@ -1,5 +1,25 @@ -import { ipcRenderer } from 'electron'; +import log from 'electron-log'; +import { LOG_FILENAME, MAX_LOG_SIZE } from '../config'; -export function logError(error: Error, message: string, info?: string): void { - ipcRenderer.invoke('log-error', error, message, info); +export function setupLogging(isDev?: boolean) { + log.transports.file.fileName = LOG_FILENAME; + log.transports.file.maxSize = MAX_LOG_SIZE; + if (!isDev) { + log.transports.console.level = false; + } + log.transports.file.format = + '[{y}-{m}-{d}T{h}:{i}:{s}{z}] [{level}]{scope} {text}'; +} + +export function makeID(length: number) { + let result = ''; + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt( + Math.floor(Math.random() * charactersLength) + ); + } + return result; } diff --git a/src/utils/main.ts b/src/utils/main.ts index 30b58e721..a863f7998 100644 --- a/src/utils/main.ts +++ b/src/utils/main.ts @@ -4,22 +4,31 @@ import electronReload from 'electron-reload'; import serveNextAt from 'next-electron-server'; import path from 'path'; import { existsSync } from 'promise-fs'; -import appUpdater from '../services/appUpdater'; import { isDev } from './common'; import { buildContextMenu, buildMenuBar } from './menu'; import autoLauncher from '../services/autoLauncher'; import { getHideDockIconPreference } from '../services/userPreference'; +import { + checkForUpdateAndNotify, + setupAutoUpdater, +} from '../services/appUpdater'; +import ElectronLog from 'electron-log'; +import os from 'os'; -export function handleUpdates(mainWindow: BrowserWindow, tray: Tray) { +export function handleUpdates(mainWindow: BrowserWindow) { if (!isDev) { - appUpdater.checkForUpdate(tray, mainWindow); + setupAutoUpdater(); + checkForUpdateAndNotify(mainWindow); } } - export function setupTrayItem(mainWindow: BrowserWindow) { - const trayImgPath = isDev - ? 'build/taskbar-icon.png' - : path.join(process.resourcesPath, 'taskbar-icon.png'); + const iconName = isPlatform('mac') + ? 'taskbar-icon-Template.png' + : 'taskbar-icon.png'; + const trayImgPath = path.join( + isDev ? 'build' : process.resourcesPath, + iconName + ); const trayIcon = nativeImage.createFromPath(trayImgPath); const tray = new Tray(trayIcon); tray.setToolTip('ente'); @@ -79,19 +88,23 @@ export function setupNextElectronServe() { }); } -export function isPlatformMac() { - return process.platform === 'darwin'; -} - -export function isPlatformWindows() { - return process.platform === 'win32'; +export function isPlatform(platform: 'mac' | 'windows' | 'linux') { + if (process.platform === 'darwin') { + return platform === 'mac'; + } else if (process.platform === 'win32') { + return platform === 'windows'; + } else if (process.platform === 'linux') { + return platform === 'linux'; + } else { + return false; + } } export async function handleDockIconHideOnAutoLaunch() { const shouldHideDockIcon = getHideDockIconPreference(); const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); - if (isPlatformMac() && shouldHideDockIcon && wasAutoLaunched) { + if (isPlatform('mac') && shouldHideDockIcon && wasAutoLaunched) { app.dock.hide(); } } @@ -99,3 +112,10 @@ export async function handleDockIconHideOnAutoLaunch() { export function enableSharedArrayBufferSupport() { app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer'); } + +export function logSystemInfo() { + const systemVersion = process.getSystemVersion(); + const osName = process.platform; + const osRelease = os.release(); + ElectronLog.info({ osName, osRelease, systemVersion }); +} diff --git a/src/utils/menu.ts b/src/utils/menu.ts index a802b62ae..2c93fc313 100644 --- a/src/utils/menu.ts +++ b/src/utils/menu.ts @@ -9,11 +9,10 @@ import { getHideDockIconPreference, setHideDockIconPreference, } from '../services/userPreference'; -import { isUpdateAvailable, setIsAppQuitting } from '../main'; +import { setIsAppQuitting } from '../main'; import autoLauncher from '../services/autoLauncher'; -import { isPlatformMac } from './main'; -import { showUpdateDialog } from '../services/appUpdater'; -import { isDev } from './common'; +import { isPlatform } from './main'; +import ElectronLog from 'electron-log'; export function buildContextMenu( mainWindow: BrowserWindow, @@ -26,15 +25,6 @@ export function buildContextMenu( paused, } = args; const contextMenu = Menu.buildFromTemplate([ - ...(isUpdateAvailable() - ? [ - { - label: 'Update available', - click: () => showUpdateDialog(), - }, - ] - : []), - { type: 'separator' }, ...(exportProgress ? [ { @@ -87,6 +77,7 @@ export function buildContextMenu( { label: 'Quit ente', click: function () { + ElectronLog.log('user quit the app'); setIsAppQuitting(true); app.quit(); }, @@ -97,7 +88,7 @@ export function buildContextMenu( export async function buildMenuBar(): Promise { let isAutoLaunchEnabled = await autoLauncher.isEnabled(); - const isMac = isPlatformMac(); + const isMac = isPlatform('mac'); let shouldHideDockIcon = getHideDockIconPreference(); const template: MenuItemConstructorOptions[] = [ { @@ -216,6 +207,7 @@ export async function buildMenuBar(): Promise { { label: 'Window', submenu: [ + { role: 'close', label: 'Close' }, { role: 'minimize', label: 'Minimize' }, ...((isMac ? [ @@ -224,9 +216,31 @@ export async function buildMenuBar(): Promise { { type: 'separator' }, { role: 'window', label: 'ente' }, ] - : [ - { role: 'close', label: 'Close ente' }, - ]) as MenuItemConstructorOptions[]), + : []) as MenuItemConstructorOptions[]), + ], + }, + { + label: 'Help', + submenu: [ + { + label: 'FAQ', + click: () => shell.openExternal('https://ente.io/faq/'), + }, + { type: 'separator' }, + { + label: 'Support', + click: () => shell.openExternal('mailto:support@ente.io'), + }, + { + label: 'Product updates', + click: () => shell.openExternal('https://ente.io/blog/'), + }, + { + label: 'View logs', + click: () => { + shell.openPath(app.getPath('logs')); + }, + }, ], }, { diff --git a/src/utils/processStats.ts b/src/utils/processStats.ts new file mode 100644 index 000000000..2b8d8f054 --- /dev/null +++ b/src/utils/processStats.ts @@ -0,0 +1,39 @@ +import ElectronLog from 'electron-log'; +import { webFrame } from 'electron/renderer'; + +const FIVE_MINUTES_IN_MICROSECONDS = 30 * 1000; + +async function logMainProcessStats() { + const systemMemoryInfo = process.getSystemMemoryInfo(); + const cpuUsage = process.getCPUUsage(); + const processMemoryInfo = await process.getProcessMemoryInfo(); + const heapStatistics = process.getHeapStatistics(); + + ElectronLog.log('main process stats', { + systemMemoryInfo, + cpuUsage, + processMemoryInfo, + heapStatistics, + }); +} + +async function logRendererProcessStats() { + const blinkMemoryInfo = process.getBlinkMemoryInfo(); + const heapStatistics = process.getHeapStatistics(); + const processMemoryInfo = process.getProcessMemoryInfo(); + const webFrameResourceUsage = webFrame.getResourceUsage(); + ElectronLog.log('renderer process stats', { + blinkMemoryInfo, + heapStatistics, + processMemoryInfo, + webFrameResourceUsage, + }); +} + +export function setupMainProcessStatsLogger() { + setInterval(logMainProcessStats, FIVE_MINUTES_IN_MICROSECONDS); +} + +export function setupRendererProcessStatsLogger() { + setInterval(logRendererProcessStats, FIVE_MINUTES_IN_MICROSECONDS); +} diff --git a/src/utils/temp.ts b/src/utils/temp.ts new file mode 100644 index 000000000..838d3692f --- /dev/null +++ b/src/utils/temp.ts @@ -0,0 +1,38 @@ +import { app } from 'electron'; +import path from 'path'; +import { existsSync, mkdir } from 'promise-fs'; + +const ENTE_TEMP_DIRECTORY = 'ente'; + +const CHARACTERS = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + +export async function getTempDirPath() { + const tempDirPath = path.join(app.getPath('temp'), ENTE_TEMP_DIRECTORY); + if (!existsSync(tempDirPath)) { + await mkdir(tempDirPath); + } + return tempDirPath; +} + +function generateTempName(length: number) { + let result = ''; + + const charactersLength = CHARACTERS.length; + for (let i = 0; i < length; i++) { + result += CHARACTERS.charAt( + Math.floor(Math.random() * charactersLength) + ); + } + return result; +} + +export async function generateTempFilePath(formatSuffix: string) { + const tempDirPath = await getTempDirPath(); + const namePrefix = generateTempName(10); + const tempFilePath = path.join( + tempDirPath, + namePrefix + '-' + formatSuffix + ); + return tempFilePath; +} diff --git a/yarn.lock b/yarn.lock index 842179cdc..8dd73a05c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,6 +35,16 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@derhuerst/http-basic@^8.2.0": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@derhuerst/http-basic/-/http-basic-8.2.4.tgz#d021ebb8f65d54bea681ae6f4a8733ce89e7f59b" + integrity sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw== + dependencies: + caseless "^0.12.0" + concat-stream "^2.0.0" + http-response-object "^3.0.1" + parse-cache-control "^1.0.1" + "@develar/schema-utils@~2.6.5": version "2.6.5" resolved "https://registry.yarnpkg.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz#3ece22c5838402419a6e0425f85742b961d9b6c6" @@ -43,7 +53,7 @@ ajv "^6.12.0" ajv-keywords "^3.4.1" -"@electron/get@^1.13.0": +"@electron/get@^1.14.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.14.1.tgz#16ba75f02dffb74c23965e72d617adc721d27f40" integrity sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw== @@ -277,6 +287,11 @@ dependencies: "@types/ms" "*" +"@types/ffmpeg-static@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/ffmpeg-static/-/ffmpeg-static-3.0.1.tgz#1003f003624bcd2f569b56185a62dcbacd935c39" + integrity sha512-hEJdQMv/g1olk9qTiWqh23BfbKsDKE6Tc7DilNJWF1MgZsU9fYOPKrgQ448vfT7aP2Yt5re9vgJDVv9TXEoTyQ== + "@types/fs-extra@^9.0.11": version "9.0.13" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" @@ -312,15 +327,28 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/node-fetch@^2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" + integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + "@types/node@*": version "18.0.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199" integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ== -"@types/node@^14.14.37", "@types/node@^14.6.2": - version "14.18.21" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.21.tgz#0155ee46f6be28b2ff0342ca1a9b9fd4468bef41" - integrity sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q== +"@types/node@^10.0.3": + version "10.17.60" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" + integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== + +"@types/node@^16.11.26", "@types/node@^16.18.3": + version "16.18.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.3.tgz#d7f7ba828ad9e540270f01ce00d391c54e6e0abc" + integrity sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -342,6 +370,11 @@ dependencies: "@types/node" "*" +"@types/semver-compare@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/semver-compare/-/semver-compare-1.0.1.tgz#17d1dc62c516c133ab01efb7803a537ee6eaf3d5" + integrity sha512-wx2LQVvKlEkhXp/HoKIZ/aSL+TvfJdKco8i0xJS3aR877mg4qBHzNT6+B5a61vewZHo79EdZavskGnRXEC2H6A== + "@types/semver@^7.3.6": version "7.3.10" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.10.tgz#5f19ee40cbeff87d916eedc8c2bfe2305d957f73" @@ -364,6 +397,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yauzl@^2.9.1": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" + integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@^5.28.0": version "5.30.6" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz#9c6017b6c1d04894141b4a87816388967f64c359" @@ -554,6 +594,11 @@ ansi-styles@^6.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.1.0.tgz#87313c102b8118abd57371afab34618bf7350ed3" integrity sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ== +any-shell-escape@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/any-shell-escape/-/any-shell-escape-0.1.1.tgz#d55ab972244c71a9a5e1ab0879f30bf110806959" + integrity sha512-36j4l5HVkboyRhIWgtMh1I9i8LTdFqVwDEHy1cp+QioJyKgAUG40X0W8s7jakWRta/Sjvm8mUG1fU6Tj8mWagQ== + anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" @@ -887,7 +932,7 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caseless@~0.12.0: +caseless@^0.12.0, caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== @@ -1066,14 +1111,14 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -concat-stream@^1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== dependencies: buffer-from "^1.0.0" inherits "^2.0.3" - readable-stream "^2.2.2" + readable-stream "^3.0.2" typedarray "^0.0.6" concurrently@^7.0.0: @@ -1194,7 +1239,7 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, d dependencies: ms "2.1.2" -debug@^2.1.3, debug@^2.2.0, debug@^2.6.8, debug@^2.6.9: +debug@^2.1.3, debug@^2.2.0, debug@^2.6.8: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -1469,14 +1514,14 @@ electron-updater@^4.3.8: lodash.isequal "^4.5.0" semver "^7.3.5" -electron@^15.3.0: - version "15.5.7" - resolved "https://registry.yarnpkg.com/electron/-/electron-15.5.7.tgz#aadb0081c504f2c2d8f81ea5fd23e38881afe86a" - integrity sha512-n4mVlxoMc4eYx07wWFWGficL+iOMz5xZEf5dBtE/wwLm0fQpYVyW4AlknMFG9F8Css0MM0JSwNMOyRg5e1vDtg== +electron@^21.2.2: + version "21.3.0" + resolved "https://registry.yarnpkg.com/electron/-/electron-21.3.0.tgz#e9905e240add950443dc115b4be13d36162f0a05" + integrity sha512-MGRpshN8fBcx4IRuBABIsGDv0tB/MclIFsyFHFFXsBCUc+vIXaE/E6vuWaniGIFSz5WyeuapfTH5IeRb+7yIfw== dependencies: - "@electron/get" "^1.13.0" - "@types/node" "^14.6.2" - extract-zip "^1.0.3" + "@electron/get" "^1.14.1" + "@types/node" "^16.11.26" + extract-zip "^2.0.1" emoji-regex@^8.0.0: version "8.0.0" @@ -1705,15 +1750,16 @@ extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -extract-zip@^1.0.3: - version "1.7.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== +extract-zip@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" + debug "^4.1.1" + get-stream "^5.1.0" yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" extsprintf@1.3.0: version "1.3.0" @@ -1765,6 +1811,16 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +ffmpeg-static@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ffmpeg-static/-/ffmpeg-static-5.1.0.tgz#133500f4566570c5a0e96795152b0526d8c936ad" + integrity sha512-eEWOiGdbf7HKPeJI5PoJ0oCwkL0hckL2JdS4JOuB/gUETppwkEpq8nF0+e6VEQnDCo/iuoipbTUsn9QJmtpNkg== + dependencies: + "@derhuerst/http-basic" "^8.2.0" + env-paths "^2.2.0" + https-proxy-agent "^5.0.0" + progress "^2.0.3" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -1819,6 +1875,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -2147,6 +2212,13 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" +http-response-object@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/http-response-object/-/http-response-object-3.0.2.tgz#7f435bb210454e4360d074ef1f989d5ea8aa9810" + integrity sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA== + dependencies: + "@types/node" "^10.0.3" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -2745,7 +2817,7 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" -mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@^0.5.5: +mkdirp@^0.5.1, mkdirp@^0.5.5: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -2973,6 +3045,11 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-cache-control@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-cache-control/-/parse-cache-control-1.0.1.tgz#8eeab3e54fa56920fe16ba38f77fa21aacc2d74e" + integrity sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg== + parse-json@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -3193,7 +3270,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.0.6, readable-stream@^2.2.2: +readable-stream@^2.0.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -3206,6 +3283,15 @@ readable-stream@^2.0.6, readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.0.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@~1.1.9: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -3350,7 +3436,7 @@ rxjs@^7.0.0, rxjs@^7.5.5: dependencies: tslib "^2.1.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.2: +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -3594,6 +3680,13 @@ string-width@^5.0.0: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -3941,7 +4034,7 @@ utf8-byte-length@^1.0.1: resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" integrity sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA== -util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==