diff --git a/desktop/package.json b/desktop/package.json index 0aa24aba3..ce88d75e2 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -28,7 +28,6 @@ "electron-store": "^8.0.1", "electron-updater": "^4.3.8", "ffmpeg-static": "^5.1.0", - "get-folder-size": "^2.0.1", "html-entities": "^2.4.0", "jpeg-js": "^0.4.4", "next-electron-server": "^1", @@ -38,7 +37,6 @@ "devDependencies": { "@types/auto-launch": "^5.0.2", "@types/ffmpeg-static": "^3.0.1", - "@types/get-folder-size": "^2.0.0", "@typescript-eslint/eslint-plugin": "^7", "@typescript-eslint/parser": "^7", "concurrently": "^8", diff --git a/desktop/src/api/cache.ts b/desktop/src/api/cache.ts deleted file mode 100644 index fe5f7bd93..000000000 --- a/desktop/src/api/cache.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ipcRenderer } from "electron/renderer"; -import { existsSync } from "node:fs"; -import * as fs from "node:fs/promises"; -import path from "path"; -import { DiskCache } from "../services/diskCache"; - -const ENTE_CACHE_DIR_NAME = "ente"; - -const getCacheDirectory = async () => { - const defaultSystemCacheDir = await ipcRenderer.invoke("get-path", "cache"); - return path.join(defaultSystemCacheDir, ENTE_CACHE_DIR_NAME); -}; - -const getCacheBucketDir = async (cacheName: string) => { - const cacheDir = await getCacheDirectory(); - const cacheBucketDir = path.join(cacheDir, cacheName); - return cacheBucketDir; -}; - -export async function openDiskCache( - cacheName: string, - cacheLimitInBytes?: number, -) { - const cacheBucketDir = await getCacheBucketDir(cacheName); - await fs.mkdir(cacheBucketDir, { recursive: true }); - return new DiskCache(cacheBucketDir, cacheLimitInBytes); -} - -export async function deleteDiskCache(cacheName: string) { - const cacheBucketDir = await getCacheBucketDir(cacheName); - if (existsSync(cacheBucketDir)) { - await fs.rm(cacheBucketDir, { recursive: true, force: true }); - return true; - } else { - return false; - } -} diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 0a1fbe818..e8a21c643 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -1,6 +1,21 @@ -import { app, BrowserWindow } from "electron"; +/** + * @file Entry point for the main (Node.js) process of our Electron app. + * + * The code in this file is invoked by Electron when our app starts - + * Conceptually (after all the transpilation etc has happened) this can be + * thought of `electron main.ts`. We're running in the context of the so called + * "main" process which runs in a Node.js environment. + * + * https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process + */ +import * as log from "electron-log"; +import { app, BrowserWindow } from "electron/main"; import serveNextAt from "next-electron-server"; +import { existsSync } from "node:fs"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; import { initWatcher } from "./services/chokidar"; +import { logErrorSentry } from "./services/sentry"; import { isDev } from "./utils/common"; import { addAllowOriginHeader } from "./utils/cors"; import { createWindow } from "./utils/createWindow"; @@ -8,7 +23,6 @@ import { setupAppEventEmitter } from "./utils/events"; import setupIpcComs from "./utils/ipcComms"; import { setupLogging } from "./utils/logging"; import { - enableSharedArrayBufferSupport, handleDockIconHideOnAutoLaunch, handleDownloads, handleExternalLinks, @@ -19,8 +33,6 @@ import { setupTrayItem, } from "./utils/main"; -let mainWindow: BrowserWindow; - let appIsQuitting = false; let updateIsAvailable = false; @@ -63,15 +75,68 @@ const setupRendererServer = () => { serveNextAt(rendererURL); }; -setupRendererServer(); -setupLogging(isDev); +function enableSharedArrayBufferSupport() { + app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); +} -const gotTheLock = app.requestSingleInstanceLock(); -if (!gotTheLock) { - app.quit(); -} else { +/** + * [Note: Increased disk cache for the desktop app] + * + * Set the "disk-cache-size" command line flag to ask the Chromium process to + * use a larger size for the caches that it keeps on disk. This allows us to use + * the same web-native caching mechanism on both the web and the desktop app, + * just ask the embedded Chromium to be a bit more generous in disk usage when + * running as the desktop app. + * + * The size we provide is in bytes. We set it to a large value, 5 GB (5 * 1024 * + * 1024 * 1024 = 5368709120) + * https://www.electronjs.org/docs/latest/api/command-line-switches#--disk-cache-sizesize + * + * Note that increasing the disk cache size does not guarantee that Chromium + * will respect in verbatim, it uses its own heuristics atop this hint. + * https://superuser.com/questions/378991/what-is-chrome-default-cache-size-limit/1577693#1577693 + */ +const increaseDiskCache = () => { + app.commandLine.appendSwitch("disk-cache-size", "5368709120"); +}; + +/** + * Older versions of our app used to maintain a cache dir using the main + * process. This has been deprecated in favor of using a normal web cache (See: + * [Note: Increased disk cache for the desktop app]). + * + * Delete the old cache dir if it exists. This code was added March 2024, and + * can be removed after some time once most people have upgraded to newer + * versions. + */ +const deleteLegacyDiskCacheDirIfExists = async () => { + // The existing code was passing "cache" as a parameter to getPath. This was + // incorrect, cache is not a valid value. However, we replicate that + // behaviour so that we get back the same path that the old got was getting. + // @ts-expect-error + const cacheDir = path.join(app.getPath("cache"), "ente"); + if (existsSync(cacheDir)) { + log.info(`Removing legacy disk cache from ${cacheDir}`); + await fs.rm(cacheDir, { recursive: true }); + } +}; + +const main = () => { + setupLogging(isDev); + + const gotTheLock = app.requestSingleInstanceLock(); + if (!gotTheLock) { + app.quit(); + return; + } + + let mainWindow: BrowserWindow; + + setupRendererServer(); handleDockIconHideOnAutoLaunch(); + increaseDiskCache(); enableSharedArrayBufferSupport(); + app.on("second-instance", () => { // Someone tried to run a second instance, we should focus our window. if (mainWindow) { @@ -99,7 +164,17 @@ if (!gotTheLock) { handleExternalLinks(mainWindow); addAllowOriginHeader(mainWindow); setupAppEventEmitter(mainWindow); + + try { + deleteLegacyDiskCacheDirIfExists(); + } catch (e) { + // Log but otherwise ignore errors during non-critical startup + // actions + logErrorSentry(e, "Ignoring startup error"); + } }); app.on("before-quit", () => setIsAppQuitting(true)); -} +}; + +main(); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 8c717e8ab..a4d8e4614 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -32,7 +32,6 @@ import { createWriteStream, existsSync } from "node:fs"; import * as fs from "node:fs/promises"; import { Readable } from "node:stream"; import path from "path"; -import { deleteDiskCache, openDiskCache } from "./api/cache"; import { logToDisk, openLogDirectory } from "./api/common"; import { runFFmpegCmd } from "./api/ffmpeg"; import { getDirFiles } from "./api/fs"; @@ -446,8 +445,6 @@ contextBridge.exposeInMainWorld("ElectronAPIs", { setToUploadCollection, getEncryptionKey, setEncryptionKey, - openDiskCache, - deleteDiskCache, getDirFiles, getWatchMappings, addWatchMapping, diff --git a/desktop/src/services/clipService.ts b/desktop/src/services/clipService.ts index 58f0d376b..268694a7f 100644 --- a/desktop/src/services/clipService.ts +++ b/desktop/src/services/clipService.ts @@ -63,17 +63,9 @@ const TEXT_MODEL_SIZE_IN_BYTES = { onnx: 64173509, // 61.2 MB }; -const MODEL_SAVE_FOLDER = "models"; - -function getModelSavePath(modelName: string) { - let userDataDir: string; - if (isDev) { - userDataDir = "."; - } else { - userDataDir = app.getPath("userData"); - } - return path.join(userDataDir, MODEL_SAVE_FOLDER, modelName); -} +/** Return the path where the given {@link modelName} is meant to be saved */ +const getModelSavePath = (modelName: string) => + path.join(app.getPath("userData"), "models", modelName); async function downloadModel(saveLocation: string, url: string) { // confirm that the save location exists diff --git a/desktop/src/services/diskCache.ts b/desktop/src/services/diskCache.ts deleted file mode 100644 index 947e45aa8..000000000 --- a/desktop/src/services/diskCache.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { existsSync } from "node:fs"; -import * as fs from "node:fs/promises"; -import * as path from "node:path"; -import DiskLRUService from "../services/diskLRU"; -import { LimitedCache } from "../types/cache"; -import { getFileStream, writeStream } from "./fs"; -import { logError } from "./logging"; - -const DEFAULT_CACHE_LIMIT = 1000 * 1000 * 1000; // 1GB - -export class DiskCache implements LimitedCache { - constructor( - private cacheBucketDir: string, - private cacheLimit = DEFAULT_CACHE_LIMIT, - ) {} - - async put(cacheKey: string, response: Response): Promise { - const cachePath = path.join(this.cacheBucketDir, cacheKey); - await writeStream(cachePath, response.body); - DiskLRUService.enforceCacheSizeLimit( - this.cacheBucketDir, - this.cacheLimit, - ); - } - - async match( - cacheKey: string, - { sizeInBytes }: { sizeInBytes?: number } = {}, - ): Promise { - const cachePath = path.join(this.cacheBucketDir, cacheKey); - if (existsSync(cachePath)) { - const fileStats = await fs.stat(cachePath); - if (sizeInBytes && fileStats.size !== sizeInBytes) { - logError( - Error(), - "Cache key exists but size does not match. Deleting cache key.", - ); - fs.unlink(cachePath).catch((e) => { - if (e.code === "ENOENT") return; - logError(e, "Failed to delete cache key"); - }); - return undefined; - } - DiskLRUService.markUse(cachePath); - return new Response(await getFileStream(cachePath)); - } else { - return undefined; - } - } - async delete(cacheKey: string): Promise { - const cachePath = path.join(this.cacheBucketDir, cacheKey); - if (existsSync(cachePath)) { - await fs.unlink(cachePath); - return true; - } else { - return false; - } - } -} diff --git a/desktop/src/services/diskLRU.ts b/desktop/src/services/diskLRU.ts deleted file mode 100644 index d42e1cabc..000000000 --- a/desktop/src/services/diskLRU.ts +++ /dev/null @@ -1,95 +0,0 @@ -import getFolderSize from "get-folder-size"; -import * as fs from "node:fs/promises"; -import * as path from "node:path"; -import { logError } from "../services/logging"; - -export interface LeastRecentlyUsedResult { - atime: Date; - path: string; -} - -class DiskLRUService { - private isRunning: Promise = null; - private reRun: boolean = false; - - /** Mark "use" of a given file by updating its modified time */ - async markUse(path: string) { - const now = new Date(); - await fs.utimes(path, now, now); - } - - enforceCacheSizeLimit(cacheDir: string, maxSize: number) { - if (!this.isRunning) { - this.isRunning = this.evictLeastRecentlyUsed(cacheDir, maxSize); - this.isRunning.then(() => { - this.isRunning = null; - if (this.reRun) { - this.reRun = false; - this.enforceCacheSizeLimit(cacheDir, maxSize); - } - }); - } else { - this.reRun = true; - } - } - - async evictLeastRecentlyUsed(cacheDir: string, maxSize: number) { - try { - await new Promise((resolve) => { - getFolderSize(cacheDir, async (err, size) => { - if (err) { - throw err; - } - if (size >= maxSize) { - const leastRecentlyUsed = - await this.findLeastRecentlyUsed(cacheDir); - try { - await fs.unlink(leastRecentlyUsed.path); - } catch (e) { - // ENOENT: File not found - // which can be ignored as we are trying to delete the file anyway - if (e.code !== "ENOENT") { - logError( - e, - "Failed to evict least recently used", - ); - } - // ignoring the error, as it would get retried on the next run - } - this.evictLeastRecentlyUsed(cacheDir, maxSize); - } - resolve(null); - }); - }); - } catch (e) { - logError(e, "evictLeastRecentlyUsed failed"); - } - } - - private async findLeastRecentlyUsed( - dir: string, - result?: LeastRecentlyUsedResult, - ): Promise { - result = result || { atime: new Date(), path: "" }; - - const files = await fs.readdir(dir); - for (const file of files) { - const newBase = path.join(dir, file); - const st = await fs.stat(newBase); - if (st.isDirectory()) { - result = await this.findLeastRecentlyUsed(newBase, result); - } else { - const { atime } = st; - if (st.atime.getTime() < result.atime.getTime()) { - result = { - atime, - path: newBase, - }; - } - } - } - return result; - } -} - -export default new DiskLRUService(); diff --git a/desktop/src/types/cache.ts b/desktop/src/types/cache.ts deleted file mode 100644 index 112716eea..000000000 --- a/desktop/src/types/cache.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface LimitedCache { - match: ( - key: string, - options?: { sizeInBytes?: number }, - ) => Promise; - put: (key: string, data: Response) => Promise; - delete: (key: string) => Promise; -} diff --git a/desktop/src/utils/ipcComms.ts b/desktop/src/utils/ipcComms.ts index 861cec75e..4312c3362 100644 --- a/desktop/src/utils/ipcComms.ts +++ b/desktop/src/utils/ipcComms.ts @@ -95,7 +95,13 @@ export default function setupIpcComs( clearElectronStore(); }); - ipcMain.handle("get-path", (_, message) => { + ipcMain.handle("convert-to-jpeg", (_, fileData, filename) => { + return convertToJPEG(fileData, filename); + }); + + ipcMain.handle("open-log-dir", () => { + // [Note: Electron app paths] + // // By default, these paths are at the following locations: // // * macOS: `~/Library/Application Support/ente` @@ -104,14 +110,6 @@ export default function setupIpcComs( // * Windows: C:\Users\\AppData\Local\ // // https://www.electronjs.org/docs/latest/api/app - return app.getPath(message); - }); - - ipcMain.handle("convert-to-jpeg", (_, fileData, filename) => { - return convertToJPEG(fileData, filename); - }); - - ipcMain.handle("open-log-dir", () => { shell.openPath(app.getPath("logs")); }); diff --git a/desktop/src/utils/main.ts b/desktop/src/utils/main.ts index 9041a6d15..28013d18a 100644 --- a/desktop/src/utils/main.ts +++ b/desktop/src/utils/main.ts @@ -94,10 +94,6 @@ export async function handleDockIconHideOnAutoLaunch() { } } -export function enableSharedArrayBufferSupport() { - app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); -} - export function logSystemInfo() { const systemVersion = process.getSystemVersion(); const osName = process.platform; diff --git a/desktop/src/utils/temp.ts b/desktop/src/utils/temp.ts index 7bb20468a..099e6b667 100644 --- a/desktop/src/utils/temp.ts +++ b/desktop/src/utils/temp.ts @@ -1,4 +1,4 @@ -import { app } from "electron"; +import { app } from "electron/main"; import { existsSync } from "node:fs"; import * as fs from "node:fs/promises"; import path from "path"; diff --git a/desktop/yarn.lock b/desktop/yarn.lock index 5d4016b91..753d50366 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -261,11 +261,6 @@ dependencies: "@types/node" "*" -"@types/get-folder-size@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/get-folder-size/-/get-folder-size-2.0.0.tgz#acbb5bf5999410c375b2739863a9d2f9483fabf6" - integrity sha512-6VKKrDB20E/6ovi2Pfpy9Pcz8Me1ue/tReaZrwrz9mfVdsr6WAMiDZ+F1oAAcss4U5n2k673i1leDIx2aEBDFQ== - "@types/http-cache-semantics@*": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz#abe102d06ccda1efdf0ed98c10ccf7f36a785a41" @@ -1535,24 +1530,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -gar@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/gar/-/gar-1.0.4.tgz#f777bc7db425c0572fdeb52676172ca1ae9888b8" - integrity sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w== - get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-folder-size@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/get-folder-size/-/get-folder-size-2.0.1.tgz#3fe0524dd3bad05257ef1311331417bcd020a497" - integrity sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA== - dependencies: - gar "^1.0.4" - tiny-each-async "2.0.3" - get-intrinsic@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" @@ -2888,11 +2870,6 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -tiny-each-async@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/tiny-each-async/-/tiny-each-async-2.0.3.tgz#8ebbbfd6d6295f1370003fbb37162afe5a0a51d1" - integrity sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA== - tmp-promise@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7"