Merge branch 'main' into bundle-ml-demo

This commit is contained in:
Abhinav 2022-09-17 18:27:01 +05:30
parent df1a63e384
commit 27876e5b6d
49 changed files with 1320 additions and 639 deletions

4
.gitmodules vendored
View file

@ -2,3 +2,7 @@
path = ui path = ui
url = https://github.com/ente-io/bada-frame url = https://github.com/ente-io/bada-frame
branch = demo branch = demo
[submodule "thirdparty/next-electron-server"]
path = thirdparty/next-electron-server
url = https://github.com/ente-io/next-electron-server.git
branch = desktop

View file

@ -25,27 +25,35 @@ If you are running into issues with this app, please drop a mail to [support@ent
- [AUR](https://aur.archlinux.org/packages/ente-desktop-appimage): - [AUR](https://aur.archlinux.org/packages/ente-desktop-appimage):
`yay -S ente-desktop-appimage` `yay -S ente-desktop-appimage`
## Develop ## Building from source
To clone and run this repository you'll need [Git](https://git-scm.com) and [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer. From your command line: You'll need to have node (and yarn) installed on your machine. e.g. on macOS you
can do `brew install node`. After that, you can run the following commands to
fetch and build from source.
```bash ```bash
# Clone this repository # Clone this repository
git clone https://github.com/ente-io/bhari-frame git clone https://github.com/ente-io/bhari-frame
# Go into the repository # Go into the repository
cd bhari-frame cd bhari-frame
# Install dependencies
npm install
# Run the app
npm start
```
Note: If you're using Linux Bash for Windows, [see this guide](https://www.howtogeek.com/261575/how-to-run-graphical-linux-desktop-applications-from-windows-10s-bash-shell/) or use `node` from the command prompt. # Clone submodules (recursively)
git submodule update --init --recursive
# Install packages
yarn
# Run the app
yarn start
```
### Re-compile automatically ### Re-compile automatically
To recompile automatically and to allow using [electron-reload](https://github.com/yan-foto/electron-reload), run this in a separate terminal: To recompile automatically and to allow using
[electron-reload](https://github.com/yan-foto/electron-reload), run this in a
separate terminal:
```bash ```bash
npm run watch yarn watch
``` ```

View file

@ -1,87 +0,0 @@
import { app, BrowserWindow, Menu, Tray, nativeImage } from 'electron';
import * as path from 'path';
import AppUpdater from './utils/appUpdater';
import { createWindow } from './utils/createWindow';
import setupIpcComs from './utils/ipcComms';
import { buildContextMenu, buildMenuBar } from './utils/menuUtil';
import initSentry from './utils/sentry';
import electronReload from 'electron-reload';
import { PROD_HOST_URL, RENDERER_OUTPUT_DIR } from './config';
import { isDev } from './utils/common';
import serveNextAt from 'next-electron-server';
if (isDev) {
electronReload(__dirname, {});
}
let tray: Tray;
let mainWindow: BrowserWindow;
let appIsQuitting = false;
let updateIsAvailable = false;
export const isAppQuitting = (): boolean => {
return appIsQuitting;
};
export const setIsAppQuitting = (value: boolean): void => {
appIsQuitting = value;
};
export const isUpdateAvailable = (): boolean => {
return updateIsAvailable;
};
export const setIsUpdateAvailable = (value: boolean): void => {
updateIsAvailable = value;
};
serveNextAt(PROD_HOST_URL, {
outputDir: RENDERER_OUTPUT_DIR,
});
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer');
app.on('second-instance', () => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
mainWindow.show();
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
mainWindow.focus();
}
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', () => {
initSentry();
setIsUpdateAvailable(false);
mainWindow = createWindow();
Menu.setApplicationMenu(buildMenuBar());
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
const trayImgPath = isDev
? 'build/taskbar-icon.png'
: path.join(process.resourcesPath, 'taskbar-icon.png');
const trayIcon = nativeImage.createFromPath(trayImgPath);
tray = new Tray(trayIcon);
tray.setToolTip('ente');
tray.setContextMenu(buildContextMenu(mainWindow));
setupIpcComs(tray, mainWindow);
if (!isDev) {
AppUpdater.checkForUpdate(tray, mainWindow);
}
});
}

View file

@ -1,199 +0,0 @@
import { Readable } from 'stream';
import * as fs from 'promise-fs';
import { webFrame, ipcRenderer } from 'electron';
import {
getElectronFile,
getPendingUploads,
setToUploadFiles,
getElectronFilesFromGoogleZip,
setToUploadCollection,
} from './utils/upload';
import { logError } from './utils/logging';
import { ElectronFile } from './types';
import { getEncryptionKey, setEncryptionKey } from './utils/safeStorage';
import { clearElectronStore } from './utils/electronStore';
import { openDiskCache, deleteDiskCache } from './utils/cache';
// Patch the global WebSocket constructor to use the correct DevServer url
const fixHotReloadNext12 = () => {
webFrame.executeJavaScript(`Object.defineProperty(globalThis, 'WebSocket', {
value: new Proxy(WebSocket, {
construct: (Target, [url, protocols]) => {
if (url.endsWith('/_next/webpack-hmr')) {
// Fix the Next.js hmr client url
return new Target("ws://localhost:3000/_next/webpack-hmr", protocols)
} else {
return new Target(url, protocols)
}
}
})
});`);
};
fixHotReloadNext12();
const responseToReadable = (fileStream: any) => {
const reader = fileStream.getReader();
const rs = new Readable();
rs._read = async () => {
const result = await reader.read();
if (!result.done) {
rs.push(Buffer.from(result.value));
} else {
rs.push(null);
return;
}
};
return rs;
};
const exists = (path: string) => {
return fs.existsSync(path);
};
const checkExistsAndCreateCollectionDir = async (dirPath: string) => {
if (!fs.existsSync(dirPath)) {
await fs.mkdir(dirPath);
}
};
const checkExistsAndRename = async (oldDirPath: string, newDirPath: string) => {
if (fs.existsSync(oldDirPath)) {
await fs.rename(oldDirPath, newDirPath);
}
};
const saveStreamToDisk = (path: string, fileStream: ReadableStream<any>) => {
const writeable = fs.createWriteStream(path);
const readable = responseToReadable(fileStream);
readable.pipe(writeable);
};
const saveFileToDisk = async (path: string, file: any) => {
await fs.writeFile(path, file);
};
const selectRootDirectory = async () => {
try {
return await ipcRenderer.invoke('select-dir');
} catch (e) {
logError(e, 'error while selecting root directory');
}
};
const sendNotification = (content: string) => {
ipcRenderer.send('send-notification', content);
};
const showOnTray = (content: string) => {
ipcRenderer.send('update-tray', content);
};
const registerResumeExportListener = (resumeExport: () => void) => {
ipcRenderer.removeAllListeners('resume-export');
ipcRenderer.on('resume-export', () => resumeExport());
};
const registerStopExportListener = (abortExport: () => void) => {
ipcRenderer.removeAllListeners('stop-export');
ipcRenderer.on('stop-export', () => abortExport());
};
const registerPauseExportListener = (pauseExport: () => void) => {
ipcRenderer.removeAllListeners('pause-export');
ipcRenderer.on('pause-export', () => pauseExport());
};
const registerRetryFailedExportListener = (retryFailedExport: () => void) => {
ipcRenderer.removeAllListeners('retry-export');
ipcRenderer.on('retry-export', () => retryFailedExport());
};
const reloadWindow = () => {
ipcRenderer.send('reload-window');
};
const getExportRecord = async (filePath: string) => {
try {
const filepath = `${filePath}`;
const recordFile = await fs.readFile(filepath, 'utf-8');
return recordFile;
} catch (e) {
// ignore exportFile missing
logError(e, 'error while selecting files');
}
};
const setExportRecord = async (filePath: string, data: string) => {
const filepath = `${filePath}`;
await fs.writeFile(filepath, data);
};
const showUploadFilesDialog = async () => {
try {
const filePaths: string[] = await ipcRenderer.invoke(
'show-upload-files-dialog'
);
const files = await Promise.all(filePaths.map(getElectronFile));
return files;
} catch (e) {
logError(e, 'error while selecting files');
}
};
const showUploadDirsDialog = async () => {
try {
const filePaths: string[] = await ipcRenderer.invoke(
'show-upload-dirs-dialog'
);
const files = await Promise.all(filePaths.map(getElectronFile));
return files;
} catch (e) {
logError(e, 'error while selecting folders');
}
};
const showUploadZipDialog = async () => {
try {
const filePaths: string[] = await ipcRenderer.invoke(
'show-upload-zip-dialog'
);
const files: ElectronFile[] = [];
for (const filePath of filePaths) {
files.push(...(await getElectronFilesFromGoogleZip(filePath)));
}
return { zipPaths: filePaths, files };
} catch (e) {
logError(e, 'error while selecting zips');
}
};
const windowObject: any = window;
windowObject['ElectronAPIs'] = {
exists,
checkExistsAndCreateCollectionDir,
checkExistsAndRename,
saveStreamToDisk,
saveFileToDisk,
selectRootDirectory,
sendNotification,
showOnTray,
reloadWindow,
registerResumeExportListener,
registerStopExportListener,
registerPauseExportListener,
registerRetryFailedExportListener,
getExportRecord,
setExportRecord,
getElectronFile,
showUploadFilesDialog,
showUploadDirsDialog,
getPendingUploads,
setToUploadFiles,
showUploadZipDialog,
getElectronFilesFromGoogleZip,
setToUploadCollection,
getEncryptionKey,
setEncryptionKey,
clearElectronStore,
openDiskCache,
deleteDiskCache,
};

View file

@ -1,52 +0,0 @@
import Store, { Schema } from 'electron-store';
import { KeysStoreType, SafeStorageStoreType, UploadStoreType } from '../types';
export const uploadStoreSchema: Schema<UploadStoreType> = {
filePaths: {
type: 'array',
items: {
type: 'string',
},
},
zipPaths: {
type: 'array',
items: {
type: 'string',
},
},
collectionName: {
type: 'string',
},
};
export const uploadStatusStore = new Store({
name: 'upload-status',
schema: uploadStoreSchema,
});
export const keysStoreSchema: Schema<KeysStoreType> = {
AnonymizeUserID: {
type: 'object',
properties: {
id: {
type: 'string',
},
},
},
};
export const keysStore = new Store({
name: 'keys',
schema: keysStoreSchema,
});
export const safeStorageSchema: Schema<SafeStorageStoreType> = {
encryptionKey: {
type: 'string',
},
};
export const safeStorageStore = new Store({
name: 'safeStorage',
schema: safeStorageSchema,
});

View file

@ -4,7 +4,7 @@
"version": "1.7.0-alpha.7", "version": "1.7.0-alpha.7",
"private": true, "private": true,
"description": "Desktop client for ente.io", "description": "Desktop client for ente.io",
"main": "app/index.js", "main": "app/main.js",
"build": { "build": {
"appId": "io.ente.bhari-frame", "appId": "io.ente.bhari-frame",
"artifactName": "${productName}-${version}.${ext}", "artifactName": "${productName}-${version}.${ext}",
@ -53,12 +53,12 @@
}, },
"scripts": { "scripts": {
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"prebuild": "eslint \"main/**/*.{js,jsx,ts,tsx}\"", "prebuild": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
"prepare": "husky install", "prepare": "husky install",
"lint": "eslint -c .eslintrc --ext .ts ./main", "lint": "eslint -c .eslintrc --ext .ts src",
"watch": "tsc -w", "watch": "tsc -w",
"build-main": "yarn install && tsc", "build-main": "yarn install && tsc",
"start-main": "yarn build-main && electron app/index.js", "start-main": "yarn build-main && electron app/main.js",
"start-renderer": "cd ui && yarn install && yarn dev", "start-renderer": "cd ui && yarn install && yarn dev",
"start": "concurrently \"yarn start-main\" \"yarn start-renderer\"", "start": "concurrently \"yarn start-main\" \"yarn start-renderer\"",
"build-renderer": "cd ui && yarn install && yarn build && cd ..", "build-renderer": "cd ui && yarn install && yarn build && cd ..",
@ -68,6 +68,7 @@
"author": "ente <code@ente.io>", "author": "ente <code@ente.io>",
"devDependencies": { "devDependencies": {
"@sentry/cli": "^1.68.0", "@sentry/cli": "^1.68.0",
"@types/auto-launch": "^5.0.2",
"@types/get-folder-size": "^2.0.0", "@types/get-folder-size": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^5.28.0", "@typescript-eslint/eslint-plugin": "^5.28.0",
"@typescript-eslint/parser": "^5.28.0", "@typescript-eslint/parser": "^5.28.0",
@ -88,12 +89,14 @@
"@sentry/electron": "^2.5.1", "@sentry/electron": "^2.5.1",
"@types/node": "^14.14.37", "@types/node": "^14.14.37",
"@types/promise-fs": "^2.1.1", "@types/promise-fs": "^2.1.1",
"chokidar": "^3.5.3",
"auto-launch": "^5.0.5",
"electron-log": "^4.3.5", "electron-log": "^4.3.5",
"electron-reload": "^2.0.0-alpha.1", "electron-reload": "^2.0.0-alpha.1",
"electron-store": "^8.0.1", "electron-store": "^8.0.1",
"electron-updater": "^4.3.8", "electron-updater": "^4.3.8",
"get-folder-size": "^2.0.1", "get-folder-size": "^2.0.1",
"next-electron-server": "^0.0.8", "next-electron-server": "file:./thirdparty/next-electron-server",
"node-stream-zip": "^1.15.0", "node-stream-zip": "^1.15.0",
"promise-fs": "^2.1.1" "promise-fs": "^2.1.1"
}, },
@ -101,7 +104,7 @@
"parser": "babel-eslint" "parser": "babel-eslint"
}, },
"lint-staged": { "lint-staged": {
"main/**/*.{js,jsx,ts,tsx}": [ "src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix", "eslint --fix",
"prettier --write --ignore-unknown" "prettier --write --ignore-unknown"
] ]

35
src/api/cache.ts Normal file
View file

@ -0,0 +1,35 @@
import { ipcRenderer } from 'electron/renderer';
import path from 'path';
import { existsSync, mkdir, rmSync } from 'promise-fs';
import { DiskCache } from '../services/diskCache';
const CACHE_DIR = 'ente';
const getCacheDir = async () => {
const systemCacheDir = await ipcRenderer.invoke('get-path', 'cache');
return path.join(systemCacheDir, CACHE_DIR);
};
const getCacheBucketDir = async (cacheName: string) => {
const cacheDir = await getCacheDir();
const cacheBucketDir = path.join(cacheDir, cacheName);
return cacheBucketDir;
};
export async function openDiskCache(cacheName: string) {
const cacheBucketDir = await getCacheBucketDir(cacheName);
if (!existsSync(cacheBucketDir)) {
await mkdir(cacheBucketDir, { recursive: true });
}
return new DiskCache(cacheBucketDir);
}
export async function deleteDiskCache(cacheName: string) {
const cacheBucketDir = await getCacheBucketDir(cacheName);
if (existsSync(cacheBucketDir)) {
rmSync(cacheBucketDir, { recursive: true, force: true });
return true;
} else {
return false;
}
}

10
src/api/common.ts Normal file
View file

@ -0,0 +1,10 @@
import { ipcRenderer } from 'electron/renderer';
import { logError } from '../utils/logging';
export const selectRootDirectory = async (): Promise<string> => {
try {
return await ipcRenderer.invoke('select-dir');
} catch (e) {
logError(e, 'error while selecting root directory');
}
};

View file

@ -1,10 +1,7 @@
import { import { keysStore } from '../stores/keys.store';
uploadStatusStore, import { safeStorageStore } from '../stores/safeStorage.store';
keysStore, import { uploadStatusStore } from '../stores/upload.store';
safeStorageStore, import { logError } from '../utils/logging';
} from '../services/store';
import { logError } from './logging';
export const clearElectronStore = () => { export const clearElectronStore = () => {
try { try {

72
src/api/export.ts Normal file
View file

@ -0,0 +1,72 @@
import { readTextFile, writeStream } from './../services/fs';
import { ipcRenderer } from 'electron';
import { logError } from '../utils/logging';
import * as fs from 'promise-fs';
export const exists = (path: string) => {
return fs.existsSync(path);
};
export const checkExistsAndCreateCollectionDir = async (dirPath: string) => {
if (!fs.existsSync(dirPath)) {
await fs.mkdir(dirPath);
}
};
export const checkExistsAndRename = async (
oldDirPath: string,
newDirPath: string
) => {
if (fs.existsSync(oldDirPath)) {
await fs.rename(oldDirPath, newDirPath);
}
};
export const saveStreamToDisk = (
filePath: string,
fileStream: ReadableStream<any>
) => {
writeStream(filePath, fileStream);
};
export const saveFileToDisk = async (path: string, fileData: any) => {
await fs.writeFile(path, fileData);
};
export const getExportRecord = async (filePath: string) => {
try {
if (!fs.existsSync(filePath)) {
return null;
}
const recordFile = await readTextFile(filePath);
return recordFile;
} catch (e) {
logError(e, 'error while selecting files');
}
};
export const setExportRecord = async (filePath: string, data: string) => {
await fs.writeFile(filePath, data);
};
export const registerResumeExportListener = (resumeExport: () => void) => {
ipcRenderer.removeAllListeners('resume-export');
ipcRenderer.on('resume-export', () => resumeExport());
};
export const registerStopExportListener = (abortExport: () => void) => {
ipcRenderer.removeAllListeners('stop-export');
ipcRenderer.on('stop-export', () => abortExport());
};
export const registerPauseExportListener = (pauseExport: () => void) => {
ipcRenderer.removeAllListeners('pause-export');
ipcRenderer.on('pause-export', () => pauseExport());
};
export const registerRetryFailedExportListener = (
retryFailedExport: () => void
) => {
ipcRenderer.removeAllListeners('retry-export');
ipcRenderer.on('retry-export', () => retryFailedExport());
};

8
src/api/fs.ts Normal file
View file

@ -0,0 +1,8 @@
import { getElectronFile, getDirFilePaths } from '../services/fs';
export async function getDirFiles(dirPath: string) {
const files = await getDirFilePaths(dirPath);
const electronFiles = await Promise.all(files.map(getElectronFile));
return electronFiles;
}
export { isFolder } from '../services/fs';

View file

@ -1,6 +1,6 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { safeStorageStore } from '../services/store'; import { safeStorageStore } from '../stores/safeStorage.store';
import { logError } from './logging'; import { logError } from '../utils/logging';
export async function setEncryptionKey(encryptionKey: string) { export async function setEncryptionKey(encryptionKey: string) {
try { try {
@ -16,7 +16,7 @@ export async function setEncryptionKey(encryptionKey: string) {
} }
} }
export async function getEncryptionKey() { export async function getEncryptionKey(): Promise<string> {
try { try {
const b64EncryptedKey = safeStorageStore.get('encryptionKey'); const b64EncryptedKey = safeStorageStore.get('encryptionKey');
if (b64EncryptedKey) { if (b64EncryptedKey) {

11
src/api/system.ts Normal file
View file

@ -0,0 +1,11 @@
import { ipcRenderer } from 'electron';
export const sendNotification = (content: string) => {
ipcRenderer.send('send-notification', content);
};
export const showOnTray = (content: string) => {
ipcRenderer.send('update-tray', content);
};
export const reloadWindow = () => {
ipcRenderer.send('reload-window');
};

84
src/api/upload.ts Normal file
View file

@ -0,0 +1,84 @@
import { getElectronFile } from './../services/fs';
import { uploadStatusStore } from '../stores/upload.store';
import { ElectronFile, FILE_PATH_TYPE } from '../types';
import { logError } from '../utils/logging';
import { ipcRenderer } from 'electron';
import {
getElectronFilesFromGoogleZip,
getSavedFilePaths,
} from '../services/upload';
export const getPendingUploads = async () => {
const filePaths = getSavedFilePaths(FILE_PATH_TYPE.FILES);
const zipPaths = getSavedFilePaths(FILE_PATH_TYPE.ZIPS);
const collectionName = uploadStatusStore.get('collectionName');
let files: ElectronFile[] = [];
let type: FILE_PATH_TYPE;
if (zipPaths.length) {
type = FILE_PATH_TYPE.ZIPS;
for (const zipPath of zipPaths) {
files.push(...(await getElectronFilesFromGoogleZip(zipPath)));
}
const pendingFilePaths = new Set(filePaths);
files = files.filter((file) => pendingFilePaths.has(file.path));
} else if (filePaths.length) {
type = FILE_PATH_TYPE.FILES;
files = await Promise.all(filePaths.map(getElectronFile));
}
return {
files,
collectionName,
type,
};
};
export const showUploadDirsDialog = async () => {
try {
const filePaths: string[] = await ipcRenderer.invoke(
'show-upload-dirs-dialog'
);
const files = await Promise.all(filePaths.map(getElectronFile));
return files;
} catch (e) {
logError(e, 'error while selecting folders');
}
};
export const showUploadFilesDialog = async () => {
try {
const filePaths: string[] = await ipcRenderer.invoke(
'show-upload-files-dialog'
);
const files = await Promise.all(filePaths.map(getElectronFile));
return files;
} catch (e) {
logError(e, 'error while selecting files');
}
};
export const showUploadZipDialog = async () => {
try {
const filePaths: string[] = await ipcRenderer.invoke(
'show-upload-zip-dialog'
);
const files: ElectronFile[] = [];
for (const filePath of filePaths) {
files.push(...(await getElectronFilesFromGoogleZip(filePath)));
}
return {
zipPaths: filePaths,
files,
};
} catch (e) {
logError(e, 'error while selecting zips');
}
};
export {
setToUploadFiles,
getElectronFilesFromGoogleZip,
setToUploadCollection,
} from '../services/upload';

117
src/api/watch.ts Normal file
View file

@ -0,0 +1,117 @@
import { isMappingPresent } from '../utils/watch';
import path from 'path';
import { ipcRenderer } from 'electron';
import { ElectronFile, WatchMapping } from '../types';
import { getElectronFile } from '../services/fs';
import { getWatchMappings, setWatchMappings } from '../services/watch';
export async function addWatchMapping(
rootFolderName: string,
folderPath: string,
uploadStrategy: number
) {
folderPath = path.normalize(folderPath);
const watchMappings = getWatchMappings();
if (isMappingPresent(watchMappings, folderPath)) {
throw new Error(`Watch mapping already exists`);
}
await ipcRenderer.invoke('add-watcher', {
dir: folderPath,
});
watchMappings.push({
rootFolderName,
uploadStrategy,
folderPath,
syncedFiles: [],
ignoredFiles: [],
});
setWatchMappings(watchMappings);
}
export async function removeWatchMapping(folderPath: string) {
let watchMappings = getWatchMappings();
const watchMapping = watchMappings.find(
(mapping) => mapping.folderPath === folderPath
);
if (!watchMapping) {
throw new Error(`Watch mapping does not exist`);
}
await ipcRenderer.invoke('remove-watcher', {
dir: watchMapping.folderPath,
});
watchMappings = watchMappings.filter(
(mapping) => mapping.folderPath !== watchMapping.folderPath
);
setWatchMappings(watchMappings);
}
export function updateWatchMappingSyncedFiles(
folderPath: string,
files: WatchMapping['syncedFiles']
): void {
const watchMappings = getWatchMappings();
const watchMapping = watchMappings.find(
(mapping) => mapping.folderPath === folderPath
);
if (!watchMapping) {
throw Error(`Watch mapping not found`);
}
watchMapping.syncedFiles = files;
setWatchMappings(watchMappings);
}
export function updateWatchMappingIgnoredFiles(
folderPath: string,
files: WatchMapping['ignoredFiles']
): void {
const watchMappings = getWatchMappings();
const watchMapping = watchMappings.find(
(mapping) => mapping.folderPath === folderPath
);
if (!watchMapping) {
throw Error(`Watch mapping not found`);
}
watchMapping.ignoredFiles = files;
setWatchMappings(watchMappings);
}
export function registerWatcherFunctions(
addFile: (file: ElectronFile) => Promise<void>,
removeFile: (path: string) => Promise<void>,
removeFolder: (folderPath: string) => Promise<void>
) {
ipcRenderer.removeAllListeners('watch-add');
ipcRenderer.removeAllListeners('watch-change');
ipcRenderer.removeAllListeners('watch-unlink');
ipcRenderer.on('watch-add', async (_, filePath: string) => {
filePath = path.normalize(
filePath.split(path.sep).join(path.posix.sep)
);
await addFile(await getElectronFile(filePath));
});
ipcRenderer.on('watch-unlink', async (_, filePath: string) => {
filePath = path.normalize(
filePath.split(path.sep).join(path.posix.sep)
);
await removeFile(filePath);
});
ipcRenderer.on('watch-unlink-dir', async (_, folderPath: string) => {
folderPath = path.normalize(
folderPath.split(path.sep).join(path.posix.sep)
);
await removeFolder(folderPath);
});
}
export { getWatchMappings } from '../services/watch';

78
src/main.ts Normal file
View file

@ -0,0 +1,78 @@
import { app, BrowserWindow } from 'electron';
import { createWindow } from './utils/createWindow';
import setupIpcComs from './utils/ipcComms';
import { initWatcher } from './services/chokidar';
import { addAllowOriginHeader } from './utils/cors';
import {
setupTrayItem,
handleUpdates,
handleDownloads,
setupMacWindowOnDockIconClick,
setupMainMenu,
setupMainHotReload,
setupNextElectronServe,
enableSharedArrayBufferSupport,
handleDockIconHideOnAutoLaunch,
} from './utils/main';
import { initSentry } from './services/sentry';
let mainWindow: BrowserWindow;
let appIsQuitting = false;
let updateIsAvailable = false;
export const isAppQuitting = (): boolean => {
return appIsQuitting;
};
export const setIsAppQuitting = (value: boolean): void => {
appIsQuitting = value;
};
export const isUpdateAvailable = (): boolean => {
return updateIsAvailable;
};
export const setIsUpdateAvailable = (value: boolean): void => {
updateIsAvailable = value;
};
setupMainHotReload();
setupNextElectronServe();
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
handleDockIconHideOnAutoLaunch();
enableSharedArrayBufferSupport();
app.on('second-instance', () => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
mainWindow.show();
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
mainWindow.focus();
}
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
mainWindow = await createWindow();
const tray = setupTrayItem(mainWindow);
const watcher = initWatcher(mainWindow);
setupMacWindowOnDockIconClick();
initSentry();
setupMainMenu();
setupIpcComs(tray, mainWindow, watcher);
handleUpdates(mainWindow, tray);
handleDownloads(mainWindow);
addAllowOriginHeader(mainWindow);
});
app.on('before-quit', () => setIsAppQuitting(true));
}

79
src/preload.ts Normal file
View file

@ -0,0 +1,79 @@
import { reloadWindow, sendNotification, showOnTray } from './api/system';
import {
showUploadDirsDialog,
showUploadFilesDialog,
showUploadZipDialog,
getPendingUploads,
setToUploadFiles,
getElectronFilesFromGoogleZip,
setToUploadCollection,
} from './api/upload';
import {
registerWatcherFunctions,
addWatchMapping,
removeWatchMapping,
updateWatchMappingSyncedFiles,
updateWatchMappingIgnoredFiles,
getWatchMappings,
} from './api/watch';
import { getEncryptionKey, setEncryptionKey } from './api/safeStorage';
import { clearElectronStore } from './api/electronStore';
import { openDiskCache, deleteDiskCache } from './api/cache';
import {
checkExistsAndCreateCollectionDir,
checkExistsAndRename,
saveStreamToDisk,
saveFileToDisk,
registerResumeExportListener,
registerStopExportListener,
registerPauseExportListener,
registerRetryFailedExportListener,
getExportRecord,
setExportRecord,
exists,
} from './api/export';
import { selectRootDirectory } from './api/common';
import { fixHotReloadNext12 } from './utils/preload';
import { isFolder, getDirFiles } from './api/fs';
fixHotReloadNext12();
const windowObject: any = window;
windowObject['ElectronAPIs'] = {
exists,
checkExistsAndCreateCollectionDir,
checkExistsAndRename,
saveStreamToDisk,
saveFileToDisk,
selectRootDirectory,
clearElectronStore,
sendNotification,
showOnTray,
reloadWindow,
registerResumeExportListener,
registerStopExportListener,
registerPauseExportListener,
registerRetryFailedExportListener,
getExportRecord,
setExportRecord,
showUploadFilesDialog,
showUploadDirsDialog,
getPendingUploads,
setToUploadFiles,
showUploadZipDialog,
getElectronFilesFromGoogleZip,
setToUploadCollection,
getEncryptionKey,
setEncryptionKey,
openDiskCache,
deleteDiskCache,
getDirFiles,
getWatchMappings,
addWatchMapping,
removeWatchMapping,
registerWatcherFunctions,
isFolder,
updateWatchMappingSyncedFiles,
updateWatchMappingIgnoredFiles,
};

View file

@ -1,8 +1,8 @@
import { BrowserWindow, dialog, Tray } from 'electron'; import { BrowserWindow, dialog, Tray } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import log from 'electron-log'; import log from 'electron-log';
import { setIsAppQuitting, setIsUpdateAvailable } from '..'; import { setIsAppQuitting, setIsUpdateAvailable } from '../main';
import { buildContextMenu } from './menuUtil'; import { buildContextMenu } from '../utils/menu';
class AppUpdater { class AppUpdater {
constructor() { constructor() {

View file

@ -0,0 +1,36 @@
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()) {
this.client = macAndWindowsAutoLauncher;
} else {
this.client = linuxAutoLauncher;
}
}
async isEnabled() {
if (!this.client) {
this.init();
}
return await this.client.isEnabled();
}
async toggleAutoLaunch() {
if (!this.client) {
this.init();
}
await this.client.toggleAutoLaunch();
}
wasAutoLaunched() {
if (!this.client) {
this.init();
}
return this.client.wasAutoLaunched();
}
}
export default new AutoLauncher();

View file

@ -0,0 +1,37 @@
import AutoLaunch from 'auto-launch';
import { AutoLauncherClient } from '../../types/autoLauncher';
class LinuxAutoLauncher implements AutoLauncherClient {
private instance: AutoLaunch;
constructor() {
const autoLauncher = new AutoLaunch({
name: 'ente',
isHidden: true,
});
this.instance = autoLauncher;
}
async isEnabled() {
return await this.instance.isEnabled();
}
async toggleAutoLaunch() {
if (await this.isEnabled()) {
await this.disableAutoLaunch();
} else {
await this.enableAutoLaunch();
}
}
async wasAutoLaunched() {
// can't determine if it was auto launched
return false;
}
private async disableAutoLaunch() {
await this.instance.disable();
}
private async enableAutoLaunch() {
await this.instance.enable();
}
}
export default new LinuxAutoLauncher();

View file

@ -0,0 +1,28 @@
import { app } from 'electron';
import { AutoLauncherClient } from '../../types/autoLauncher';
class MacAndWindowsAutoLauncher implements AutoLauncherClient {
async isEnabled() {
return app.getLoginItemSettings().openAtLogin;
}
async toggleAutoLaunch() {
if (await this.isEnabled()) {
this.disableAutoLogin();
} else {
this.enableAutoLogin();
}
}
async wasAutoLaunched() {
return app.getLoginItemSettings().wasOpenedAtLogin;
}
private disableAutoLogin() {
app.setLoginItemSettings({ openAsHidden: true, openAtLogin: false });
}
private enableAutoLogin() {
app.setLoginItemSettings({ openAsHidden: true, openAtLogin: true });
}
}
export default new MacAndWindowsAutoLauncher();

33
src/services/chokidar.ts Normal file
View file

@ -0,0 +1,33 @@
import chokidar from 'chokidar';
import { BrowserWindow } from 'electron';
import { logError } from '../utils/logging';
import { getWatchMappings } from '../api/watch';
export function initWatcher(mainWindow: BrowserWindow) {
const mappings = getWatchMappings();
const folderPaths = mappings.map((mapping) => {
return mapping.folderPath;
});
const watcher = chokidar.watch(folderPaths, {
awaitWriteFinish: true,
});
watcher
.on('add', (path) => {
mainWindow.webContents.send('watch-add', path);
})
.on('change', (path) => {
mainWindow.webContents.send('watch-change', path);
})
.on('unlink', (path) => {
mainWindow.webContents.send('watch-unlink', path);
})
.on('unlinkDir', (path) => {
mainWindow.webContents.send('watch-unlink-dir', path);
})
.on('error', (error) => {
logError(error, 'error while watching files');
});
return watcher;
}

View file

@ -1,49 +1,11 @@
import { ipcRenderer } from 'electron/renderer';
import path from 'path'; import path from 'path';
import { import { readFile, writeFile, existsSync, unlink } from 'promise-fs';
readFile, import DiskLRUService from '../services/diskLRU';
writeFile,
existsSync,
mkdir,
rmSync,
unlink,
} from 'promise-fs';
import crypto from 'crypto'; import crypto from 'crypto';
import DiskLRUService from './diskLRU';
const CACHE_DIR = 'ente';
const MAX_CACHE_SIZE = 1000 * 1000 * 1000; // 1GB const MAX_CACHE_SIZE = 1000 * 1000 * 1000; // 1GB
const getCacheDir = async () => { export class DiskCache {
const systemCacheDir = await ipcRenderer.invoke('get-path', 'cache');
return path.join(systemCacheDir, CACHE_DIR);
};
const getCacheBucketDir = async (cacheName: string) => {
const cacheDir = await getCacheDir();
const cacheBucketDir = path.join(cacheDir, cacheName);
return cacheBucketDir;
};
export async function openDiskCache(cacheName: string) {
const cacheBucketDir = await getCacheBucketDir(cacheName);
if (!existsSync(cacheBucketDir)) {
await mkdir(cacheBucketDir, { recursive: true });
}
return new DiskCache(cacheBucketDir);
}
export async function deleteDiskCache(cacheName: string) {
const cacheBucketDir = await getCacheBucketDir(cacheName);
if (existsSync(cacheBucketDir)) {
rmSync(cacheBucketDir, { recursive: true, force: true });
return true;
} else {
return false;
}
}
class DiskCache {
constructor(private cacheBucketDir: string) {} constructor(private cacheBucketDir: string) {}
async put(cacheKey: string, response: Response): Promise<void> { async put(cacheKey: string, response: Response): Promise<void> {
@ -77,7 +39,6 @@ class DiskCache {
} }
} }
} }
function getAssetCachePath(cacheDir: string, cacheKey: string) { function getAssetCachePath(cacheDir: string, cacheKey: string) {
// hashing the key to prevent illegal filenames // hashing the key to prevent illegal filenames
const cacheKeyHash = crypto const cacheKeyHash = crypto

View file

@ -2,7 +2,7 @@ import path from 'path';
import { readdir, stat, unlink } from 'promise-fs'; import { readdir, stat, unlink } from 'promise-fs';
import getFolderSize from 'get-folder-size'; import getFolderSize from 'get-folder-size';
import { utimes, close, open } from 'promise-fs'; import { utimes, close, open } from 'promise-fs';
import { logError } from './logging'; import { logError } from '../utils/logging';
export interface LeastRecentlyUsedResult { export interface LeastRecentlyUsedResult {
atime: Date; atime: Date;

View file

@ -1,13 +1,12 @@
import path from 'path';
import StreamZip from 'node-stream-zip';
import * as fs from 'promise-fs';
import { FILE_STREAM_CHUNK_SIZE } from '../config'; import { FILE_STREAM_CHUNK_SIZE } from '../config';
import { uploadStatusStore } from '../services/store'; import path from 'path';
import { ElectronFile, FILE_PATH_KEYS, FILE_PATH_TYPE } from '../types'; import * as fs from 'promise-fs';
import { logError } from './logging'; import { ElectronFile } from '../types';
import StreamZip from 'node-stream-zip';
import { Readable } from 'stream';
// https://stackoverflow.com/a/63111390 // https://stackoverflow.com/a/63111390
export const getFilesFromDir = async (dirPath: string) => { export const getDirFilePaths = async (dirPath: string) => {
if (!(await fs.stat(dirPath)).isDirectory()) { if (!(await fs.stat(dirPath)).isDirectory()) {
return [dirPath]; return [dirPath];
} }
@ -17,20 +16,19 @@ export const getFilesFromDir = async (dirPath: string) => {
for (const filePath of filePaths) { for (const filePath of filePaths) {
const absolute = path.join(dirPath, filePath); const absolute = path.join(dirPath, filePath);
files = files.concat(await getFilesFromDir(absolute)); files = files.concat(await getDirFilePaths(absolute));
} }
return files; return files;
}; };
const getFileStream = async (filePath: string) => { export const getFileStream = async (filePath: string) => {
const file = await fs.open(filePath, 'r'); const file = await fs.open(filePath, 'r');
let offset = 0; let offset = 0;
const readableStream = new ReadableStream<Uint8Array>({ const readableStream = new ReadableStream<Uint8Array>({
async pull(controller) { async pull(controller) {
try { try {
const buff = new Uint8Array(FILE_STREAM_CHUNK_SIZE); const buff = new Uint8Array(FILE_STREAM_CHUNK_SIZE);
// original types were not working correctly // original types were not working correctly
const bytesRead = (await fs.read( const bytesRead = (await fs.read(
file, file,
@ -44,10 +42,9 @@ const getFileStream = async (filePath: string) => {
controller.close(); controller.close();
await fs.close(file); await fs.close(file);
} else { } else {
controller.enqueue(buff); controller.enqueue(buff.slice(0, bytesRead));
} }
} catch (e) { } catch (e) {
logError(e, 'stream pull failed');
await fs.close(file); await fs.close(file);
} }
}, },
@ -55,92 +52,6 @@ const getFileStream = async (filePath: string) => {
return readableStream; return readableStream;
}; };
const getZipFileStream = async (
zip: StreamZip.StreamZipAsync,
filePath: string
) => {
const stream = await zip.stream(filePath);
const done = { 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;
}
}
});
stream.on('end', () => {
done.current = true;
});
stream.on('error', (e) => {
done.current = true;
if (rejectObj) {
rejectObj(e);
rejectObj = null;
}
});
const readStreamData = () => {
return new Promise<Uint8Array>((resolve, reject) => {
const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer;
if (chunk || done.current) {
resolve(chunk);
} else {
resolveObj = resolve;
rejectObj = reject;
}
});
};
const readableStream = new ReadableStream<Uint8Array>({
async pull(controller) {
try {
const data = await readStreamData();
if (data) {
controller.enqueue(data);
} else {
controller.close();
}
} catch (e) {
logError(e, 'stream reading failed');
controller.close();
}
},
});
return readableStream;
};
async function getZipEntryAsElectronFile(
zip: StreamZip.StreamZipAsync,
entry: StreamZip.ZipEntry
): Promise<ElectronFile> {
return {
path: entry.name,
name: path.basename(entry.name),
size: entry.size,
lastModified: entry.time,
stream: async () => {
return await getZipFileStream(zip, entry.name);
},
blob: async () => {
const buffer = await zip.entryData(entry.name);
return new Blob([new Uint8Array(buffer)]);
},
arrayBuffer: async () => {
const buffer = await zip.entryData(entry.name);
return new Uint8Array(buffer);
},
};
}
export async function getElectronFile(filePath: string): Promise<ElectronFile> { export async function getElectronFile(filePath: string): Promise<ElectronFile> {
const fileStats = await fs.stat(filePath); const fileStats = await fs.stat(filePath);
return { return {
@ -162,77 +73,115 @@ export async function getElectronFile(filePath: string): Promise<ElectronFile> {
}; };
} }
export const setToUploadFiles = (type: FILE_PATH_TYPE, filePaths: string[]) => { export const getValidPaths = (paths: string[]) => {
const key = FILE_PATH_KEYS[type]; if (!paths) {
if (filePaths) { return [] as string[];
uploadStatusStore.set(key, filePaths);
} else {
uploadStatusStore.delete(key);
} }
}; return paths.filter(async (path) => {
export const setToUploadCollection = (collectionName: string) => {
if (collectionName) {
uploadStatusStore.set('collectionName', collectionName);
} else {
uploadStatusStore.delete('collectionName');
}
};
export const getSavedPaths = (type: FILE_PATH_TYPE) => {
const paths =
(uploadStatusStore.get(FILE_PATH_KEYS[type]) as string[]) ?? [];
const validPaths = paths.filter(async (path) => {
try { try {
await fs.stat(path).then((stat) => stat.isFile()); await fs.stat(path).then((stat) => stat.isFile());
} catch (e) { } catch (e) {
return false; return false;
} }
}); });
setToUploadFiles(type, validPaths);
return validPaths;
}; };
export const getPendingUploads = async () => { export const getZipFileStream = async (
const filePaths = getSavedPaths(FILE_PATH_TYPE.FILES); zip: StreamZip.StreamZipAsync,
const zipPaths = getSavedPaths(FILE_PATH_TYPE.ZIPS); filePath: string
const collectionName = uploadStatusStore.get('collectionName'); ) => {
const stream = await zip.stream(filePath);
const done = {
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;
let files: ElectronFile[] = []; if (chunk) {
let type: FILE_PATH_TYPE; resolveObj(new Uint8Array(chunk));
if (zipPaths.length) { resolveObj = null;
type = FILE_PATH_TYPE.ZIPS;
for (const zipPath of zipPaths) {
files.push(...(await getElectronFilesFromGoogleZip(zipPath)));
} }
const pendingFilePaths = new Set(filePaths);
files = files.filter((file) => pendingFilePaths.has(file.path));
} else if (filePaths.length) {
type = FILE_PATH_TYPE.FILES;
files = await Promise.all(filePaths.map(getElectronFile));
} }
return { });
files, stream.on('end', () => {
collectionName, done.current = true;
type, });
}; stream.on('error', (e) => {
}; done.current = true;
export const getElectronFilesFromGoogleZip = async (filePath: string) => { if (rejectObj) {
const zip = new StreamZip.async({ rejectObj(e);
file: filePath, rejectObj = null;
}
}); });
const entries = await zip.entries(); const readStreamData = () => {
const files: ElectronFile[] = []; return new Promise<Uint8Array>((resolve, reject) => {
const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer;
for (const entry of Object.values(entries)) { if (chunk || done.current) {
const basename = path.basename(entry.name); resolve(chunk);
if (entry.isFile && basename.length > 0 && basename[0] !== '.') { } else {
files.push(await getZipEntryAsElectronFile(zip, entry)); resolveObj = resolve;
rejectObj = reject;
} }
} });
return files;
}; };
const readableStream = new ReadableStream<Uint8Array>({
async pull(controller) {
try {
const data = await readStreamData();
if (data) {
controller.enqueue(data);
} else {
controller.close();
}
} catch (e) {
controller.close();
}
},
});
return readableStream;
};
export async function isFolder(dirPath: string) {
return await fs
.stat(dirPath)
.then((stats) => {
return stats.isDirectory();
})
.catch(() => false);
}
export const convertBrowserStreamToNode = (fileStream: any) => {
const reader = fileStream.getReader();
const rs = new Readable();
rs._read = async () => {
const result = await reader.read();
if (!result.done) {
rs.push(Buffer.from(result.value));
} else {
rs.push(null);
return;
}
};
return rs;
};
export function writeStream(filePath: string, fileStream: any) {
const writeable = fs.createWriteStream(filePath);
const readable = convertBrowserStreamToNode(fileStream);
readable.pipe(writeable);
}
export async function readTextFile(filePath: string) {
return await fs.readFile(filePath, 'utf-8');
}

View file

@ -1,14 +1,14 @@
import * as Sentry from '@sentry/electron/dist/main'; import * as Sentry from '@sentry/electron/dist/main';
import { keysStore } from '../stores/keys.store';
import { keysStore } from '../services/store'; import { isDev } from '../utils/common';
import { isDev } from './common';
const SENTRY_DSN = 'https://e9268b784d1042a7a116f53c58ad2165@sentry.ente.io/5'; const SENTRY_DSN = 'https://e9268b784d1042a7a116f53c58ad2165@sentry.ente.io/5';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const version = require('../../package.json').version; const version = require('../../package.json').version;
function initSentry(): void { export function initSentry(): void {
Sentry.init({ Sentry.init({
dsn: SENTRY_DSN, dsn: SENTRY_DSN,
release: version, release: version,
@ -46,6 +46,15 @@ function errorWithContext(originalError: Error, context: string) {
return errorWithContext; return errorWithContext;
} }
function getUserAnonymizedID() {
let anonymizeUserID = keysStore.get('AnonymizeUserID')?.id;
if (!anonymizeUserID) {
anonymizeUserID = makeID(6);
keysStore.set('AnonymizeUserID', { id: anonymizeUserID });
}
return anonymizeUserID;
}
function makeID(length: number) { function makeID(length: number) {
let result = ''; let result = '';
const characters = const characters =
@ -58,14 +67,3 @@ function makeID(length: number) {
} }
return result; return result;
} }
function getUserAnonymizedID() {
let anonymizeUserID = keysStore.get('AnonymizeUserID')?.id;
if (!anonymizeUserID) {
anonymizeUserID = makeID(6);
keysStore.set('AnonymizeUserID', { id: anonymizeUserID });
}
return anonymizeUserID;
}
export default initSentry;

73
src/services/upload.ts Normal file
View file

@ -0,0 +1,73 @@
import StreamZip from 'node-stream-zip';
import path from 'path';
import { uploadStatusStore } from '../stores/upload.store';
import { FILE_PATH_TYPE, FILE_PATH_KEYS, ElectronFile } from '../types';
import { getValidPaths, getZipFileStream } from './fs';
export const getSavedFilePaths = (type: FILE_PATH_TYPE) => {
const paths =
getValidPaths(
uploadStatusStore.get(FILE_PATH_KEYS[type]) as string[]
) ?? [];
setToUploadFiles(type, paths);
return paths;
};
export async function getZipEntryAsElectronFile(
zip: StreamZip.StreamZipAsync,
entry: StreamZip.ZipEntry
): Promise<ElectronFile> {
return {
path: entry.name,
name: path.basename(entry.name),
size: entry.size,
lastModified: entry.time,
stream: async () => {
return await getZipFileStream(zip, entry.name);
},
blob: async () => {
const buffer = await zip.entryData(entry.name);
return new Blob([new Uint8Array(buffer)]);
},
arrayBuffer: async () => {
const buffer = await zip.entryData(entry.name);
return new Uint8Array(buffer);
},
};
}
export const setToUploadFiles = (type: FILE_PATH_TYPE, filePaths: string[]) => {
const key = FILE_PATH_KEYS[type];
if (filePaths) {
uploadStatusStore.set(key, filePaths);
} else {
uploadStatusStore.delete(key);
}
};
export const setToUploadCollection = (collectionName: string) => {
if (collectionName) {
uploadStatusStore.set('collectionName', collectionName);
} else {
uploadStatusStore.delete('collectionName');
}
};
export const getElectronFilesFromGoogleZip = async (filePath: string) => {
const zip = new StreamZip.async({
file: filePath,
});
const entries = await zip.entries();
const files: ElectronFile[] = [];
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));
}
}
return files;
};

View file

@ -0,0 +1,10 @@
import { userPreferencesStore } from '../stores/userPreferences.store';
export function getHideDockIconPreference() {
const shouldHideDockIcon = userPreferencesStore.get('hideDockIcon');
return shouldHideDockIcon;
}
export function setHideDockIconPreference(shouldHideDockIcon: boolean) {
userPreferencesStore.set('hideDockIcon', shouldHideDockIcon);
}

11
src/services/watch.ts Normal file
View file

@ -0,0 +1,11 @@
import { WatchStoreType } from '../types';
import { watchStore } from '../stores/watch.store';
export function getWatchMappings() {
const mappings = watchStore.get('mappings') ?? [];
return mappings;
}
export function setWatchMappings(watchMappings: WatchStoreType['mappings']) {
watchStore.set('mappings', watchMappings);
}

18
src/stores/keys.store.ts Normal file
View file

@ -0,0 +1,18 @@
import Store, { Schema } from 'electron-store';
import { KeysStoreType } from '../types';
const keysStoreSchema: Schema<KeysStoreType> = {
AnonymizeUserID: {
type: 'object',
properties: {
id: {
type: 'string',
},
},
},
};
export const keysStore = new Store({
name: 'keys',
schema: keysStoreSchema,
});

View file

@ -0,0 +1,13 @@
import Store, { Schema } from 'electron-store';
import { SafeStorageStoreType } from '../types';
const safeStorageSchema: Schema<SafeStorageStoreType> = {
encryptionKey: {
type: 'string',
},
};
export const safeStorageStore = new Store({
name: 'safeStorage',
schema: safeStorageSchema,
});

View file

@ -0,0 +1,25 @@
import Store, { Schema } from 'electron-store';
import { UploadStoreType } from '../types';
const uploadStoreSchema: Schema<UploadStoreType> = {
filePaths: {
type: 'array',
items: {
type: 'string',
},
},
zipPaths: {
type: 'array',
items: {
type: 'string',
},
},
collectionName: {
type: 'string',
},
};
export const uploadStatusStore = new Store({
name: 'upload-status',
schema: uploadStoreSchema,
});

View file

@ -0,0 +1,13 @@
import Store, { Schema } from 'electron-store';
import { UserPreferencesType } from '../types';
const userPreferencesSchema: Schema<UserPreferencesType> = {
hideDockIcon: {
type: 'boolean',
},
};
export const userPreferencesStore = new Store({
name: 'userPreferences',
schema: userPreferencesSchema,
});

47
src/stores/watch.store.ts Normal file
View file

@ -0,0 +1,47 @@
import Store, { Schema } from 'electron-store';
import { WatchStoreType } from '../types';
const watchStoreSchema: Schema<WatchStoreType> = {
mappings: {
type: 'array',
items: {
type: 'object',
properties: {
rootFolderName: {
type: 'string',
},
uploadStrategy: {
type: 'number',
},
folderPath: {
type: 'string',
},
syncedFiles: {
type: 'array',
items: {
type: 'object',
properties: {
path: {
type: 'string',
},
id: {
type: 'number',
},
},
},
},
ignoredFiles: {
type: 'array',
items: {
type: 'string',
},
},
},
},
},
};
export const watchStore = new Store({
name: 'watch-status',
schema: watchStoreSchema,
});

View file

@ -0,0 +1,5 @@
export interface AutoLauncherClient {
isEnabled: () => Promise<boolean>;
toggleAutoLaunch: () => Promise<void>;
wasAutoLaunched: () => Promise<boolean>;
}

View file

@ -20,6 +20,24 @@ export interface KeysStoreType {
}; };
} }
interface WatchMappingSyncedFile {
path: string;
uploadedFileID: number;
collectionID: number;
}
export interface WatchMapping {
rootFolderName: string;
uploadStrategy: number;
folderPath: string;
syncedFiles: WatchMappingSyncedFile[];
ignoredFiles: string[];
}
export interface WatchStoreType {
mappings: WatchMapping[];
}
export enum FILE_PATH_TYPE { export enum FILE_PATH_TYPE {
FILES = 'files', FILES = 'files',
ZIPS = 'zips', ZIPS = 'zips',
@ -35,3 +53,7 @@ export const FILE_PATH_KEYS: {
export interface SafeStorageStoreType { export interface SafeStorageStoreType {
encryptionKey: string; encryptionKey: string;
} }
export interface UserPreferencesType {
hideDockIcon: boolean;
}

View file

@ -1,11 +1,13 @@
import { app, BrowserWindow, nativeImage } from 'electron'; import { app, BrowserWindow, nativeImage } from 'electron';
import * as path from 'path'; import * as path from 'path';
import { isDev } from './common'; import { isDev } from './common';
import { isAppQuitting } from '..'; import { isAppQuitting } from '../main';
import { addAllowOriginHeader } from './cors';
import { PROD_HOST_URL } from '../config'; import { PROD_HOST_URL } from '../config';
import { isPlatformMac } from './main';
import { getHideDockIconPreference } from '../services/userPreference';
import autoLauncher from '../services/autoLauncher';
export function createWindow(): BrowserWindow { export async function createWindow(): Promise<BrowserWindow> {
const appImgPath = isDev const appImgPath = isDev
? 'build/window-icon.png' ? 'build/window-icon.png'
: path.join(process.resourcesPath, 'window-icon.png'); : path.join(process.resourcesPath, 'window-icon.png');
@ -23,15 +25,16 @@ export function createWindow(): BrowserWindow {
show: false, // don't show the main window on load show: false, // don't show the main window on load
}); });
mainWindow.maximize(); mainWindow.maximize();
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
const splash = new BrowserWindow({ const splash = new BrowserWindow({
height: 600, height: 600,
width: 800, width: 800,
transparent: true, transparent: true,
show: !wasAutoLaunched,
}); });
splash.maximize(); splash.maximize();
addAllowOriginHeader(mainWindow);
if (isDev) { if (isDev) {
splash.loadFile(`../build/splash.html`); splash.loadFile(`../build/splash.html`);
mainWindow.loadURL(PROD_HOST_URL); mainWindow.loadURL(PROD_HOST_URL);
@ -45,25 +48,35 @@ export function createWindow(): BrowserWindow {
} }
mainWindow.webContents.on('did-fail-load', () => { mainWindow.webContents.on('did-fail-load', () => {
splash.close(); splash.close();
mainWindow.show();
isDev isDev
? mainWindow.loadFile(`../../build/error.html`) ? mainWindow.loadFile(`../../build/error.html`)
: splash.loadURL( : splash.loadURL(
`file://${path.join(process.resourcesPath, 'error.html')}` `file://${path.join(process.resourcesPath, 'error.html')}`
); );
}); });
mainWindow.once('ready-to-show', () => { mainWindow.once('ready-to-show', async () => {
mainWindow.show();
splash.destroy(); splash.destroy();
if (!wasAutoLaunched) {
mainWindow.show();
}
}); });
mainWindow.on('close', function (event) { mainWindow.on('close', function (event) {
if (!isAppQuitting()) { if (!isAppQuitting()) {
event.preventDefault(); event.preventDefault();
mainWindow.hide(); mainWindow.hide();
const isMac = process.platform === 'darwin';
isMac && app.dock.hide();
} }
return false; return false;
}); });
mainWindow.on('hide', () => {
const shouldHideDockIcon = getHideDockIconPreference();
if (isPlatformMac() && shouldHideDockIcon) {
app.dock.hide();
}
});
mainWindow.on('show', () => {
if (isPlatformMac()) {
app.dock.show();
}
});
return mainWindow; return mainWindow;
} }

View file

@ -8,23 +8,24 @@ import {
app, app,
} from 'electron'; } from 'electron';
import { createWindow } from './createWindow'; import { createWindow } from './createWindow';
import { buildContextMenu } from './menuUtil'; import { buildContextMenu } from './menu';
import { logErrorSentry } from './sentry'; import { logErrorSentry } from '../services/sentry';
import { getFilesFromDir } from './upload'; import chokidar from 'chokidar';
import path from 'path';
import { getDirFilePaths } from '../services/fs';
export default function setupIpcComs( export default function setupIpcComs(
tray: Tray, tray: Tray,
mainWindow: BrowserWindow mainWindow: BrowserWindow,
watcher: chokidar.FSWatcher
): void { ): void {
ipcMain.handle('select-dir', async () => { ipcMain.handle('select-dir', async () => {
const result = await dialog.showOpenDialog({ const result = await dialog.showOpenDialog({
properties: ['openDirectory'], properties: ['openDirectory'],
}); });
const dir = if (result.filePaths && result.filePaths.length > 0) {
result.filePaths && return result.filePaths[0]?.split(path.sep)?.join(path.posix.sep);
result.filePaths.length > 0 && }
result.filePaths[0];
return dir;
}); });
ipcMain.on('update-tray', (_, args) => { ipcMain.on('update-tray', (_, args) => {
@ -38,8 +39,8 @@ export default function setupIpcComs(
}; };
new Notification(notification).show(); new Notification(notification).show();
}); });
ipcMain.on('reload-window', () => { ipcMain.on('reload-window', async () => {
const secondWindow = createWindow(); const secondWindow = await createWindow();
mainWindow.destroy(); mainWindow.destroy();
mainWindow = secondWindow; mainWindow = secondWindow;
}); });
@ -66,12 +67,20 @@ export default function setupIpcComs(
let files: string[] = []; let files: string[] = [];
for (const dirPath of dir.filePaths) { for (const dirPath of dir.filePaths) {
files = files.concat(await getFilesFromDir(dirPath)); files = files.concat(await getDirFilePaths(dirPath));
} }
return files; return files;
}); });
ipcMain.handle('add-watcher', async (_, args: { dir: string }) => {
watcher.add(args.dir);
});
ipcMain.handle('remove-watcher', async (_, args: { dir: string }) => {
watcher.unwatch(args.dir);
});
ipcMain.handle('log-error', (_, err, msg, info?) => { ipcMain.handle('log-error', (_, err, msg, info?) => {
logErrorSentry(err, msg, info); logErrorSentry(err, msg, info);
}); });

101
src/utils/main.ts Normal file
View file

@ -0,0 +1,101 @@
import { PROD_HOST_URL, RENDERER_OUTPUT_DIR } from '../config';
import { nativeImage, Tray, app, BrowserWindow, Menu } from 'electron';
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';
export function handleUpdates(mainWindow: BrowserWindow, tray: Tray) {
if (!isDev) {
appUpdater.checkForUpdate(tray, mainWindow);
}
}
export function setupTrayItem(mainWindow: BrowserWindow) {
const trayImgPath = isDev
? 'build/taskbar-icon.png'
: path.join(process.resourcesPath, 'taskbar-icon.png');
const trayIcon = nativeImage.createFromPath(trayImgPath);
const tray = new Tray(trayIcon);
tray.setToolTip('ente');
tray.setContextMenu(buildContextMenu(mainWindow));
return tray;
}
export function handleDownloads(mainWindow: BrowserWindow) {
mainWindow.webContents.session.on('will-download', (_, item) => {
item.setSavePath(
getUniqueSavePath(item.getFilename(), app.getPath('downloads'))
);
});
}
export function getUniqueSavePath(filename: string, directory: string): string {
let uniqueFileSavePath = path.join(directory, filename);
const { name: filenameWithoutExtension, ext: extension } =
path.parse(filename);
let n = 0;
while (existsSync(uniqueFileSavePath)) {
n++;
// filter need to remove undefined extension from the array
// else [`${fileName}`, undefined].join(".") will lead to `${fileName}.` as joined string
const fileNameWithNumberedSuffix = [
`${filenameWithoutExtension}(${n})`,
extension,
]
.filter((x) => x) // filters out undefined/null values
.join('.');
uniqueFileSavePath = path.join(directory, fileNameWithNumberedSuffix);
}
return uniqueFileSavePath;
}
export function setupMacWindowOnDockIconClick() {
app.on('activate', function () {
const windows = BrowserWindow.getAllWindows();
// we allow only one window
windows[0].show();
});
}
export async function setupMainMenu() {
Menu.setApplicationMenu(await buildMenuBar());
}
export function setupMainHotReload() {
if (isDev) {
electronReload(__dirname, {});
}
}
export function setupNextElectronServe() {
serveNextAt(PROD_HOST_URL, {
outputDir: RENDERER_OUTPUT_DIR,
});
}
export function isPlatformMac() {
return process.platform === 'darwin';
}
export function isPlatformWindows() {
return process.platform === 'win32';
}
export async function handleDockIconHideOnAutoLaunch() {
const shouldHideDockIcon = getHideDockIconPreference();
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
if (isPlatformMac() && shouldHideDockIcon && wasAutoLaunched) {
app.dock.hide();
}
}
export function enableSharedArrayBufferSupport() {
app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer');
}

View file

@ -5,10 +5,15 @@ import {
BrowserWindow, BrowserWindow,
MenuItemConstructorOptions, MenuItemConstructorOptions,
} from 'electron'; } from 'electron';
import { isUpdateAvailable, setIsAppQuitting } from '..'; import {
import { showUpdateDialog } from './appUpdater'; getHideDockIconPreference,
setHideDockIconPreference,
const isMac = process.platform === 'darwin'; } from '../services/userPreference';
import { isUpdateAvailable, setIsAppQuitting } from '../main';
import autoLauncher from '../services/autoLauncher';
import { isPlatformMac } from './main';
import { showUpdateDialog } from '../services/appUpdater';
import { isDev } from './common';
export function buildContextMenu( export function buildContextMenu(
mainWindow: BrowserWindow, mainWindow: BrowserWindow,
@ -77,8 +82,6 @@ export function buildContextMenu(
label: 'Open ente', label: 'Open ente',
click: function () { click: function () {
mainWindow.show(); mainWindow.show();
const isMac = process.platform === 'darwin';
isMac && app.dock.show();
}, },
}, },
{ {
@ -92,34 +95,65 @@ export function buildContextMenu(
return contextMenu; return contextMenu;
} }
export function buildMenuBar(): Menu { export async function buildMenuBar(): Promise<Menu> {
let isAutoLaunchEnabled = await autoLauncher.isEnabled();
const isMac = isPlatformMac();
let shouldHideDockIcon = getHideDockIconPreference();
const template: MenuItemConstructorOptions[] = [ const template: MenuItemConstructorOptions[] = [
{ {
label: app.name, label: 'ente',
submenu: [ submenu: [
...((isMac ...((isMac
? [ ? [
{ {
label: 'About', label: 'About ente',
role: 'about', role: 'about',
}, },
] ]
: []) as MenuItemConstructorOptions[]), : []) as MenuItemConstructorOptions[]),
{ type: 'separator' },
{ {
label: 'FAQ', label: 'Preferences',
click: () => shell.openExternal('https://ente.io/faq/'), submenu: [
{
label: 'Open ente on startup',
type: 'checkbox',
checked: isAutoLaunchEnabled,
click: () => {
autoLauncher.toggleAutoLaunch();
isAutoLaunchEnabled = !isAutoLaunchEnabled;
},
}, },
{ {
label: 'Support', label: 'Hide dock icon',
click: () => shell.openExternal('mailto:support@ente.io'), type: 'checkbox',
checked: shouldHideDockIcon,
click: () => {
setHideDockIconPreference(!shouldHideDockIcon);
shouldHideDockIcon = !shouldHideDockIcon;
},
},
],
},
{ type: 'separator' },
...((isMac
? [
{
label: 'Hide ente',
role: 'hide',
}, },
{ {
label: 'Quit', label: 'Hide others',
accelerator: 'CommandOrControl+Q', role: 'hideOthers',
click() {
setIsAppQuitting(true);
app.quit();
}, },
]
: []) as MenuItemConstructorOptions[]),
{ type: 'separator' },
{
label: 'Quit ente',
role: 'quit',
}, },
], ],
}, },
@ -161,14 +195,17 @@ export function buildMenuBar(): Menu {
]) as MenuItemConstructorOptions[]), ]) as MenuItemConstructorOptions[]),
], ],
}, },
// { role: 'viewMenu' }
{ {
label: 'View', label: 'View',
submenu: [ submenu: [
...((isDev
? [
{ role: 'reload', label: 'Reload' }, { role: 'reload', label: 'Reload' },
{ role: 'forceReload', label: 'Force reload' }, { role: 'forceReload', label: 'Force reload' },
{ role: 'toggleDevTools', label: 'Toggle dev tools' }, { role: 'toggleDevTools', label: 'Toggle dev tools' },
{ type: 'separator' }, { type: 'separator' },
]
: []) as MenuItemConstructorOptions[]),
{ role: 'resetZoom', label: 'Reset zoom' }, { role: 'resetZoom', label: 'Reset zoom' },
{ role: 'zoomIn', label: 'Zoom in' }, { role: 'zoomIn', label: 'Zoom in' },
{ role: 'zoomOut', label: 'Zoom out' }, { role: 'zoomOut', label: 'Zoom out' },
@ -176,7 +213,6 @@ export function buildMenuBar(): Menu {
{ role: 'togglefullscreen', label: 'Toggle fullscreen' }, { role: 'togglefullscreen', label: 'Toggle fullscreen' },
], ],
}, },
// { role: 'windowMenu' }
{ {
label: 'Window', label: 'Window',
submenu: [ submenu: [
@ -184,15 +220,33 @@ export function buildMenuBar(): Menu {
...((isMac ...((isMac
? [ ? [
{ type: 'separator' }, { type: 'separator' },
{ role: 'front', label: 'Front' }, { role: 'front', label: 'Bring to front' },
{ type: 'separator' }, { type: 'separator' },
{ role: 'window', label: 'Window' }, { role: 'window', label: 'ente' },
] ]
: [ : [
{ role: 'close', label: 'Close' }, { 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/'),
},
],
},
]; ];
return Menu.buildFromTemplate(template); return Menu.buildFromTemplate(template);
} }

16
src/utils/preload.ts Normal file
View file

@ -0,0 +1,16 @@
import { webFrame } from 'electron';
export const fixHotReloadNext12 = () => {
webFrame.executeJavaScript(`Object.defineProperty(globalThis, 'WebSocket', {
value: new Proxy(WebSocket, {
construct: (Target, [url, protocols]) => {
if (url.endsWith('/_next/webpack-hmr')) {
// Fix the Next.js hmr client url
return new Target("ws://localhost:3000/_next/webpack-hmr", protocols)
} else {
return new Target(url, protocols)
}
}
})
});`);
};

11
src/utils/watch.ts Normal file
View file

@ -0,0 +1,11 @@
import { WatchMapping } from '../types';
export function isMappingPresent(
watchMappings: WatchMapping[],
folderPath: string
) {
const watchMapping = watchMappings?.find(
(mapping) => mapping.folderPath === folderPath
);
return !!watchMapping;
}

1
thirdparty/next-electron-server vendored Submodule

@ -0,0 +1 @@
Subproject commit a88030295c89dd8f43d9e3a45025678d95c78a45

View file

@ -6,10 +6,10 @@
"noImplicitAny": true, "noImplicitAny": true,
"sourceMap": true, "sourceMap": true,
"outDir": "app", "outDir": "app",
"baseUrl": "./main", "baseUrl": "src",
"paths": { "paths": {
"*": ["node_modules/*"] "*": ["node_modules/*"]
} }
}, },
"include": ["main/**/*"] "include": ["src/**/*"]
} }

View file

@ -265,6 +265,11 @@
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
"@types/auto-launch@^5.0.2":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@types/auto-launch/-/auto-launch-5.0.2.tgz#4970f01e5dd27572489b7fe77590204a19f86bd0"
integrity sha512-b03X09+GCM9t6AUECpwA2gUPYs8s5tJHFJw92sK8EiJ7G4QNbsHmXV7nfCfP6G6ivtm230vi4oNfe8AzRgzxMQ==
"@types/debug@^4.1.6": "@types/debug@^4.1.6":
version "4.1.7" version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
@ -594,6 +599,11 @@ app-builder-lib@23.1.0:
tar "^6.1.11" tar "^6.1.11"
temp-file "^3.4.0" temp-file "^3.4.0"
applescript@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/applescript/-/applescript-1.0.0.tgz#bb87af568cad034a4e48c4bdaf6067a3a2701317"
integrity sha512-yvtNHdWvtbYEiIazXAdp/NY+BBb65/DAseqlNiJQjOx9DynuzOYDbVLBJvuc0ve0VL9x6B3OHF6eH52y9hCBtQ==
aproba@^1.0.3: aproba@^1.0.3:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
@ -678,6 +688,17 @@ atomically@^1.7.0:
resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe" resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe"
integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w== integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==
auto-launch@^5.0.5:
version "5.0.5"
resolved "https://registry.yarnpkg.com/auto-launch/-/auto-launch-5.0.5.tgz#d14bd002b1ef642f85e991a6195ff5300c8ad3c0"
integrity sha512-ppdF4mihhYzMYLuCcx9H/c5TUOCev8uM7en53zWVQhyYAJrurd2bFZx3qQVeJKF2jrc7rsPRNN5cD+i23l6PdA==
dependencies:
applescript "^1.0.0"
mkdirp "^0.5.1"
path-is-absolute "^1.0.0"
untildify "^3.0.2"
winreg "1.2.4"
aws-sign2@~0.7.0: aws-sign2@~0.7.0:
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@ -888,7 +909,7 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
chokidar@^3.5.2: chokidar@^3.5.2, chokidar@^3.5.3:
version "3.5.3" version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@ -2724,7 +2745,7 @@ minizlib@^2.1.1:
minipass "^3.0.0" minipass "^3.0.0"
yallist "^4.0.0" yallist "^4.0.0"
mkdirp@^0.5.4, mkdirp@^0.5.5: mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@^0.5.5:
version "0.5.6" version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
@ -2756,10 +2777,8 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
next-electron-server@^0.0.8: "next-electron-server@file:./thirdparty/next-electron-server":
version "0.0.8" version "0.0.8"
resolved "https://registry.yarnpkg.com/next-electron-server/-/next-electron-server-0.0.8.tgz#bc17772636a099d678a6f0c3f9bc8e952ee75967"
integrity sha512-LQhqf1Lrw9QVPntWN8NtqQk4WMufxXinsfg2ODgfqiHpvhcG58MOVIfvtDL37yGRvswORLXUHYRNB+p59LAd/w==
node-addon-api@^1.6.3: node-addon-api@^1.6.3:
version "1.7.2" version "1.7.2"
@ -3878,6 +3897,11 @@ universalify@^2.0.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
untildify@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9"
integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==
update-notifier@^5.1.0: update-notifier@^5.1.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9"
@ -3992,6 +4016,11 @@ widest-line@^3.1.0:
dependencies: dependencies:
string-width "^4.0.0" string-width "^4.0.0"
winreg@1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b"
integrity sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA==
word-wrap@^1.2.3: word-wrap@^1.2.3:
version "1.2.3" version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"