diff --git a/desktop/.github/workflows/desktop-draft-release.yml b/desktop/.github/workflows/desktop-draft-release.yml new file mode 100644 index 000000000..8c0652dfc --- /dev/null +++ b/desktop/.github/workflows/desktop-draft-release.yml @@ -0,0 +1,70 @@ +name: "Draft release" + +# Build the desktop/draft-release branch and update the existing draft release +# with the resultant artifacts. +# +# This is meant for doing tests that require the app to be signed and packaged. +# Such releases should not be published to end users. +# +# Workflow: +# +# 1. Push your changes to the "desktop/draft-release" branch on +# https://github.com/ente-io/ente. +# +# 2. Create a draft release with tag equal to the version in the `package.json`. +# +# 3. Trigger this workflow. You can trigger it multiple times, each time it'll +# just update the artifacts attached to the same draft. +# +# 4. Once testing is done delete the draft. + +on: + # Trigger manually or `gh workflow run desktop-draft-release.yml`. + workflow_dispatch: + +jobs: + release: + runs-on: macos-latest + + defaults: + run: + working-directory: desktop + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + repository: ente-io/ente + ref: desktop/draft-release + submodules: recursive + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: yarn install + + - name: Build + uses: ente-io/action-electron-builder@v1.0.0 + with: + package_root: desktop + + # GitHub token, automatically provided to the action + # (No need to define this secret in the repo settings) + github_token: ${{ secrets.GITHUB_TOKEN }} + + # If the commit is tagged with a version (e.g. "v1.0.0"), + # release the app after building. + release: ${{ startsWith(github.ref, 'refs/tags/v') }} + + mac_certs: ${{ secrets.MAC_CERTS }} + mac_certs_password: ${{ secrets.MAC_CERTS_PASSWORD }} + env: + # macOS notarization credentials key details + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: + ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + USE_HARD_LINKS: false diff --git a/desktop/electron-builder.yml b/desktop/electron-builder.yml index f62033fb9..298b1c5f3 100644 --- a/desktop/electron-builder.yml +++ b/desktop/electron-builder.yml @@ -29,5 +29,4 @@ mac: arch: [universal] category: public.app-category.photography hardenedRuntime: true - notarize: true afterSign: electron-builder-notarize diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 49b316206..9cba9178d 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -142,7 +142,7 @@ const createMainWindow = () => { // Create the main window. This'll show our web content. const window = new BrowserWindow({ webPreferences: { - preload: path.join(app.getAppPath(), "preload.js"), + preload: path.join(__dirname, "preload.js"), sandbox: true, }, // The color to show in the window until the web content gets loaded. @@ -287,13 +287,29 @@ const setupTrayItem = (mainWindow: BrowserWindow) => { /** * 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. + * process. This has been removed in favor of cache on the web layer. * - * 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. + * Delete the old cache dir if it exists. + * + * This will happen in two phases. The cache had three subdirectories: + * + * - Two of them, "thumbs" and "files", will be removed now (v1.7.0, May 2024). + * + * - The third one, "face-crops" will be removed once we finish the face search + * changes. See: [Note: Legacy face crops]. + * + * This migration code can be removed after some time once most people have + * upgraded to newer versions. */ const deleteLegacyDiskCacheDirIfExists = async () => { + const removeIfExists = async (dirPath: string) => { + if (existsSync(dirPath)) { + log.info(`Removing legacy disk cache from ${dirPath}`); + await fs.rm(dirPath, { recursive: true }); + } + }; + // [Note: Getting the cache path] + // // The existing code was passing "cache" as a parameter to getPath. // // However, "cache" is not a valid parameter to getPath. It works! (for @@ -309,8 +325,8 @@ const deleteLegacyDiskCacheDirIfExists = async () => { // @ts-expect-error "cache" works but is not part of the public API. 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 }); + await removeIfExists(path.join(cacheDir, "thumbs")); + await removeIfExists(path.join(cacheDir, "files")); } }; @@ -375,7 +391,7 @@ const main = () => { // Continue on with the rest of the startup sequence. Menu.setApplicationMenu(await createApplicationMenu(mainWindow)); setupTrayItem(mainWindow); - if (!isDev) setupAutoUpdater(mainWindow); + setupAutoUpdater(mainWindow); try { await deleteLegacyDiskCacheDirIfExists(); diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index f59969202..1393f4bfd 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -24,6 +24,7 @@ import { updateOnNextRestart, } from "./services/app-update"; import { + legacyFaceCrop, openDirectory, openLogDirectory, selectDirectory, @@ -198,6 +199,10 @@ export const attachIPCHandlers = () => { faceEmbedding(input), ); + ipcMain.handle("legacyFaceCrop", (_, faceID: string) => + legacyFaceCrop(faceID), + ); + // - Upload ipcMain.handle("listZipItems", (_, zipPath: string) => diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts index b6fa7acfe..45cbd6362 100644 --- a/desktop/src/main/menu.ts +++ b/desktop/src/main/menu.ts @@ -10,7 +10,6 @@ import { forceCheckForAppUpdates } from "./services/app-update"; import autoLauncher from "./services/auto-launcher"; import { openLogDirectory } from "./services/dir"; import { userPreferences } from "./stores/user-preferences"; -import { isDev } from "./utils/electron"; /** Create and return the entries in the app's main menu bar */ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { @@ -24,9 +23,6 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { const macOSOnly = (options: MenuItemConstructorOptions[]) => process.platform == "darwin" ? options : []; - const devOnly = (options: MenuItemConstructorOptions[]) => - isDev ? options : []; - const handleCheckForUpdates = () => forceCheckForAppUpdates(mainWindow); const handleViewChangelog = () => @@ -130,11 +126,11 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { submenu: [ { role: "startSpeaking", - label: "start speaking", + label: "Start Speaking", }, { role: "stopSpeaking", - label: "stop speaking", + label: "Stop Speaking", }, ], }, @@ -145,9 +141,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { label: "View", submenu: [ { label: "Reload", role: "reload" }, - ...devOnly([ - { label: "Toggle Dev Tools", role: "toggleDevTools" }, - ]), + { label: "Toggle Dev Tools", role: "toggleDevTools" }, { type: "separator" }, { label: "Toggle Full Screen", role: "togglefullscreen" }, ], diff --git a/desktop/src/main/services/app-update.ts b/desktop/src/main/services/app-update.ts index 8d66cb8c3..5788b9b27 100644 --- a/desktop/src/main/services/app-update.ts +++ b/desktop/src/main/services/app-update.ts @@ -6,11 +6,20 @@ import { allowWindowClose } from "../../main"; import { AppUpdate } from "../../types/ipc"; import log from "../log"; import { userPreferences } from "../stores/user-preferences"; +import { isDev } from "../utils/electron"; export const setupAutoUpdater = (mainWindow: BrowserWindow) => { autoUpdater.logger = electronLog; autoUpdater.autoDownload = false; + // Skip checking for updates automatically in dev builds. Installing an + // update would fail anyway since (at least on macOS), the auto update + // process requires signed builds. + // + // Even though this is skipped on app start, we can still use the "Check for + // updates..." menu option to trigger the update if we wish in dev builds. + if (isDev) return; + const oneDay = 1 * 24 * 60 * 60 * 1000; setInterval(() => void checkForUpdatesAndNotify(mainWindow), oneDay); void checkForUpdatesAndNotify(mainWindow); diff --git a/desktop/src/main/services/auto-launcher.ts b/desktop/src/main/services/auto-launcher.ts index 4e97a0225..0942a4935 100644 --- a/desktop/src/main/services/auto-launcher.ts +++ b/desktop/src/main/services/auto-launcher.ts @@ -27,14 +27,14 @@ class AutoLauncher { } async toggleAutoLaunch() { - const isEnabled = await this.isEnabled(); + const wasEnabled = await this.isEnabled(); const autoLaunch = this.autoLaunch; if (autoLaunch) { - if (isEnabled) await autoLaunch.disable(); + if (wasEnabled) await autoLaunch.disable(); else await autoLaunch.enable(); } else { - if (isEnabled) app.setLoginItemSettings({ openAtLogin: false }); - else app.setLoginItemSettings({ openAtLogin: true }); + const openAtLogin = !wasEnabled; + app.setLoginItemSettings({ openAtLogin }); } } @@ -42,8 +42,7 @@ class AutoLauncher { if (this.autoLaunch) { return app.commandLine.hasSwitch("hidden"); } else { - // TODO(MR): This apparently doesn't work anymore. - return app.getLoginItemSettings().wasOpenedAtLogin; + return app.getLoginItemSettings().openAtLogin; } } } diff --git a/desktop/src/main/services/dir.ts b/desktop/src/main/services/dir.ts index d375648f6..293a720f0 100644 --- a/desktop/src/main/services/dir.ts +++ b/desktop/src/main/services/dir.ts @@ -1,5 +1,7 @@ import { shell } from "electron/common"; import { app, dialog } from "electron/main"; +import { existsSync } from "fs"; +import fs from "node:fs/promises"; import path from "node:path"; import { posixPath } from "../utils/electron"; @@ -38,14 +40,50 @@ export const openLogDirectory = () => openDirectory(logDirectoryPath()); * * [Note: Electron app paths] * - * By default, these paths are at the following locations: + * There are three paths we need to be aware of usually. * - * - macOS: `~/Library/Application Support/ente` + * First is the "appData". We can obtain this with `app.getPath("appData")`. + * This is per-user application data directory. This is usually the following: + * + * - Windows: `%APPDATA%`, e.g. `C:\Users\\AppData\Local` + * - Linux: `~/.config` + * - macOS: `~/Library/Application Support` + * + * Now, if we suffix the app's name onto the appData directory, we get the + * "userData" directory. This is the **primary** place applications are meant to + * store user's data, e.g. various configuration files and saved state. + * + * During development, our app name is "Electron", so this'd be, for example, + * `~/Library/Application Support/Electron` if we run using `yarn dev`. For the + * packaged production app, our app name is "ente", so this would be: + * + * - Windows: `%APPDATA%\ente`, e.g. `C:\Users\\AppData\Local\ente` * - Linux: `~/.config/ente` - * - Windows: `%APPDATA%`, e.g. `C:\Users\\AppData\Local\ente` - * - Windows: C:\Users\\AppData\Local\ + * - macOS: `~/Library/Application Support/ente` + * + * Note that Chromium also stores the browser state, e.g. localStorage or disk + * caches, in userData. + * + * Finally, there is the "logs" directory. This is not within "appData" but has + * a slightly different OS specific path. Since our log file is named + * "ente.log", it can be found at: + * + * - macOS: ~/Library/Logs/ente/ente.log (production) + * - macOS: ~/Library/Logs/Electron/ente.log (dev) * * https://www.electronjs.org/docs/latest/api/app - * */ const logDirectoryPath = () => app.getPath("logs"); + +/** + * See: [Note: Legacy face crops] + */ +export const legacyFaceCrop = async ( + faceID: string, +): Promise => { + // See: [Note: Getting the cache path] + // @ts-expect-error "cache" works but is not part of the public API. + const cacheDir = path.join(app.getPath("cache"), "ente"); + const filePath = path.join(cacheDir, "face-crops", faceID); + return existsSync(filePath) ? await fs.readFile(filePath) : undefined; +}; diff --git a/desktop/src/main/services/store.ts b/desktop/src/main/services/store.ts index 20cc91ea4..471928d76 100644 --- a/desktop/src/main/services/store.ts +++ b/desktop/src/main/services/store.ts @@ -14,6 +14,15 @@ export const clearStores = () => { watchStore.clear(); }; +/** + * [Note: Safe storage keys] + * + * On macOS, `safeStorage` stores our data under a Keychain entry named + * " Safe Storage". Which resolves to: + * + * - Electron Safe Storage (dev) + * - ente Safe Storage (prod) + */ export const saveEncryptionKey = (encryptionKey: string) => { const encryptedKey = safeStorage.encryptString(encryptionKey); const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64"); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 407e541ff..f9147e288 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -164,6 +164,9 @@ const detectFaces = (input: Float32Array) => const faceEmbedding = (input: Float32Array) => ipcRenderer.invoke("faceEmbedding", input); +const legacyFaceCrop = (faceID: string) => + ipcRenderer.invoke("legacyFaceCrop", faceID); + // - Watch const watchGet = () => ipcRenderer.invoke("watchGet"); @@ -341,6 +344,7 @@ contextBridge.exposeInMainWorld("electron", { clipTextEmbeddingIfAvailable, detectFaces, faceEmbedding, + legacyFaceCrop, // - Watch diff --git a/web/apps/photos/src/components/ml/MLSearchSettings.tsx b/web/apps/photos/src/components/ml/MLSearchSettings.tsx index 9b50c2d6a..409df4fc6 100644 --- a/web/apps/photos/src/components/ml/MLSearchSettings.tsx +++ b/web/apps/photos/src/components/ml/MLSearchSettings.tsx @@ -22,7 +22,7 @@ import { getFaceSearchEnabledStatus, updateFaceSearchEnabledStatus, } from "services/userService"; -import { isInternalUser } from "utils/user"; +import { isInternalUserForML } from "utils/user"; export const MLSearchSettings = ({ open, onClose, onRootClose }) => { const { @@ -280,7 +280,7 @@ function EnableMLSearch({ onClose, enableMlSearch, onRootClose }) {

- {isInternalUser() && ( + {isInternalUserForML() && (