[photos-desktop] Various changes, bringing the code up to speed (#1227)

See commit titles for a gist.
This commit is contained in:
Manav Rathi 2024-03-27 21:22:54 +05:30 committed by GitHub
commit 3213fe0d26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 513 additions and 485 deletions

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -12,7 +12,6 @@ linux:
arch: [x64, arm64]
- target: pacman
arch: [x64, arm64]
icon: ./resources/icon.icns
category: Photography
mac:
target:

View file

@ -8,27 +8,26 @@
*
* https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process
*/
import log from "electron-log";
import { app, BrowserWindow } from "electron/main";
import { app, BrowserWindow, Menu } from "electron/main";
import serveNextAt from "next-electron-server";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { isDev } from "./main/general";
import {
addAllowOriginHeader,
createWindow,
handleDockIconHideOnAutoLaunch,
handleDownloads,
handleExternalLinks,
handleUpdates,
logSystemInfo,
logStartupBanner,
setupMacWindowOnDockIconClick,
setupMainMenu,
setupTrayItem,
} from "./main/init";
import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc";
import { logErrorSentry, setupLogging } from "./main/log";
import log, { initLogging } from "./main/log";
import { createApplicationMenu } from "./main/menu";
import { isDev } from "./main/util";
import { setupAutoUpdater } from "./services/appUpdater";
import { initWatcher } from "./services/chokidar";
let appIsQuitting = false;
@ -135,8 +134,6 @@ function setupAppEventEmitter(mainWindow: BrowserWindow) {
}
const main = () => {
setupLogging(isDev);
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
@ -145,6 +142,7 @@ const main = () => {
let mainWindow: BrowserWindow;
initLogging();
setupRendererServer();
handleDockIconHideOnAutoLaunch();
increaseDiskCache();
@ -161,19 +159,19 @@ const main = () => {
}
});
// 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.
// Emitted once, when Electron has finished initializing.
//
// Note that some Electron APIs can only be used after this event occurs.
app.on("ready", async () => {
logSystemInfo();
logStartupBanner();
mainWindow = await createWindow();
const watcher = initWatcher(mainWindow);
setupTrayItem(mainWindow);
setupMacWindowOnDockIconClick();
setupMainMenu(mainWindow);
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
attachIPCHandlers();
attachFSWatchIPCHandlers(watcher);
await handleUpdates(mainWindow);
if (!isDev) setupAutoUpdater(mainWindow);
handleDownloads(mainWindow);
handleExternalLinks(mainWindow);
addAllowOriginHeader(mainWindow);
@ -184,7 +182,7 @@ const main = () => {
} catch (e) {
// Log but otherwise ignore errors during non-critical startup
// actions
logErrorSentry(e, "Ignoring startup error");
log.error("Ignoring startup error", e);
}
});

View file

@ -5,7 +5,6 @@ import { createWriteStream, existsSync } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { Readable } from "node:stream";
import { logError } from "./log";
export const fsExists = (path: string) => existsSync(path);
@ -99,54 +98,36 @@ export const moveFile = async (sourcePath: string, destinationPath: string) => {
};
export const isFolder = async (dirPath: string) => {
try {
const stats = await fs.stat(dirPath);
return stats.isDirectory();
} catch (e) {
let err = e;
// if code is defined, it's an error from fs.stat
if (typeof e.code !== "undefined") {
// ENOENT means the file does not exist
if (e.code === "ENOENT") {
return false;
}
err = Error(`fs error code: ${e.code}`);
}
logError(err, "isFolder failed");
return false;
}
if (!existsSync(dirPath)) return false;
const stats = await fs.stat(dirPath);
return stats.isDirectory();
};
export const deleteFolder = async (folderPath: string) => {
if (!existsSync(folderPath)) {
return;
}
const stat = await fs.stat(folderPath);
if (!stat.isDirectory()) {
throw new Error("Path is not a folder");
}
// check if folder is empty
// Ensure it is folder
if (!isFolder(folderPath)) return;
// Ensure folder is empty
const files = await fs.readdir(folderPath);
if (files.length > 0) {
throw new Error("Folder is not empty");
}
if (files.length > 0) throw new Error("Folder is not empty");
// rm -rf it
await fs.rmdir(folderPath);
};
export const rename = async (oldPath: string, newPath: string) => {
if (!existsSync(oldPath)) {
throw new Error("Path does not exist");
}
if (!existsSync(oldPath)) throw new Error("Path does not exist");
await fs.rename(oldPath, newPath);
};
export const deleteFile = async (filePath: string) => {
if (!existsSync(filePath)) {
return;
}
// Ensure it exists
if (!existsSync(filePath)) return;
// And is a file
const stat = await fs.stat(filePath);
if (!stat.isFile()) {
throw new Error("Path is not a file");
}
if (!stat.isFile()) throw new Error("Path is not a file");
// rm it
return fs.rm(filePath);
};

View file

@ -1,42 +0,0 @@
import { shell } from "electron"; /* TODO(MR): Why is this not in /main? */
import { app } from "electron/main";
import path from "node:path";
/** `true` if the app is running in development mode. */
export const isDev = !app.isPackaged;
/**
* Open the given {@link dirPath} in the system's folder viewer.
*
* For example, on macOS this'll open {@link dirPath} in Finder.
*/
export const openDirectory = async (dirPath: string) => {
const res = await shell.openPath(path.normalize(dirPath));
// shell.openPath resolves with a string containing the error message
// corresponding to the failure if a failure occurred, otherwise "".
if (res) throw new Error(`Failed to open directory ${dirPath}: res`);
};
/**
* Return the path where the logs for the app are saved.
*
* [Note: Electron app paths]
*
* By default, these paths are at the following locations:
*
* - macOS: `~/Library/Application Support/ente`
* - Linux: `~/.config/ente`
* - Windows: `%APPDATA%`, e.g. `C:\Users\<username>\AppData\Local\ente`
* - Windows: C:\Users\<you>\AppData\Local\<Your App Name>
*
* https://www.electronjs.org/docs/latest/api/app
*
*/
const logDirectoryPath = () => app.getPath("logs");
/**
* Open the app's log directory in the system's folder viewer.
*
* @see {@link openDirectory}
*/
export const openLogDirectory = () => openDirectory(logDirectoryPath());

View file

@ -1,18 +1,14 @@
import { app, BrowserWindow, Menu, nativeImage, Tray } from "electron";
import ElectronLog from "electron-log";
import { app, BrowserWindow, nativeImage, Tray } from "electron";
import { existsSync } from "node:fs";
import os from "os";
import path from "path";
import util from "util";
import os from "node:os";
import path from "node:path";
import { isAppQuitting, rendererURL } from "../main";
import { setupAutoUpdater } from "../services/appUpdater";
import autoLauncher from "../services/autoLauncher";
import { getHideDockIconPreference } from "../services/userPreference";
import { isPlatform } from "../utils/common/platform";
import { buildContextMenu, buildMenuBar } from "../utils/menu";
import { isDev } from "./general";
import { logErrorSentry } from "./log";
const execAsync = util.promisify(require("child_process").exec);
import log from "./log";
import { createTrayContextMenu } from "./menu";
import { isDev } from "./util";
/**
* Create an return the {@link BrowserWindow} that will form our app's UI.
@ -20,21 +16,15 @@ const execAsync = util.promisify(require("child_process").exec);
* This window will show the HTML served from {@link rendererURL}.
*/
export const createWindow = async () => {
const appImgPath = isDev
? "../build/window-icon.png"
: path.join(process.resourcesPath, "window-icon.png");
const appIcon = nativeImage.createFromPath(appImgPath);
// Create the main window. This'll show our web content.
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(app.getAppPath(), "preload.js"),
},
icon: appIcon,
// The color to show in the window until the web content gets loaded.
// See: https://www.electronjs.org/docs/latest/api/browser-window#setting-the-backgroundcolor-property
backgroundColor: "black",
// We'll show conditionally depending on `wasAutoLaunched` later
// We'll show it conditionally depending on `wasAutoLaunched` later.
show: false,
});
@ -53,19 +43,14 @@ export const createWindow = async () => {
// Open the DevTools automatically when running in dev mode
if (isDev) mainWindow.webContents.openDevTools();
mainWindow.webContents.on("render-process-gone", (event, details) => {
mainWindow.webContents.on("render-process-gone", (_, details) => {
log.error(`render-process-gone: ${details}`);
mainWindow.webContents.reload();
logErrorSentry(
Error("render-process-gone"),
"webContents event render-process-gone",
{ details },
);
ElectronLog.log("webContents event render-process-gone", details);
});
mainWindow.webContents.on("unresponsive", () => {
log.error("webContents unresponsive");
mainWindow.webContents.forcefullyCrashRenderer();
ElectronLog.log("webContents event unresponsive");
});
mainWindow.on("close", function (event) {
@ -92,12 +77,7 @@ export const createWindow = async () => {
return mainWindow;
};
export async function handleUpdates(mainWindow: BrowserWindow) {
const isInstalledViaBrew = await checkIfInstalledViaBrew();
if (!isDev && !isInstalledViaBrew) {
setupAutoUpdater(mainWindow);
}
}
export async function handleUpdates(mainWindow: BrowserWindow) {}
export const setupTrayItem = (mainWindow: BrowserWindow) => {
const iconName = isPlatform("mac")
@ -110,7 +90,7 @@ export const setupTrayItem = (mainWindow: BrowserWindow) => {
const trayIcon = nativeImage.createFromPath(trayImgPath);
const tray = new Tray(trayIcon);
tray.setToolTip("ente");
tray.setContextMenu(buildContextMenu(mainWindow));
tray.setContextMenu(createTrayContextMenu(mainWindow));
};
export function handleDownloads(mainWindow: BrowserWindow) {
@ -160,10 +140,6 @@ export function setupMacWindowOnDockIconClick() {
});
}
export async function setupMainMenu(mainWindow: BrowserWindow) {
Menu.setApplicationMenu(await buildMenuBar(mainWindow));
}
export async function handleDockIconHideOnAutoLaunch() {
const shouldHideDockIcon = getHideDockIconPreference();
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
@ -173,27 +149,14 @@ export async function handleDockIconHideOnAutoLaunch() {
}
}
export function logSystemInfo() {
const systemVersion = process.getSystemVersion();
const osName = process.platform;
const osRelease = os.release();
ElectronLog.info({ osName, osRelease, systemVersion });
const appVersion = app.getVersion();
ElectronLog.info({ appVersion });
}
export function logStartupBanner() {
const version = isDev ? "dev" : app.getVersion();
log.info(`Hello from ente-photos-desktop ${version}`);
export async function checkIfInstalledViaBrew() {
if (!isPlatform("mac")) {
return false;
}
try {
await execAsync("brew list --cask ente");
ElectronLog.info("ente installed via brew");
return true;
} catch (e) {
ElectronLog.info("ente not installed via brew");
return false;
}
const platform = process.platform;
const osRelease = os.release();
const systemVersion = process.getSystemVersion();
log.info("Running on", { platform, osRelease, systemVersion });
}
function lowerCaseHeaders(responseHeaders: Record<string, string[]>) {

View file

@ -65,8 +65,8 @@ import {
saveFileToDisk,
saveStreamToDisk,
} from "./fs";
import { openDirectory, openLogDirectory } from "./general";
import { logToDisk } from "./log";
import { openDirectory, openLogDirectory } from "./util";
/**
* Listen for IPC events sent/invoked by the renderer process, and route them to

View file

@ -1,18 +1,34 @@
import log from "electron-log";
import { isDev } from "./general";
import util from "node:util";
import { isDev } from "./util";
export function setupLogging(isDev?: boolean) {
/**
* Initialize logging in the main process.
*
* This will set our underlying logger up to log to a file named `ente.log`,
*
* - on Linux at ~/.config/ente/logs/main.log
* - on macOS at ~/Library/Logs/ente/main.log
* - on Windows at %USERPROFILE%\AppData\Roaming\ente\logs\main.log
*
* On dev builds, it will also log to the console.
*/
export const initLogging = () => {
log.transports.file.fileName = "ente.log";
log.transports.file.maxSize = 50 * 1024 * 1024; // 50MB;
if (!isDev) {
log.transports.console.level = false;
}
log.transports.file.format =
"[{y}-{m}-{d}T{h}:{i}:{s}{z}] [{level}]{scope} {text}";
}
log.transports.file.format = "[{y}-{m}-{d}T{h}:{i}:{s}{z}] {text}";
log.transports.console.level = false;
};
/**
* Write a {@link message} to the on-disk log.
*
* This is used by the renderer process (via the contextBridge) to add entries
* in the log that is saved on disk.
*/
export const logToDisk = (message: string) => {
log.info(message);
log.info(`[rndr] ${message}`);
};
export const logError = logErrorSentry;
@ -32,3 +48,84 @@ export function logErrorSentry(
console.log(error, { msg, info });
}
}
const logError1 = (message: string, e?: unknown) => {
if (!e) {
logError_(message);
return;
}
let es: string;
if (e instanceof Error) {
// In practice, we expect ourselves to be called with Error objects, so
// this is the happy path so to say.
es = `${e.name}: ${e.message}\n${e.stack}`;
} else {
// For the rest rare cases, use the default string serialization of e.
es = String(e);
}
logError_(`${message}: ${es}`);
};
const logError_ = (message: string) => {
log.error(`[main] [error] ${message}`);
if (isDev) console.error(`[error] ${message}`);
};
const logInfo = (...params: any[]) => {
const message = params
.map((p) => (typeof p == "string" ? p : util.inspect(p)))
.join(" ");
log.info(`[main] ${message}`);
if (isDev) console.log(message);
};
const logDebug = (param: () => any) => {
if (isDev) console.log(`[debug] ${util.inspect(param())}`);
};
/**
* Ente's logger.
*
* This is an object that provides three functions to log at the corresponding
* levels - error, info or debug.
*
* {@link initLogging} needs to be called once before using any of these.
*/
export default {
/**
* Log an error message with an optional associated error object.
*
* {@link e} is generally expected to be an `instanceof Error` but it can be
* any arbitrary object that we obtain, say, when in a try-catch handler.
*
* The log is written to disk. In development builds, the log is also
* printed to the (Node.js process') console.
*/
error: logError1,
/**
* Log a message.
*
* This is meant as a replacement of {@link console.log}, and takes an
* arbitrary number of arbitrary parameters that it then serializes.
*
* The log is written to disk. In development builds, the log is also
* printed to the (Node.js process') console.
*/
info: logInfo,
/**
* Log a debug message.
*
* To avoid running unnecessary code in release builds, this takes a
* function to call to get the log message instead of directly taking the
* message. The provided function will only be called in development builds.
*
* The function can return an arbitrary value which is serialied before
* being logged.
*
* This log is not written to disk. It is printed to the (Node.js process')
* console only on development builds.
*/
debug: logDebug,
};

214
desktop/src/main/menu.ts Normal file
View file

@ -0,0 +1,214 @@
import {
app,
BrowserWindow,
Menu,
MenuItemConstructorOptions,
shell,
} from "electron";
import { setIsAppQuitting } from "../main";
import { forceCheckForUpdateAndNotify } from "../services/appUpdater";
import autoLauncher from "../services/autoLauncher";
import {
getHideDockIconPreference,
setHideDockIconPreference,
} from "../services/userPreference";
import { openLogDirectory } from "./util";
/** Create and return the entries in the app's main menu bar */
export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
// The state of checkboxes
//
// Whenever the menu is redrawn the current value of these variables is used
// to set the checked state for the various settings checkboxes.
let isAutoLaunchEnabled = await autoLauncher.isEnabled();
let shouldHideDockIcon = getHideDockIconPreference();
const macOSOnly = (options: MenuItemConstructorOptions[]) =>
process.platform == "darwin" ? options : [];
const handleCheckForUpdates = () =>
forceCheckForUpdateAndNotify(mainWindow);
const handleViewChangelog = () =>
shell.openExternal(
"https://github.com/ente-io/ente/blob/main/desktop/CHANGELOG.md",
);
const toggleAutoLaunch = () => {
autoLauncher.toggleAutoLaunch();
isAutoLaunchEnabled = !isAutoLaunchEnabled;
};
const toggleHideDockIcon = () => {
setHideDockIconPreference(!shouldHideDockIcon);
shouldHideDockIcon = !shouldHideDockIcon;
};
const handleHelp = () => shell.openExternal("https://help.ente.io/photos/");
const handleSupport = () => shell.openExternal("mailto:support@ente.io");
const handleBlog = () => shell.openExternal("https://ente.io/blog/");
const handleViewLogs = openLogDirectory;
return Menu.buildFromTemplate([
{
label: "ente",
submenu: [
...macOSOnly([
{
label: "About Ente",
role: "about",
},
]),
{ type: "separator" },
{
label: "Check for Updates...",
click: handleCheckForUpdates,
},
{
label: "View Changelog",
click: handleViewChangelog,
},
{ type: "separator" },
{
label: "Preferences",
submenu: [
{
label: "Open Ente on Startup",
type: "checkbox",
checked: isAutoLaunchEnabled,
click: toggleAutoLaunch,
},
{
label: "Hide Dock Icon",
type: "checkbox",
checked: shouldHideDockIcon,
click: toggleHideDockIcon,
},
],
},
{ type: "separator" },
...macOSOnly([
{
label: "Hide Ente",
role: "hide",
},
{
label: "Hide Others",
role: "hideOthers",
},
{ type: "separator" },
]),
{
label: "Quit",
role: "quit",
},
],
},
{
label: "Edit",
submenu: [
{ label: "Undo", role: "undo" },
{ label: "Redo", role: "redo" },
{ type: "separator" },
{ label: "Cut", role: "cut" },
{ label: "Copy", role: "copy" },
{ label: "Paste", role: "paste" },
{ label: "Select All", role: "selectAll" },
...macOSOnly([
{ type: "separator" },
{
label: "Speech",
submenu: [
{
role: "startSpeaking",
label: "start speaking",
},
{
role: "stopSpeaking",
label: "stop speaking",
},
],
},
]),
],
},
{
label: "View",
submenu: [
{ label: "Reload", role: "reload" },
{ label: "Toggle Dev Tools", role: "toggleDevTools" },
{ type: "separator" },
{ label: "Toggle Full Screen", role: "togglefullscreen" },
],
},
{
label: "Window",
submenu: [
{ label: "Minimize", role: "minimize" },
{ label: "Zoom", role: "zoom" },
{ label: "Close", role: "close" },
...macOSOnly([
{ type: "separator" },
{ label: "Bring All to Front", role: "front" },
{ type: "separator" },
{ label: "Ente", role: "window" },
]),
],
},
{
label: "Help",
submenu: [
{
label: "Ente Help",
click: handleHelp,
},
{ type: "separator" },
{
label: "Support",
click: handleSupport,
},
{
label: "Product Updates",
click: handleBlog,
},
{ type: "separator" },
{
label: "View Logs",
click: handleViewLogs,
},
],
},
]);
};
/**
* Create and return a {@link Menu} that is shown when the user clicks on our
* system tray icon (e.g. the icon list at the top right of the screen on macOS)
*/
export const createTrayContextMenu = (mainWindow: BrowserWindow) => {
const handleOpen = () => {
mainWindow.maximize();
mainWindow.show();
};
const handleClose = () => {
setIsAppQuitting(true);
app.quit();
};
return Menu.buildFromTemplate([
{
label: "Open Ente",
click: handleOpen,
},
{
label: "Quit Ente",
click: handleClose,
},
]);
};

81
desktop/src/main/util.ts Normal file
View file

@ -0,0 +1,81 @@
import shellescape from "any-shell-escape";
import { shell } from "electron"; /* TODO(MR): Why is this not in /main? */
import { app } from "electron/main";
import { exec } from "node:child_process";
import path from "node:path";
import { promisify } from "node:util";
import log from "./log";
/** `true` if the app is running in development mode. */
export const isDev = !app.isPackaged;
/**
* Run a shell command asynchronously.
*
* This is a convenience promisified version of child_process.exec. It runs the
* command asynchronously and returns its stdout and stderr if there were no
* errors.
*
* If the command is passed as a string, then it will be executed verbatim.
*
* If the command is passed as an array, then the first argument will be treated
* as the executable and the remaining (optional) items as the command line
* parameters. This function will shellescape and join the array to form the
* command that finally gets executed.
*
* > Note: This is not a 1-1 replacement of child_process.exec - if you're
* > trying to run a trivial shell command, say something that produces a lot of
* > output, this might not be the best option and it might be better to use the
* > underlying functions.
*/
export const execAsync = (command: string | string[]) => {
const escapedCommand = Array.isArray(command)
? shellescape(command)
: command;
const startTime = Date.now();
log.debug(() => `Running shell command: ${escapedCommand}`);
const result = execAsync_(escapedCommand);
log.debug(
() =>
`Completed in ${Math.round(Date.now() - startTime)} ms (${escapedCommand})`,
);
return result;
};
const execAsync_ = promisify(exec);
/**
* Open the given {@link dirPath} in the system's folder viewer.
*
* For example, on macOS this'll open {@link dirPath} in Finder.
*/
export const openDirectory = async (dirPath: string) => {
const res = await shell.openPath(path.normalize(dirPath));
// shell.openPath resolves with a string containing the error message
// corresponding to the failure if a failure occurred, otherwise "".
if (res) throw new Error(`Failed to open directory ${dirPath}: res`);
};
/**
* Return the path where the logs for the app are saved.
*
* [Note: Electron app paths]
*
* By default, these paths are at the following locations:
*
* - macOS: `~/Library/Application Support/ente`
* - Linux: `~/.config/ente`
* - Windows: `%APPDATA%`, e.g. `C:\Users\<username>\AppData\Local\ente`
* - Windows: C:\Users\<you>\AppData\Local\<Your App Name>
*
* https://www.electronjs.org/docs/latest/api/app
*
*/
const logDirectoryPath = () => app.getPath("logs");
/**
* Open the app's log directory in the system's folder viewer.
*
* @see {@link openDirectory}
*/
export const openLogDirectory = () => openDirectory(logDirectoryPath());

View file

@ -1,20 +1,16 @@
import log from "electron-log";
import { app, net } from "electron/main";
import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "node:path";
import util from "util";
import { CustomErrors } from "../constants/errors";
import { writeStream } from "../main/fs";
import { isDev } from "../main/general";
import { logErrorSentry } from "../main/log";
import log, { logErrorSentry } from "../main/log";
import { execAsync, isDev } from "../main/util";
import { Model } from "../types/ipc";
import Tokenizer from "../utils/clip-bpe-ts/mod";
import { getPlatform } from "../utils/common/platform";
import { generateTempFilePath } from "../utils/temp";
import { deleteTempFile } from "./ffmpeg";
const shellescape = require("any-shell-escape");
const execAsync = util.promisify(require("child_process").exec);
const jpeg = require("jpeg-js");
const CLIP_MODEL_PATH_PLACEHOLDER = "CLIP_MODEL";
@ -100,8 +96,7 @@ export async function getClipImageModelPath(type: "ggml" | "onnx") {
const localFileSize = (await fs.stat(modelSavePath)).size;
if (localFileSize !== IMAGE_MODEL_SIZE_IN_BYTES[type]) {
log.info(
"clip image model size mismatch, downloading again got:",
localFileSize,
`clip image model size mismatch, downloading again got: ${localFileSize}`,
);
imageModelDownloadInProgress = downloadModel(
modelSavePath,
@ -139,8 +134,7 @@ export async function getClipTextModelPath(type: "ggml" | "onnx") {
const localFileSize = (await fs.stat(modelSavePath)).size;
if (localFileSize !== TEXT_MODEL_SIZE_IN_BYTES[type]) {
log.info(
"clip text model size mismatch, downloading again got:",
localFileSize,
`clip text model size mismatch, downloading again got: ${localFileSize}`,
);
textModelDownloadInProgress = true;
downloadModel(modelSavePath, TEXT_MODEL_DOWNLOAD_URL[type])
@ -278,11 +272,7 @@ export async function computeGGMLImageEmbedding(
}
});
const escapedCmd = shellescape(cmd);
log.info("running clip command", escapedCmd);
const startTime = Date.now();
const { stdout } = await execAsync(escapedCmd);
log.info("clip command execution time ", Date.now() - startTime);
const { stdout } = await execAsync(cmd);
// parse stdout and return embedding
// get the last line of stdout
const lines = stdout.split("\n");
@ -291,7 +281,7 @@ export async function computeGGMLImageEmbedding(
const embeddingArray = new Float32Array(embedding);
return embeddingArray;
} catch (err) {
logErrorSentry(err, "Error in computeGGMLImageEmbedding");
log.error("Failed to compute GGML image embedding", err);
throw err;
}
}
@ -316,7 +306,7 @@ export async function computeONNXImageEmbedding(
const imageEmbedding = results["output"].data; // Float32Array
return normalizeEmbedding(imageEmbedding);
} catch (err) {
logErrorSentry(err, "Error in computeONNXImageEmbedding");
log.error("Failed to compute ONNX image embedding", err);
throw err;
}
}
@ -367,11 +357,7 @@ export async function computeGGMLTextEmbedding(
}
});
const escapedCmd = shellescape(cmd);
log.info("running clip command", escapedCmd);
const startTime = Date.now();
const { stdout } = await execAsync(escapedCmd);
log.info("clip command execution time ", Date.now() - startTime);
const { stdout } = await execAsync(cmd);
// parse stdout and return embedding
// get the last line of stdout
const lines = stdout.split("\n");
@ -383,7 +369,7 @@ export async function computeGGMLTextEmbedding(
if (err.message === CustomErrors.MODEL_DOWNLOAD_PENDING) {
log.info(CustomErrors.MODEL_DOWNLOAD_PENDING);
} else {
logErrorSentry(err, "Error in computeGGMLTextEmbedding");
log.error("Failed to compute GGML text embedding", err);
}
throw err;
}

View file

@ -1,18 +1,13 @@
import log from "electron-log";
import pathToFfmpeg from "ffmpeg-static";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import util from "util";
import { CustomErrors } from "../constants/errors";
import { writeStream } from "../main/fs";
import { logError, logErrorSentry } from "../main/log";
import log from "../main/log";
import { execAsync } from "../main/util";
import { ElectronFile } from "../types/ipc";
import { generateTempFilePath, getTempDirPath } from "../utils/temp";
const shellescape = require("any-shell-escape");
const execAsync = util.promisify(require("child_process").exec);
const INPUT_PATH_PLACEHOLDER = "INPUT";
const FFMPEG_PLACEHOLDER = "FFMPEG";
const OUTPUT_PATH_PLACEHOLDER = "OUTPUT";
@ -70,11 +65,7 @@ export async function runFFmpegCmd(
return new File([outputFileData], outputFileName);
} finally {
if (createdTempInputFile) {
try {
await deleteTempFile(inputFilePath);
} catch (e) {
logError(e, "failed to deleteTempFile");
}
await deleteTempFile(inputFilePath);
}
}
}
@ -100,35 +91,23 @@ export async function runFFmpegCmd_(
return cmdPart;
}
});
const escapedCmd = shellescape(cmd);
log.info("running ffmpeg command", escapedCmd);
const startTime = Date.now();
if (dontTimeout) {
await execAsync(escapedCmd);
await execAsync(cmd);
} else {
await promiseWithTimeout(execAsync(escapedCmd), 30 * 1000);
await promiseWithTimeout(execAsync(cmd), 30 * 1000);
}
if (!existsSync(tempOutputFilePath)) {
throw new Error("ffmpeg output file not found");
}
log.info(
"ffmpeg command execution time ",
escapedCmd,
Date.now() - startTime,
"ms",
);
const outputFile = await fs.readFile(tempOutputFilePath);
return new Uint8Array(outputFile);
} catch (e) {
logErrorSentry(e, "ffmpeg run command error");
log.error("FFMPEG command failed", e);
throw e;
} finally {
try {
await fs.rm(tempOutputFilePath, { force: true });
} catch (e) {
logErrorSentry(e, "failed to remove tempOutputFile");
}
await deleteTempFile(tempOutputFilePath);
}
}
@ -153,16 +132,12 @@ export async function writeTempFile(fileStream: Uint8Array, fileName: string) {
export async function deleteTempFile(tempFilePath: string) {
const tempDirPath = await getTempDirPath();
if (!tempFilePath.startsWith(tempDirPath)) {
logErrorSentry(
Error("not a temp file"),
"tried to delete a non temp file",
);
}
if (!tempFilePath.startsWith(tempDirPath))
log.error("Attempting to delete a non-temp file ${tempFilePath}");
await fs.rm(tempFilePath, { force: true });
}
export const promiseWithTimeout = async <T>(
const promiseWithTimeout = async <T>(
request: Promise<T>,
timeout: number,
): Promise<T> => {

View file

@ -1,22 +1,15 @@
import { exec } from "child_process";
import log from "electron-log";
import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "path";
import util from "util";
import { CustomErrors } from "../constants/errors";
import { writeStream } from "../main/fs";
import { isDev } from "../main/general";
import { logError, logErrorSentry } from "../main/log";
import { execAsync, isDev } from "../main/util";
import { ElectronFile } from "../types/ipc";
import { isPlatform } from "../utils/common/platform";
import { generateTempFilePath } from "../utils/temp";
import { deleteTempFile } from "./ffmpeg";
const shellescape = require("any-shell-escape");
const asyncExec = util.promisify(exec);
const IMAGE_MAGICK_PLACEHOLDER = "IMAGE_MAGICK";
const MAX_DIMENSION_PLACEHOLDER = "MAX_DIMENSION";
const SAMPLE_SIZE_PLACEHOLDER = "SAMPLE_SIZE";
@ -104,7 +97,9 @@ async function convertToJPEG_(
await fs.writeFile(tempInputFilePath, fileData);
await runConvertCommand(tempInputFilePath, tempOutputFilePath);
await execAsync(
constructConvertCommand(tempInputFilePath, tempOutputFilePath),
);
return new Uint8Array(await fs.readFile(tempOutputFilePath));
} catch (e) {
@ -124,19 +119,6 @@ async function convertToJPEG_(
}
}
async function runConvertCommand(
tempInputFilePath: string,
tempOutputFilePath: string,
) {
const convertCmd = constructConvertCommand(
tempInputFilePath,
tempOutputFilePath,
);
const escapedCmd = shellescape(convertCmd);
log.info("running convert command: " + escapedCmd);
await asyncExec(escapedCmd);
}
function constructConvertCommand(
tempInputFilePath: string,
tempOutputFilePath: string,
@ -222,13 +204,14 @@ async function generateImageThumbnail_(
tempOutputFilePath = await generateTempFilePath("thumb.jpeg");
let thumbnail: Uint8Array;
do {
await runThumbnailGenerationCommand(
inputFilePath,
tempOutputFilePath,
width,
quality,
await execAsync(
constructThumbnailGenerationCommand(
inputFilePath,
tempOutputFilePath,
width,
quality,
),
);
thumbnail = new Uint8Array(await fs.readFile(tempOutputFilePath));
quality -= 10;
} while (thumbnail.length > maxSize && quality > MIN_QUALITY);
@ -245,23 +228,6 @@ async function generateImageThumbnail_(
}
}
async function runThumbnailGenerationCommand(
inputFilePath: string,
tempOutputFilePath: string,
maxDimension: number,
quality: number,
) {
const thumbnailGenerationCmd: string[] =
constructThumbnailGenerationCommand(
inputFilePath,
tempOutputFilePath,
maxDimension,
quality,
);
const escapedCmd = shellescape(thumbnailGenerationCmd);
log.info("running thumbnail generation command: " + escapedCmd);
await asyncExec(escapedCmd);
}
function constructThumbnailGenerationCommand(
inputFilePath: string,
tempOutputFilePath: string,

25
desktop/src/types/any-shell-escape.d.ts vendored Normal file
View file

@ -0,0 +1,25 @@
/**
* Escape and stringify an array of arguments to be executed on the shell.
*
* @example
*
* const shellescape = require('any-shell-escape');
*
* const args = ['curl', '-v', '-H', 'Location;', '-H', "User-Agent: FooBar's so-called \"Browser\"", 'http://www.daveeddy.com/?name=dave&age=24'];
*
* const escaped = shellescape(args);
* console.log(escaped);
*
* yields (on POSIX shells):
*
* curl -v -H 'Location;' -H 'User-Agent: FoorBar'"'"'s so-called "Browser"' 'http://www.daveeddy.com/?name=dave&age=24'
*
* or (on Windows):
*
* curl -v -H "Location;" -H "User-Agent: FooBar's so-called ""Browser""" "http://www.daveeddy.com/?name=dave&age=24"
Which is suitable for being executed by the shell.
*/
declare module "any-shell-escape" {
declare const shellescape: (args: readonly string | string[]) => string;
export default shellescape;
}

View file

@ -1,215 +0,0 @@
import {
app,
BrowserWindow,
Menu,
MenuItemConstructorOptions,
shell,
} from "electron";
import ElectronLog from "electron-log";
import { setIsAppQuitting } from "../main";
import { openDirectory, openLogDirectory } from "../main/general";
import { forceCheckForUpdateAndNotify } from "../services/appUpdater";
import autoLauncher from "../services/autoLauncher";
import {
getHideDockIconPreference,
setHideDockIconPreference,
} from "../services/userPreference";
import { isPlatform } from "./common/platform";
export function buildContextMenu(mainWindow: BrowserWindow): Menu {
// eslint-disable-next-line camelcase
const contextMenu = Menu.buildFromTemplate([
{
label: "Open ente",
click: function () {
mainWindow.maximize();
mainWindow.show();
},
},
{
label: "Quit ente",
click: function () {
ElectronLog.log("user quit the app");
setIsAppQuitting(true);
app.quit();
},
},
]);
return contextMenu;
}
export async function buildMenuBar(mainWindow: BrowserWindow): Promise<Menu> {
let isAutoLaunchEnabled = await autoLauncher.isEnabled();
const isMac = isPlatform("mac");
let shouldHideDockIcon = getHideDockIconPreference();
const template: MenuItemConstructorOptions[] = [
{
label: "ente",
submenu: [
...((isMac
? [
{
label: "About ente",
role: "about",
},
]
: []) as MenuItemConstructorOptions[]),
{ type: "separator" },
{
label: "Check for updates...",
click: () => {
forceCheckForUpdateAndNotify(mainWindow);
},
},
{
label: "View Changelog",
click: () => {
shell.openExternal(
"https://github.com/ente-io/ente/blob/main/desktop/CHANGELOG.md",
);
},
},
{ type: "separator" },
{
label: "Preferences",
submenu: [
{
label: "Open ente on startup",
type: "checkbox",
checked: isAutoLaunchEnabled,
click: () => {
autoLauncher.toggleAutoLaunch();
isAutoLaunchEnabled = !isAutoLaunchEnabled;
},
},
{
label: "Hide dock icon",
type: "checkbox",
checked: shouldHideDockIcon,
click: () => {
setHideDockIconPreference(!shouldHideDockIcon);
shouldHideDockIcon = !shouldHideDockIcon;
},
},
],
},
{ type: "separator" },
...((isMac
? [
{
label: "Hide ente",
role: "hide",
},
{
label: "Hide others",
role: "hideOthers",
},
]
: []) as MenuItemConstructorOptions[]),
{ type: "separator" },
{
label: "Quit ente",
role: "quit",
},
],
},
{
label: "Edit",
submenu: [
{ role: "undo", label: "Undo" },
{ role: "redo", label: "Redo" },
{ type: "separator" },
{ role: "cut", label: "Cut" },
{ role: "copy", label: "Copy" },
{ role: "paste", label: "Paste" },
...((isMac
? [
{
role: "pasteAndMatchStyle",
label: "Paste and match style",
},
{ role: "delete", label: "Delete" },
{ role: "selectAll", label: "Select all" },
{ type: "separator" },
{
label: "Speech",
submenu: [
{
role: "startSpeaking",
label: "start speaking",
},
{
role: "stopSpeaking",
label: "stop speaking",
},
],
},
]
: [
{ type: "separator" },
{ role: "selectAll", label: "Select all" },
]) as MenuItemConstructorOptions[]),
],
},
{
label: "View",
submenu: [
{ role: "reload", label: "Reload" },
{ role: "forceReload", label: "Force reload" },
{ role: "toggleDevTools", label: "Toggle dev tools" },
{ type: "separator" },
{ role: "resetZoom", label: "Reset zoom" },
{ role: "zoomIn", label: "Zoom in" },
{ role: "zoomOut", label: "Zoom out" },
{ type: "separator" },
{ role: "togglefullscreen", label: "Toggle fullscreen" },
],
},
{
label: "Window",
submenu: [
{ role: "close", label: "Close" },
{ role: "minimize", label: "Minimize" },
...((isMac
? [
{ type: "separator" },
{ role: "front", label: "Bring to front" },
{ type: "separator" },
{ role: "window", label: "ente" },
]
: []) as MenuItemConstructorOptions[]),
],
},
{
label: "Help",
submenu: [
{
label: "Ente Help",
click: () => shell.openExternal("https://help.ente.io/photos/"),
},
{ type: "separator" },
{
label: "Support",
click: () => shell.openExternal("mailto:support@ente.io"),
},
{
label: "Product updates",
click: () => shell.openExternal("https://ente.io/blog/"),
},
{ type: "separator" },
{
label: "View crash reports",
click: () => openDirectory(app.getPath("crashDumps")),
},
{
label: "View logs",
click: openLogDirectory,
},
],
},
];
return Menu.buildFromTemplate(template);
}

View file

@ -9,12 +9,12 @@
/* Recommended target, lib and other settings for code running in the
version of Node.js bundled with Electron.
Currently, with Electron 25, this is Node.js 18
https://www.electronjs.org/blog/electron-25-0
Currently, with Electron 29, this is Node.js 20.9
https://www.electronjs.org/blog/electron-29-0
Note that we cannot do
"extends": "@tsconfig/node18/tsconfig.json",
"extends": "@tsconfig/node20/tsconfig.json",
because that sets "lib": ["es2023"]. However (and I don't fully
understand what's going on here), that breaks our compilation since