diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 6f8881dd6..42c5ab732 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -26,7 +26,7 @@ import { createWatcher } from "./main/services/watch"; import { userPreferences } from "./main/stores/user-preferences"; import { migrateLegacyWatchStoreIfNeeded } from "./main/stores/watch"; import { registerStreamProtocol } from "./main/stream"; -import { isDev } from "./main/utils/electron"; +import { isDev } from "./main/utils"; /** * The URL where the renderer HTML is being served from. diff --git a/desktop/src/main/log.ts b/desktop/src/main/log.ts index d2421da62..c1902d8eb 100644 --- a/desktop/src/main/log.ts +++ b/desktop/src/main/log.ts @@ -1,6 +1,6 @@ import log from "electron-log"; import util from "node:util"; -import { isDev } from "./utils/electron"; +import { isDev } from "./utils"; /** * Initialize logging in the main process. diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts index 990dd40e5..0693c01dc 100644 --- a/desktop/src/main/menu.ts +++ b/desktop/src/main/menu.ts @@ -8,8 +8,9 @@ import { import { allowWindowClose } from "../main"; 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, openLogDirectory } from "./utils/electron"; +import { isDev } from "./utils"; /** Create and return the entries in the app's main menu bar */ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { diff --git a/desktop/src/main/services/dir.ts b/desktop/src/main/services/dir.ts index 4e2a8c65e..ef3adb013 100644 --- a/desktop/src/main/services/dir.ts +++ b/desktop/src/main/services/dir.ts @@ -1,7 +1,7 @@ import { shell } from "electron/common"; import { app, dialog } from "electron/main"; import path from "node:path"; -import { posixPath } from "../utils/electron"; +import { posixPath } from "../utils"; export const selectDirectory = async () => { const result = await dialog.showOpenDialog({ diff --git a/desktop/src/main/services/ffmpeg.ts b/desktop/src/main/services/ffmpeg.ts index 78b7a9e9a..dc417c595 100644 --- a/desktop/src/main/services/ffmpeg.ts +++ b/desktop/src/main/services/ffmpeg.ts @@ -2,8 +2,8 @@ import pathToFfmpeg from "ffmpeg-static"; import fs from "node:fs/promises"; import type { ZipItem } from "../../types/ipc"; import log from "../log"; -import { withTimeout } from "../utils"; -import { execAsync } from "../utils/electron"; +import { execAsync } from "../utils"; +import { withTimeout } from "../utils/common"; import { deleteTempFile, makeFileForDataOrPathOrZipItem, diff --git a/desktop/src/main/services/image.ts b/desktop/src/main/services/image.ts index 957fe8120..d607b0ead 100644 --- a/desktop/src/main/services/image.ts +++ b/desktop/src/main/services/image.ts @@ -4,7 +4,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { CustomErrorMessage, type ZipItem } from "../../types/ipc"; import log from "../log"; -import { execAsync, isDev } from "../utils/electron"; +import { execAsync, isDev } from "../utils"; import { deleteTempFile, makeFileForDataOrPathOrZipItem, diff --git a/desktop/src/main/services/ml-clip.ts b/desktop/src/main/services/ml-clip.ts index 99e512aa6..67c6d2db7 100644 --- a/desktop/src/main/services/ml-clip.ts +++ b/desktop/src/main/services/ml-clip.ts @@ -22,6 +22,7 @@ const cachedCLIPImageSession = makeCachedInferenceSession( export const clipImageEmbedding = async (jpegImageData: Uint8Array) => { const tempFilePath = await makeTempFilePath(); const imageStream = new Response(jpegImageData.buffer).body; + if (!imageStream) throw new Error("Missing body that we just fed data to"); await writeStream(tempFilePath, imageStream); try { return await clipImageEmbedding_(tempFilePath); @@ -134,11 +135,9 @@ const cachedCLIPTextSession = makeCachedInferenceSession( 64173509 /* 61.2 MB */, ); -let _tokenizer: Tokenizer = null; +let _tokenizer: Tokenizer | undefined; const getTokenizer = () => { - if (!_tokenizer) { - _tokenizer = new Tokenizer(); - } + if (!_tokenizer) _tokenizer = new Tokenizer(); return _tokenizer; }; diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index 588279b70..4d7b89e46 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -5,7 +5,7 @@ import path from "node:path"; import { FolderWatch, type CollectionMapping } from "../../types/ipc"; import log from "../log"; import { watchStore } from "../stores/watch"; -import { posixPath } from "../utils/electron"; +import { posixPath } from "../utils"; import { fsIsDir } from "./fs"; /** diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index b37970cfa..be84c022f 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -99,7 +99,10 @@ const handleReadZip = async (zipPath: string, entryName: string) => { try { const zip = new StreamZip.async({ file: zipPath }); const entry = await zip.entry(entryName); + if (!entry) return new Response("", { status: 404 }); + const stream = await zip.stream(entry); + // TODO(MR): when to call zip.close() return new Response(Readable.toWeb(new Readable(stream)), { @@ -122,7 +125,7 @@ const handleReadZip = async (zipPath: string, entryName: string) => { `Failed to read entry ${entryName} from zip file at ${zipPath}`, e, ); - return new Response(`Failed to read stream: ${e.message}`, { + return new Response(`Failed to read stream: ${String(e)}`, { status: 500, }); } diff --git a/desktop/src/main/utils/common.ts b/desktop/src/main/utils/common.ts new file mode 100644 index 000000000..100a8ad2d --- /dev/null +++ b/desktop/src/main/utils/common.ts @@ -0,0 +1,35 @@ +/** + * @file grab bag of utility functions. + * + * These are verbatim copies of functions from web code since there isn't + * currently a common package that both of them share. + */ + +/** + * Wait for {@link ms} milliseconds + * + * This function is a promisified `setTimeout`. It returns a promise that + * resolves after {@link ms} milliseconds. + */ +export const wait = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Await the given {@link promise} for {@link timeoutMS} milliseconds. If it + * does not resolve within {@link timeoutMS}, then reject with a timeout error. + */ +export const withTimeout = async (promise: Promise, ms: number) => { + let timeoutId: ReturnType; + const rejectOnTimeout = new Promise((_, reject) => { + timeoutId = setTimeout( + () => reject(new Error("Operation timed out")), + ms, + ); + }); + const promiseAndCancelTimeout = async () => { + const result = await promise; + clearTimeout(timeoutId); + return result; + }; + return Promise.race([promiseAndCancelTimeout(), rejectOnTimeout]); +}; diff --git a/desktop/src/main/utils/electron.ts b/desktop/src/main/utils/electron.ts deleted file mode 100644 index d627ec5c4..000000000 --- a/desktop/src/main/utils/electron.ts +++ /dev/null @@ -1,49 +0,0 @@ -import shellescape from "any-shell-escape"; -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; - -/** - * Convert a file system {@link filePath} that uses the local system specific - * path separators into a path that uses POSIX file separators. - */ -export const posixPath = (filePath: string) => - filePath.split(path.sep).join(path.posix.sep); - -/** - * 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(); - const result = execAsync_(escapedCommand); - log.debug( - () => `${escapedCommand} (${Math.round(Date.now() - startTime)} ms)`, - ); - return result; -}; - -const execAsync_ = promisify(exec); diff --git a/desktop/src/main/utils/index.ts b/desktop/src/main/utils/index.ts index 1ae35d55d..d627ec5c4 100644 --- a/desktop/src/main/utils/index.ts +++ b/desktop/src/main/utils/index.ts @@ -1,35 +1,49 @@ -/** - * @file grab bag of utility functions. - * - * Many of these are verbatim copies of functions from web code since there - * isn't currently a common package that both of them share. - */ +import shellescape from "any-shell-escape"; +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; /** - * Wait for {@link ms} milliseconds - * - * This function is a promisified `setTimeout`. It returns a promise that - * resolves after {@link ms} milliseconds. + * Convert a file system {@link filePath} that uses the local system specific + * path separators into a path that uses POSIX file separators. */ -export const wait = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); +export const posixPath = (filePath: string) => + filePath.split(path.sep).join(path.posix.sep); /** - * Await the given {@link promise} for {@link timeoutMS} milliseconds. If it - * does not resolve within {@link timeoutMS}, then reject with a timeout error. + * 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 withTimeout = async (promise: Promise, ms: number) => { - let timeoutId: ReturnType; - const rejectOnTimeout = new Promise((_, reject) => { - timeoutId = setTimeout( - () => reject(new Error("Operation timed out")), - ms, - ); - }); - const promiseAndCancelTimeout = async () => { - const result = await promise; - clearTimeout(timeoutId); - return result; - }; - return Promise.race([promiseAndCancelTimeout(), rejectOnTimeout]); +export const execAsync = (command: string | string[]) => { + const escapedCommand = Array.isArray(command) + ? shellescape(command) + : command; + const startTime = Date.now(); + const result = execAsync_(escapedCommand); + log.debug( + () => `${escapedCommand} (${Math.round(Date.now() - startTime)} ms)`, + ); + return result; }; + +const execAsync_ = promisify(exec); diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json index 700ea3fa0..16946bf3f 100644 --- a/desktop/tsconfig.json +++ b/desktop/tsconfig.json @@ -50,12 +50,12 @@ "outDir": "app", /* Temporary overrides to get things to compile with the older config */ - "strict": false, - "noImplicitAny": true + // "strict": false, + "noImplicitAny": true, /* Below is the state we want */ /* Enable these one by one */ - // "strict": true, + "strict": true, /* Require the `type` modifier when importing types */ // "verbatimModuleSyntax": true