diff --git a/desktop/docs/dependencies.md b/desktop/docs/dependencies.md index 502ea6ea9..4f2bd152d 100644 --- a/desktop/docs/dependencies.md +++ b/desktop/docs/dependencies.md @@ -42,13 +42,6 @@ This spins up a server for serving files using a protocol handler inside our Electron process. This allows us to directly use the output produced by `next build` for loading into our renderer process. -### electron-reload - -Reloads contents of the BrowserWindow (renderer process) when source files are -changed. - -* TODO (MR): Do we need this? Isn't the next-electron-server HMR covering this? - ## DX See [web/docs/dependencies#DX](../../web/docs/dependencies.md#dx) for the diff --git a/desktop/package.json b/desktop/package.json index 3dafdce33..0aa24aba3 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -25,7 +25,6 @@ "chokidar": "^3.5.3", "compare-versions": "^6.1.0", "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", @@ -33,17 +32,13 @@ "html-entities": "^2.4.0", "jpeg-js": "^0.4.4", "next-electron-server": "^1", - "node-fetch": "^2.6.7", "node-stream-zip": "^1.15.0", - "onnxruntime-node": "^1.16.3", - "promise-fs": "^2.1.1" + "onnxruntime-node": "^1.16.3" }, "devDependencies": { "@types/auto-launch": "^5.0.2", "@types/ffmpeg-static": "^3.0.1", "@types/get-folder-size": "^2.0.0", - "@types/node-fetch": "^2.6.2", - "@types/promise-fs": "^2.1.1", "@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 index bf7182fad..fe5f7bd93 100644 --- a/desktop/src/api/cache.ts +++ b/desktop/src/api/cache.ts @@ -1,6 +1,7 @@ import { ipcRenderer } from "electron/renderer"; +import { existsSync } from "node:fs"; +import * as fs from "node:fs/promises"; import path from "path"; -import { existsSync, mkdir, rmSync } from "promise-fs"; import { DiskCache } from "../services/diskCache"; const ENTE_CACHE_DIR_NAME = "ente"; @@ -21,16 +22,14 @@ export async function openDiskCache( cacheLimitInBytes?: number, ) { const cacheBucketDir = await getCacheBucketDir(cacheName); - if (!existsSync(cacheBucketDir)) { - await mkdir(cacheBucketDir, { recursive: true }); - } + 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)) { - rmSync(cacheBucketDir, { recursive: true, force: true }); + await fs.rm(cacheBucketDir, { recursive: true, force: true }); return true; } else { return false; diff --git a/desktop/src/api/export.ts b/desktop/src/api/export.ts deleted file mode 100644 index 8adaa236f..000000000 --- a/desktop/src/api/export.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as fs from "promise-fs"; -import { writeStream } from "./../services/fs"; - -export const exists = (path: string) => { - return fs.existsSync(path); -}; - -export const checkExistsAndCreateDir = async (dirPath: string) => { - if (!fs.existsSync(dirPath)) { - await fs.mkdir(dirPath); - } -}; - -export const saveStreamToDisk = async ( - filePath: string, - fileStream: ReadableStream, -) => { - await writeStream(filePath, fileStream); -}; - -export const saveFileToDisk = async (path: string, fileData: string) => { - await fs.writeFile(path, fileData); -}; diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 4de93046a..0a1fbe818 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -1,5 +1,4 @@ import { app, BrowserWindow } from "electron"; -import electronReload from "electron-reload"; import serveNextAt from "next-electron-server"; import { initWatcher } from "./services/chokidar"; import { isDev } from "./utils/common"; @@ -42,19 +41,6 @@ export const setIsUpdateAvailable = (value: boolean): void => { updateIsAvailable = value; }; -/** - * Hot reload the main process if anything changes in the source directory that - * we're running from. - * - * In particular, this gets triggered when the `tsc -w` rebuilds JS files in the - * `app/` directory when we change the TS files in the `src/` directory. - */ -const setupMainHotReload = () => { - if (isDev) { - electronReload(__dirname, {}); - } -}; - /** * The URL where the renderer HTML is being served from. */ @@ -77,7 +63,6 @@ const setupRendererServer = () => { serveNextAt(rendererURL); }; -setupMainHotReload(); setupRendererServer(); setupLogging(isDev); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 72ba5c61b..8c717e8ab 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -28,18 +28,12 @@ */ import { contextBridge, ipcRenderer } from "electron"; -import { existsSync } from "fs"; +import { createWriteStream, existsSync } from "node:fs"; +import * as fs from "node:fs/promises"; +import { Readable } from "node:stream"; import path from "path"; -import * as fs from "promise-fs"; -import { Readable } from "stream"; import { deleteDiskCache, openDiskCache } from "./api/cache"; import { logToDisk, openLogDirectory } from "./api/common"; -import { - checkExistsAndCreateDir, - exists, - saveFileToDisk, - saveStreamToDisk, -} from "./api/export"; import { runFFmpegCmd } from "./api/ffmpeg"; import { getDirFiles } from "./api/fs"; import { convertToJPEG, generateImageThumbnail } from "./api/imageProcessor"; @@ -67,8 +61,12 @@ import { } from "./api/watch"; import { setupLogging } from "./utils/logging"; -/* Some of the code below has been duplicated to make this file self contained. -Enhancement: consider alternatives */ +/* + Some of the code below has been duplicated to make this file self contained + (see the documentation at the top of why it needs to be a single file). + + Enhancement: consider alternatives +*/ /* preload: duplicated logError */ export function logError(error: Error, message: string, info?: string): void { @@ -77,10 +75,27 @@ export function logError(error: Error, message: string, info?: string): void { // - -export const convertBrowserStreamToNode = ( - fileStream: ReadableStream, -) => { - const reader = fileStream.getReader(); +/* preload: duplicated writeStream */ +/** + * Write a (web) ReadableStream to a file at the given {@link filePath}. + * + * The returned promise resolves when the write completes. + * + * @param filePath The local filesystem path where the file should be written. + * @param readableStream A [web + * ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) + */ +const writeStream = (filePath: string, readableStream: ReadableStream) => + writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream)); + +/** + * Convert a Web ReadableStream into a Node.js ReadableStream + * + * This can be used to, for example, write a ReadableStream obtained via + * `net.fetch` into a file using the Node.js `fs` APIs + */ +const convertWebReadableStreamToNode = (readableStream: ReadableStream) => { + const reader = readableStream.getReader(); const rs = new Readable(); rs._read = async () => { @@ -101,11 +116,11 @@ export const convertBrowserStreamToNode = ( return rs; }; -export async function writeNodeStream( +const writeNodeStream = async ( filePath: string, fileStream: NodeJS.ReadableStream, -) { - const writeable = fs.createWriteStream(filePath); +) => { + const writeable = createWriteStream(filePath); fileStream.on("error", (error) => { writeable.destroy(error); // Close the writable stream with an error @@ -115,23 +130,26 @@ export async function writeNodeStream( await new Promise((resolve, reject) => { writeable.on("finish", resolve); - writeable.on("error", async (e) => { + writeable.on("error", async (e: unknown) => { if (existsSync(filePath)) { await fs.unlink(filePath); } reject(e); }); }); -} +}; -/* preload: duplicated writeStream */ -export async function writeStream( - filePath: string, - fileStream: ReadableStream, -) { - const readable = convertBrowserStreamToNode(fileStream); - await writeNodeStream(filePath, readable); -} +// - Export + +const exists = (path: string) => existsSync(path); + +const checkExistsAndCreateDir = (dirPath: string) => + fs.mkdir(dirPath, { recursive: true }); + +const saveStreamToDisk = writeStream; + +const saveFileToDisk = (path: string, contents: string) => + fs.writeFile(path, contents); // - @@ -154,9 +172,7 @@ async function moveFile( } // check if destination folder exists const destinationFolder = path.dirname(destinationPath); - if (!existsSync(destinationFolder)) { - await fs.mkdir(destinationFolder, { recursive: true }); - } + await fs.mkdir(destinationFolder, { recursive: true }); await fs.rename(sourcePath, destinationPath); } @@ -183,7 +199,8 @@ async function deleteFolder(folderPath: string): Promise { if (!existsSync(folderPath)) { return; } - if (!fs.statSync(folderPath).isDirectory()) { + const stat = await fs.stat(folderPath); + if (!stat.isDirectory()) { throw new Error("Path is not a folder"); } // check if folder is empty @@ -201,17 +218,18 @@ async function rename(oldPath: string, newPath: string) { await fs.rename(oldPath, newPath); } -function deleteFile(filePath: string): void { +const deleteFile = async (filePath: string) => { if (!existsSync(filePath)) { return; } - if (!fs.statSync(filePath).isFile()) { + const stat = await fs.stat(filePath); + if (!stat.isFile()) { throw new Error("Path is not a file"); } - fs.rmSync(filePath); -} + return fs.rm(filePath); +}; -// - +// - ML /* preload: duplicated Model */ export enum Model { @@ -315,7 +333,7 @@ const parseExecError = (err: any) => { } }; -// - +// - General const selectDirectory = async (): Promise => { try { @@ -349,7 +367,7 @@ const clearElectronStore = () => { ipcRenderer.send("clear-electron-store"); }; -// - +// - App update const updateAndRestart = () => { ipcRenderer.send("update-and-restart"); @@ -410,10 +428,12 @@ setupLogging(); // running out of memory, causing the app to crash as it copies it over across // the processes. contextBridge.exposeInMainWorld("ElectronAPIs", { + // - Export exists, checkExistsAndCreateDir, saveStreamToDisk, saveFileToDisk, + selectDirectory, clearElectronStore, readTextFile, @@ -438,20 +458,29 @@ contextBridge.exposeInMainWorld("ElectronAPIs", { updateWatchMappingIgnoredFiles, logToDisk, convertToJPEG, - openLogDirectory, registerUpdateEventListener, - updateAndRestart, - skipAppUpdate, - getAppVersion, + runFFmpegCmd, - muteUpdateNotification, generateImageThumbnail, registerForegroundEventListener, - openDirectory, moveFile, deleteFolder, rename, deleteFile, + + // General + getAppVersion, + openDirectory, + + // Logging + openLogDirectory, + + // - App update + updateAndRestart, + skipAppUpdate, + muteUpdateNotification, + + // - ML computeImageEmbedding, computeTextEmbedding, }); diff --git a/desktop/src/services/appUpdater.ts b/desktop/src/services/appUpdater.ts index 2ddcef704..89f428a72 100644 --- a/desktop/src/services/appUpdater.ts +++ b/desktop/src/services/appUpdater.ts @@ -2,10 +2,8 @@ import { compareVersions } from "compare-versions"; import { app, BrowserWindow } from "electron"; import { default as ElectronLog, default as log } from "electron-log"; import { autoUpdater } from "electron-updater"; -import fetch from "node-fetch"; import { setIsAppQuitting, setIsUpdateAvailable } from "../main"; -import { AppUpdateInfo, GetFeatureFlagResponse } from "../types"; -import { isPlatform } from "../utils/common/platform"; +import { AppUpdateInfo } from "../types"; import { logErrorSentry } from "./sentry"; import { clearMuteUpdateNotificationVersion, @@ -64,56 +62,42 @@ async function checkForUpdateAndNotify(mainWindow: BrowserWindow) { ); return; } - const desktopCutoffVersion = await getDesktopCutoffVersion(); + + let timeout: NodeJS.Timeout; + log.debug("attempting auto update"); + autoUpdater.downloadUpdate(); + const muteUpdateNotificationVersion = + getMuteUpdateNotificationVersion(); if ( - desktopCutoffVersion && - isPlatform("mac") && - compareVersions( - updateCheckResult.updateInfo.version, - desktopCutoffVersion, - ) > 0 + muteUpdateNotificationVersion && + updateCheckResult.updateInfo.version === + muteUpdateNotificationVersion ) { - log.debug("auto update not possible due to key change"); + log.info( + "user chose to mute update notification for version ", + updateCheckResult.updateInfo.version, + ); + return; + } + 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, }); - } else { - let timeout: NodeJS.Timeout; - log.debug("attempting auto update"); - autoUpdater.downloadUpdate(); - const muteUpdateNotificationVersion = - getMuteUpdateNotificationVersion(); - if ( - muteUpdateNotificationVersion && - updateCheckResult.updateInfo.version === - muteUpdateNotificationVersion - ) { - log.info( - "user chose to mute update notification for version ", - updateCheckResult.updateInfo.version, - ); - return; - } - 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"); @@ -138,18 +122,6 @@ export function muteUpdateNotification(version: string) { setMuteUpdateNotificationVersion(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, diff --git a/desktop/src/services/clipService.ts b/desktop/src/services/clipService.ts index 4a808d7a4..58f0d376b 100644 --- a/desktop/src/services/clipService.ts +++ b/desktop/src/services/clipService.ts @@ -1,17 +1,15 @@ -import { app } from "electron"; import * as log from "electron-log"; +import { app, net } from "electron/main"; import { existsSync } from "fs"; -import fs from "fs/promises"; -import fetch from "node-fetch"; -import path from "path"; -import { readFile } from "promise-fs"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; import util from "util"; import { CustomErrors } from "../constants/errors"; import { Model } from "../types"; import Tokenizer from "../utils/clip-bpe-ts/mod"; import { isDev } from "../utils/common"; import { getPlatform } from "../utils/common/platform"; -import { writeNodeStream } from "./fs"; +import { writeStream } from "./fs"; import { logErrorSentry } from "./sentry"; const shellescape = require("any-shell-escape"); const execAsync = util.promisify(require("child_process").exec); @@ -80,13 +78,11 @@ function getModelSavePath(modelName: string) { async function downloadModel(saveLocation: string, url: string) { // confirm that the save location exists const saveDir = path.dirname(saveLocation); - if (!existsSync(saveDir)) { - log.info("creating model save dir"); - await fs.mkdir(saveDir, { recursive: true }); - } + await fs.mkdir(saveDir, { recursive: true }); log.info("downloading clip model"); - const resp = await fetch(url); - await writeNodeStream(saveLocation, resp.body); + const res = await net.fetch(url); + if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`); + await writeStream(saveLocation, res.body); log.info("clip model downloaded"); } diff --git a/desktop/src/services/diskCache.ts b/desktop/src/services/diskCache.ts index 1f4cd5293..947e45aa8 100644 --- a/desktop/src/services/diskCache.ts +++ b/desktop/src/services/diskCache.ts @@ -1,5 +1,6 @@ -import path from "path"; -import { existsSync, stat, unlink } from "promise-fs"; +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"; @@ -28,19 +29,19 @@ export class DiskCache implements LimitedCache { ): Promise { const cachePath = path.join(this.cacheBucketDir, cacheKey); if (existsSync(cachePath)) { - const fileStats = await stat(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.", ); - unlink(cachePath).catch((e) => { + fs.unlink(cachePath).catch((e) => { if (e.code === "ENOENT") return; logError(e, "Failed to delete cache key"); }); return undefined; } - DiskLRUService.touch(cachePath); + DiskLRUService.markUse(cachePath); return new Response(await getFileStream(cachePath)); } else { return undefined; @@ -49,7 +50,7 @@ export class DiskCache implements LimitedCache { async delete(cacheKey: string): Promise { const cachePath = path.join(this.cacheBucketDir, cacheKey); if (existsSync(cachePath)) { - await unlink(cachePath); + await fs.unlink(cachePath); return true; } else { return false; diff --git a/desktop/src/services/diskLRU.ts b/desktop/src/services/diskLRU.ts index 44b05c099..d42e1cabc 100644 --- a/desktop/src/services/diskLRU.ts +++ b/desktop/src/services/diskLRU.ts @@ -1,6 +1,6 @@ import getFolderSize from "get-folder-size"; -import path from "path"; -import { close, open, readdir, stat, unlink, utimes } from "promise-fs"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; import { logError } from "../services/logging"; export interface LeastRecentlyUsedResult { @@ -12,19 +12,10 @@ class DiskLRUService { private isRunning: Promise = null; private reRun: boolean = false; - async touch(path: string) { - try { - const time = new Date(); - await utimes(path, time, time); - } catch (err) { - logError(err, "utimes method touch failed"); - try { - await close(await open(path, "w")); - } catch (e) { - logError(e, "open-close method touch failed"); - } - // log and ignore - } + /** 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) { @@ -53,7 +44,7 @@ class DiskLRUService { const leastRecentlyUsed = await this.findLeastRecentlyUsed(cacheDir); try { - await unlink(leastRecentlyUsed.path); + await fs.unlink(leastRecentlyUsed.path); } catch (e) { // ENOENT: File not found // which can be ignored as we are trying to delete the file anyway @@ -81,16 +72,15 @@ class DiskLRUService { ): Promise { result = result || { atime: new Date(), path: "" }; - const files = await readdir(dir); + const files = await fs.readdir(dir); for (const file of files) { const newBase = path.join(dir, file); - const stats = await stat(newBase); - if (stats.isDirectory()) { + const st = await fs.stat(newBase); + if (st.isDirectory()) { result = await this.findLeastRecentlyUsed(newBase, result); } else { - const { atime } = await stat(newBase); - - if (atime.getTime() < result.atime.getTime()) { + const { atime } = st; + if (st.atime.getTime() < result.atime.getTime()) { result = { atime, path: newBase, diff --git a/desktop/src/services/ffmpeg.ts b/desktop/src/services/ffmpeg.ts index 0a1927232..227bd310e 100644 --- a/desktop/src/services/ffmpeg.ts +++ b/desktop/src/services/ffmpeg.ts @@ -1,17 +1,15 @@ import log from "electron-log"; import pathToFfmpeg from "ffmpeg-static"; -import { existsSync } from "fs"; -import { readFile, rmSync, writeFile } from "promise-fs"; +import { existsSync } from "node:fs"; +import * as fs from "node:fs/promises"; import util from "util"; -import { promiseWithTimeout } from "../utils/common"; +import { CustomErrors } from "../constants/errors"; import { generateTempFilePath, getTempDirPath } from "../utils/temp"; import { logErrorSentry } from "./sentry"; const shellescape = require("any-shell-escape"); const execAsync = util.promisify(require("child_process").exec); -const FFMPEG_EXECUTION_WAIT_TIME = 30 * 1000; - const INPUT_PATH_PLACEHOLDER = "INPUT"; const FFMPEG_PLACEHOLDER = "FFMPEG"; const OUTPUT_PATH_PLACEHOLDER = "OUTPUT"; @@ -70,10 +68,7 @@ export async function runFFmpegCmd( if (dontTimeout) { await execAsync(escapedCmd); } else { - await promiseWithTimeout( - execAsync(escapedCmd), - FFMPEG_EXECUTION_WAIT_TIME, - ); + await promiseWithTimeout(execAsync(escapedCmd), 30 * 1000); } if (!existsSync(tempOutputFilePath)) { throw new Error("ffmpeg output file not found"); @@ -85,14 +80,14 @@ export async function runFFmpegCmd( "ms", ); - const outputFile = await readFile(tempOutputFilePath); + const outputFile = await fs.readFile(tempOutputFilePath); return new Uint8Array(outputFile); } catch (e) { logErrorSentry(e, "ffmpeg run command error"); throw e; } finally { try { - rmSync(tempOutputFilePath, { force: true }); + await fs.rm(tempOutputFilePath, { force: true }); } catch (e) { logErrorSentry(e, "failed to remove tempOutputFile"); } @@ -114,7 +109,7 @@ const ffmpegBinaryPath = () => { export async function writeTempFile(fileStream: Uint8Array, fileName: string) { const tempFilePath = await generateTempFilePath(fileName); - await writeFile(tempFilePath, fileStream); + await fs.writeFile(tempFilePath, fileStream); return tempFilePath; } @@ -126,5 +121,29 @@ export async function deleteTempFile(tempFilePath: string) { "tried to delete a non temp file", ); } - rmSync(tempFilePath, { force: true }); + await fs.rm(tempFilePath, { force: true }); } + +export const promiseWithTimeout = async ( + request: Promise, + timeout: number, +): Promise => { + const timeoutRef: { + current: NodeJS.Timeout; + } = { current: null }; + const rejectOnTimeout = new Promise((_, reject) => { + timeoutRef.current = setTimeout( + () => reject(Error(CustomErrors.WAIT_TIME_EXCEEDED)), + timeout, + ); + }); + const requestWithTimeOutCancellation = async () => { + const resp = await request; + clearTimeout(timeoutRef.current); + return resp; + }; + return await Promise.race([ + requestWithTimeOutCancellation(), + rejectOnTimeout, + ]); +}; diff --git a/desktop/src/services/fs.ts b/desktop/src/services/fs.ts index bcc49ae5c..c74790d01 100644 --- a/desktop/src/services/fs.ts +++ b/desktop/src/services/fs.ts @@ -1,7 +1,7 @@ -import { existsSync } from "fs"; import StreamZip from "node-stream-zip"; -import path from "path"; -import * as fs from "promise-fs"; +import { createWriteStream, existsSync } from "node:fs"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; import { Readable } from "stream"; import { ElectronFile } from "../types"; import { logError } from "./logging"; @@ -212,7 +212,7 @@ export async function writeNodeStream( filePath: string, fileStream: NodeJS.ReadableStream, ) { - const writeable = fs.createWriteStream(filePath); + const writeable = createWriteStream(filePath); fileStream.on("error", (error) => { writeable.destroy(error); // Close the writable stream with an error @@ -222,7 +222,7 @@ export async function writeNodeStream( await new Promise((resolve, reject) => { writeable.on("finish", resolve); - writeable.on("error", async (e) => { + writeable.on("error", async (e: unknown) => { if (existsSync(filePath)) { await fs.unlink(filePath); } diff --git a/desktop/src/services/imageProcessor.ts b/desktop/src/services/imageProcessor.ts index baa99a4c7..d3a74ebb6 100644 --- a/desktop/src/services/imageProcessor.ts +++ b/desktop/src/services/imageProcessor.ts @@ -2,9 +2,8 @@ import { exec } from "child_process"; import util from "util"; import log from "electron-log"; -import { existsSync, rmSync } from "fs"; +import * as fs from "node:fs/promises"; import path from "path"; -import { readFile, writeFile } from "promise-fs"; import { CustomErrors } from "../constants/errors"; import { isDev } from "../utils/common"; import { isPlatform } from "../utils/common/platform"; @@ -88,28 +87,22 @@ export async function convertToJPEG( tempInputFilePath = await generateTempFilePath(filename); tempOutputFilePath = await generateTempFilePath("output.jpeg"); - await writeFile(tempInputFilePath, fileData); + await fs.writeFile(tempInputFilePath, fileData); 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; + return new Uint8Array(await fs.readFile(tempOutputFilePath)); } catch (e) { logErrorSentry(e, "failed to convert heic"); throw e; } finally { try { - rmSync(tempInputFilePath, { force: true }); + await fs.rm(tempInputFilePath, { force: true }); } catch (e) { logErrorSentry(e, "failed to remove tempInputFile"); } try { - rmSync(tempOutputFilePath, { force: true }); + await fs.rm(tempOutputFilePath, { force: true }); } catch (e) { logErrorSentry(e, "failed to remove tempOutputFile"); } @@ -183,10 +176,7 @@ export async function generateImageThumbnail( quality, ); - if (!existsSync(tempOutputFilePath)) { - throw new Error("output thumbnail file not found"); - } - thumbnail = new Uint8Array(await readFile(tempOutputFilePath)); + thumbnail = new Uint8Array(await fs.readFile(tempOutputFilePath)); quality -= 10; } while (thumbnail.length > maxSize && quality > MIN_QUALITY); return thumbnail; @@ -195,7 +185,7 @@ export async function generateImageThumbnail( throw e; } finally { try { - rmSync(tempOutputFilePath, { force: true }); + await fs.rm(tempOutputFilePath, { force: true }); } catch (e) { logErrorSentry(e, "failed to remove tempOutputFile"); } diff --git a/desktop/src/types/index.ts b/desktop/src/types/index.ts index d2cd8ef64..9473ee6dd 100644 --- a/desktop/src/types/index.ts +++ b/desktop/src/types/index.ts @@ -77,10 +77,6 @@ export interface AppUpdateInfo { version: string; } -export interface GetFeatureFlagResponse { - desktopCutoffVersion?: string; -} - export enum Model { GGML_CLIP = "ggml-clip", ONNX_CLIP = "onnx-clip", diff --git a/desktop/src/utils/common/index.ts b/desktop/src/utils/common/index.ts index e970dfec4..592e7c373 100644 --- a/desktop/src/utils/common/index.ts +++ b/desktop/src/utils/common/index.ts @@ -1,27 +1,2 @@ import { app } from "electron"; -import { CustomErrors } from "../../constants/errors"; export const isDev = !app.isPackaged; - -export const promiseWithTimeout = async ( - request: Promise, - timeout: number, -): Promise => { - const timeoutRef: { - current: NodeJS.Timeout; - } = { current: null }; - const rejectOnTimeout = new Promise((_, reject) => { - timeoutRef.current = setTimeout( - () => reject(Error(CustomErrors.WAIT_TIME_EXCEEDED)), - timeout, - ); - }); - const requestWithTimeOutCancellation = async () => { - const resp = await request; - clearTimeout(timeoutRef.current); - return resp; - }; - return await Promise.race([ - requestWithTimeOutCancellation(), - rejectOnTimeout, - ]); -}; diff --git a/desktop/src/utils/main.ts b/desktop/src/utils/main.ts index 569752326..9041a6d15 100644 --- a/desktop/src/utils/main.ts +++ b/desktop/src/utils/main.ts @@ -1,8 +1,8 @@ import { app, BrowserWindow, Menu, nativeImage, Tray } from "electron"; import ElectronLog from "electron-log"; +import { existsSync } from "node:fs"; import os from "os"; import path from "path"; -import { existsSync } from "promise-fs"; import util from "util"; import { rendererURL } from "../main"; import { setupAutoUpdater } from "../services/appUpdater"; diff --git a/desktop/src/utils/temp.ts b/desktop/src/utils/temp.ts index 91496ce13..7bb20468a 100644 --- a/desktop/src/utils/temp.ts +++ b/desktop/src/utils/temp.ts @@ -1,17 +1,14 @@ import { app } from "electron"; +import { existsSync } from "node:fs"; +import * as fs from "node:fs/promises"; 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); - } + const tempDirPath = path.join(app.getPath("temp"), "ente"); + await fs.mkdir(tempDirPath, { recursive: true }); return tempDirPath; } diff --git a/desktop/yarn.lock b/desktop/yarn.lock index f23ac47cc..5d4016b91 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -205,11 +205,6 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@octetstream/promisify@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@octetstream/promisify/-/promisify-2.0.2.tgz#29ac3bd7aefba646db670227f895d812c1a19615" - integrity sha512-7XHoRB61hxsz8lBQrjC1tq/3OEIgpvGWg6DKAdwi7WRzruwkmsdwmOoUXbU4Dtd4RSOMDwed0SkP3y8UlMt1Bg== - "@pkgr/core@^0.1.0": version "0.1.1" resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" @@ -293,14 +288,6 @@ 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" @@ -329,13 +316,6 @@ "@types/node" "*" xmlbuilder ">=11.0.1" -"@types/promise-fs@^2.1.1": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@types/promise-fs/-/promise-fs-2.1.2.tgz#7ef6ab00c7fbc68081e34e560d2f008d3dd27fd2" - integrity sha512-s3YON1LmplAUVrvTT2d1I0m2Rk0hSgc/1l5/krnU96YpP4NG9VEN/qopaFv8yk5a2Z+AgYzafS1LCP+kQH0MYw== - dependencies: - "@types/node" "*" - "@types/responselike@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" @@ -809,7 +789,7 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chokidar@^3.5.2, chokidar@^3.5.3: +chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -1182,13 +1162,6 @@ electron-publish@24.5.0: lazy-val "^1.0.5" mime "^2.5.2" -electron-reload@^2.0.0-alpha.1: - version "2.0.0-alpha.1" - resolved "https://registry.yarnpkg.com/electron-reload/-/electron-reload-2.0.0-alpha.1.tgz#6cad98df96695ca1d5462dc9407f7c620028ce99" - integrity sha512-hTde7gv0TEqxbxlB3pj2CwoyCQ9sdiQrcP8GkpzhosxyVeYM3mZbMEVKCZK3L0fED7Mz5A9IWmK7zEvi4H3P1g== - dependencies: - chokidar "^3.5.2" - electron-store@^8.0.1: version "8.0.2" resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-8.0.2.tgz#95c8cf81c1e1cf48b24f3ceeea24b921c1ff62d7" @@ -1503,15 +1476,6 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2" integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ== -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" @@ -2246,13 +2210,6 @@ node-addon-api@^1.6.3: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== -node-fetch@^2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - node-stream-zip@^1.15.0: version "1.15.0" resolved "https://registry.yarnpkg.com/node-stream-zip/-/node-stream-zip-1.15.0.tgz#158adb88ed8004c6c49a396b50a6a5de3bca33ea" @@ -2485,13 +2442,6 @@ progress@^2.0.3: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -promise-fs@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/promise-fs/-/promise-fs-2.1.1.tgz#0b725a592c165ff16157d1f13640ba390637e557" - integrity sha512-43p7e4QzAQ3w6eyN0+gbBL7jXiZFWLWYITg9wIObqkBySu/a5K1EDcQ/S6UyB/bmiZWDA4NjTbcopKLTaKcGSw== - dependencies: - "@octetstream/promisify" "2.0.2" - promise-retry@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" @@ -2964,11 +2914,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" @@ -3092,19 +3037,6 @@ verror@^1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index 7190b5b93..df17a6e79 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -1150,7 +1150,7 @@ class ExportService { videoStream, ); } catch (e) { - ElectronAPIs.deleteFile( + await ElectronAPIs.deleteFile( getFileExportPath(collectionExportPath, imageExportName), ); throw e; diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 576ccd336..8081d86e9 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -51,10 +51,7 @@ import { import { FileTypeInfo } from "types/upload"; import { isPlaybackPossible } from "utils/photoFrame"; -import { - default as ElectronAPIs, - default as ElectronFSService, -} from "@ente/shared/electron"; +import { default as ElectronAPIs } from "@ente/shared/electron"; import { downloadUsingAnchor } from "@ente/shared/utils"; import { t } from "i18next"; import imageProcessor from "services/imageProcessor"; @@ -801,7 +798,7 @@ export async function downloadFileDesktop( videoStream, ); } catch (e) { - ElectronFSService.deleteFile( + await ElectronAPIs.deleteFile( getFileExportPath(downloadPath, imageExportName), ); throw e; diff --git a/web/packages/shared/electron/types.ts b/web/packages/shared/electron/types.ts index 43d55faac..304ada6c4 100644 --- a/web/packages/shared/electron/types.ts +++ b/web/packages/shared/electron/types.ts @@ -96,7 +96,7 @@ export interface ElectronAPIsType { openDirectory: (dirPath: string) => Promise; moveFile: (oldPath: string, newPath: string) => Promise; deleteFolder: (path: string) => Promise; - deleteFile: (path: string) => void; + deleteFile: (path: string) => Promise; rename: (oldPath: string, newPath: string) => Promise; computeImageEmbedding: ( model: Model,