diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 3d7dfc417..2774ec730 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -8,14 +8,15 @@ * * https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process */ -import { nativeImage } from "electron"; -import { app, BrowserWindow, Menu, protocol, Tray } from "electron/main"; + +import { nativeImage, shell } from "electron/common"; +import type { WebContents } from "electron/main"; +import { BrowserWindow, Menu, Tray, app, protocol } from "electron/main"; import serveNextAt from "next-electron-server"; import { existsSync } from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { handleDownloads, handleExternalLinks } from "./main/init"; import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc"; import log, { initLogging } from "./main/log"; import { createApplicationMenu, createTrayContextMenu } from "./main/menu"; @@ -30,7 +31,7 @@ import { isDev } from "./main/utils-electron"; /** * The URL where the renderer HTML is being served from. */ -export const rendererURL = "ente://app"; +const rendererURL = "ente://app"; /** * We want to hide our window instead of closing it when the user presses the @@ -205,7 +206,7 @@ const createMainWindow = async () => { // webContents is not responding to input messages for > 30 seconds." window.webContents.on("unresponsive", () => { log.error( - "Main window's webContents are unresponsive, will restart the renderer process", + "MainWindow's webContents are unresponsive, will restart the renderer process", ); window.webContents.forcefullyCrashRenderer(); }); @@ -236,6 +237,58 @@ const createMainWindow = async () => { return window; }; +/** + * Automatically set the save path for user initiated downloads to the system's + * "downloads" directory instead of asking the user to select a save location. + */ +export const setDownloadPath = (webContents: WebContents) => { + webContents.session.on("will-download", (_, item) => { + item.setSavePath( + uniqueSavePath(app.getPath("downloads"), item.getFilename()), + ); + }); +}; + +const uniqueSavePath = (dirPath: string, fileName: string) => { + const { name, ext } = path.parse(fileName); + + let savePath = path.join(dirPath, fileName); + let n = 1; + while (existsSync(savePath)) { + const suffixedName = [`${name}(${n})`, ext].filter((x) => x).join("."); + savePath = path.join(dirPath, suffixedName); + n++; + } + return savePath; +}; + +/** + * Allow opening external links, e.g. when the user clicks on the "Feature + * requests" button in the sidebar (to open our GitHub repository), or when they + * click the "Support" button to send an email to support. + * + * @param webContents The renderer to configure. + */ +export const allowExternalLinks = (webContents: WebContents) => { + // By default, if the user were open a link, say + // https://github.com/ente-io/ente/discussions, then it would open a _new_ + // BrowserWindow within our app. + // + // This is not the behaviour we want; what we want is to ask the system to + // handle the link (e.g. open the URL in the default browser, or if it is a + // mailto: link, then open the user's mail client). + // + // Returning `action` "deny" accomplishes this. + webContents.setWindowOpenHandler(({ url }) => { + if (!url.startsWith(rendererURL)) { + shell.openExternal(url); + return { action: "deny" }; + } else { + return { action: "allow" }; + } + }); +}; + /** * Add an icon for our app in the system tray. * @@ -338,23 +391,26 @@ const main = () => { // // Note that some Electron APIs can only be used after this event occurs. app.on("ready", async () => { - // Create window and prepare for renderer + // Create window and prepare for the renderer. mainWindow = await createMainWindow(); attachIPCHandlers(); attachFSWatchIPCHandlers(createWatcher(mainWindow)); registerStreamProtocol(); - handleDownloads(mainWindow); - handleExternalLinks(mainWindow); + + // Configure the renderer's environment. + setDownloadPath(mainWindow.webContents); + allowExternalLinks(mainWindow.webContents); + // TODO(MR): Remove or resurrect // The commit that introduced this header override had the message // "fix cors issue for uploads". Not sure what that means, so disabling // it for now to see why exactly this is required. // addAllowOriginHeader(mainWindow); - // Start loading the renderer + // Start loading the renderer. mainWindow.loadURL(rendererURL); - // Continue on with the rest of the startup sequence + // Continue on with the rest of the startup sequence. Menu.setApplicationMenu(await createApplicationMenu(mainWindow)); setupTrayItem(mainWindow); if (!isDev) setupAutoUpdater(mainWindow); diff --git a/desktop/src/main/init.ts b/desktop/src/main/init.ts index 1b078dc98..d0aee17f8 100644 --- a/desktop/src/main/init.ts +++ b/desktop/src/main/init.ts @@ -1,46 +1,4 @@ -import { BrowserWindow, app, shell } from "electron"; -import { existsSync } from "node:fs"; -import path from "node:path"; -import { rendererURL } from "../main"; - -export function handleDownloads(mainWindow: BrowserWindow) { - mainWindow.webContents.session.on("will-download", (_, item) => { - item.setSavePath( - getUniqueSavePath(item.getFilename(), app.getPath("downloads")), - ); - }); -} - -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 handleExternalLinks(mainWindow: BrowserWindow) { - mainWindow.webContents.setWindowOpenHandler(({ url }) => { - if (!url.startsWith(rendererURL)) { - shell.openExternal(url); - return { action: "deny" }; - } else { - return { action: "allow" }; - } - }); -} +import { BrowserWindow } from "electron"; export function addAllowOriginHeader(mainWindow: BrowserWindow) { mainWindow.webContents.session.webRequest.onHeadersReceived( diff --git a/desktop/src/main/services/ml-clip.ts b/desktop/src/main/services/ml-clip.ts index a5f407f9e..cdd2baab7 100644 --- a/desktop/src/main/services/ml-clip.ts +++ b/desktop/src/main/services/ml-clip.ts @@ -150,7 +150,7 @@ export const clipTextEmbeddingIfAvailable = async (text: string) => { // Don't wait for the download to complete if (typeof sessionOrStatus == "string") { - console.log( + log.info( "Ignoring CLIP text embedding request because model download is pending", ); return undefined; diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 41cd21248..bae4e6afe 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -57,12 +57,14 @@ const handleRead = async (path: string) => { try { const res = await net.fetch(pathToFileURL(path).toString()); if (res.ok) { - // `net.fetch` already seems to add "Content-Type" and - // "Last-Modified" headers. But since we already are stat-ting the - // file for the "Content-Length", we explicitly add the - // "X-Last-Modified-Ms" too, (a) guaranteeing its presence, and (b) - // having it be in the exact format we want (no string <-> date - // conversions) and (c) keeping milliseconds. + // net.fetch already seems to add "Content-Type" and "Last-Modified" + // headers, but I couldn't find documentation for this. In any case, + // since we already are stat-ting the file for the "Content-Length", + // we explicitly add the "X-Last-Modified-Ms" too, + // 1. guaranteeing its presence + // 2. having it be in the exact format we want (no string <-> date + // conversions) + // 3. Retaining milliseconds. const stat = await fs.stat(path); diff --git a/web/apps/photos/src/services/detect-type.ts b/web/apps/photos/src/services/detect-type.ts index 6fd2fd70d..e92e10bf8 100644 --- a/web/apps/photos/src/services/detect-type.ts +++ b/web/apps/photos/src/services/detect-type.ts @@ -93,8 +93,7 @@ const readInitialChunkOfFile = async (file: File) => { const detectFileTypeFromBuffer = async (buffer: Uint8Array) => { const result = await FileType.fromBuffer(buffer); - if (!result?.ext || !result?.mime) { - throw Error(`Could not deduce file type from buffer`); - } + if (!result) + throw Error("Could not deduce file type from the file's contents"); return result; };