diff --git a/desktop/src/main/services/ffmpeg.ts b/desktop/src/main/services/ffmpeg.ts index ed3542f6a..ea9ceaf76 100644 --- a/desktop/src/main/services/ffmpeg.ts +++ b/desktop/src/main/services/ffmpeg.ts @@ -1,9 +1,14 @@ import pathToFfmpeg from "ffmpeg-static"; import fs from "node:fs/promises"; +import type { ZipEntry } from "../../types/ipc"; import log from "../log"; import { withTimeout } from "../utils"; import { execAsync } from "../utils-electron"; -import { deleteTempFile, makeTempFilePath } from "../utils-temp"; +import { + deleteTempFile, + makeFileForDataOrPathOrZipEntry, + makeTempFilePath, +} from "../utils-temp"; /* Duplicated in the web app's code (used by the WASM FFmpeg implementation). */ const ffmpegPathPlaceholder = "FFMPEG"; @@ -39,28 +44,24 @@ const outputPathPlaceholder = "OUTPUT"; */ export const ffmpegExec = async ( command: string[], - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, outputFileExtension: string, timeoutMS: number, ): Promise => { - // TODO (MR): This currently copies files for both input and output. This - // needs to be tested extremely large video files when invoked downstream of - // `convertToMP4` in the web code. + // TODO (MR): This currently copies files for both input (when + // dataOrPathOrZipEntry is data) and output. This needs to be tested + // extremely large video files when invoked downstream of `convertToMP4` in + // the web code. - let inputFilePath: string; - let isInputFileTemporary: boolean; - if (dataOrPath instanceof Uint8Array) { - inputFilePath = await makeTempFilePath(); - isInputFileTemporary = true; - } else { - inputFilePath = dataOrPath; - isInputFileTemporary = false; - } + const { + path: inputFilePath, + isFileTemporary: isInputFileTemporary, + writeToTemporaryFile: writeToTemporaryInputFile, + } = await makeFileForDataOrPathOrZipEntry(dataOrPathOrZipEntry); const outputFilePath = await makeTempFilePath(outputFileExtension); try { - if (dataOrPath instanceof Uint8Array) - await fs.writeFile(inputFilePath, dataOrPath); + await writeToTemporaryInputFile(); const cmd = substitutePlaceholders( command, diff --git a/desktop/src/main/services/image.ts b/desktop/src/main/services/image.ts index 894ff3404..c55bacdff 100644 --- a/desktop/src/main/services/image.ts +++ b/desktop/src/main/services/image.ts @@ -1,12 +1,15 @@ /** @file Image format conversions and thumbnail generation */ -import StreamZip from "node-stream-zip"; import fs from "node:fs/promises"; import path from "path"; import { CustomErrorMessage, type ZipEntry } from "../../types/ipc"; import log from "../log"; import { execAsync, isDev } from "../utils-electron"; -import { deleteTempFile, makeTempFilePath } from "../utils-temp"; +import { + deleteTempFile, + makeFileForDataOrPathOrZipEntry, + makeTempFilePath, +} from "../utils-temp"; export const convertToJPEG = async (imageData: Uint8Array) => { const inputFilePath = await makeTempFilePath(); @@ -68,28 +71,11 @@ export const generateImageThumbnail = async ( maxDimension: number, maxSize: number, ): Promise => { - let inputFilePath: string; - let isInputFileTemporary: boolean; - let writeToTemporaryInputFile = async () => {}; - if (typeof dataOrPathOrZipEntry == "string") { - inputFilePath = dataOrPathOrZipEntry; - isInputFileTemporary = false; - } else { - inputFilePath = await makeTempFilePath(); - isInputFileTemporary = true; - if (dataOrPathOrZipEntry instanceof Uint8Array) { - writeToTemporaryInputFile = async () => { - await fs.writeFile(inputFilePath, dataOrPathOrZipEntry); - }; - } else { - writeToTemporaryInputFile = async () => { - const [zipPath, entryName] = dataOrPathOrZipEntry; - const zip = new StreamZip.async({ file: zipPath }); - await zip.extract(entryName, inputFilePath); - zip.close(); - }; - } - } + const { + path: inputFilePath, + isFileTemporary: isInputFileTemporary, + writeToTemporaryFile: writeToTemporaryInputFile, + } = await makeFileForDataOrPathOrZipEntry(dataOrPathOrZipEntry); const outputFilePath = await makeTempFilePath("jpeg"); @@ -103,7 +89,7 @@ export const generateImageThumbnail = async ( ); try { - writeToTemporaryInputFile(); + await writeToTemporaryInputFile(); let thumbnail: Uint8Array; do { diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index a26722cb8..804a84736 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -21,6 +21,8 @@ export const listZipEntries = async (zipPath: string): Promise => { } } + zip.close(); + return entryNames.map((entryName) => [zipPath, entryName]); }; @@ -34,7 +36,9 @@ export const pathOrZipEntrySize = async ( const [zipPath, entryName] = pathOrZipEntry; const zip = new StreamZip.async({ file: zipPath }); const entry = await zip.entry(entryName); - return entry.size; + const size = entry.size; + zip.close(); + return size; } }; @@ -110,6 +114,8 @@ export const getElectronFilesFromGoogleZip = async (filePath: string) => { } } + zip.close(); + return files; }; diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index c51887449..bcffe2cc5 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -100,6 +100,7 @@ const handleReadZip = async (zipPath: string, zipEntryPath: string) => { const zip = new StreamZip.async({ file: zipPath }); const entry = await zip.entry(zipEntryPath); const stream = await zip.stream(entry); + // TODO(MR): when to call zip.close() return new Response(Readable.toWeb(new Readable(stream)), { headers: { diff --git a/desktop/src/main/utils-temp.ts b/desktop/src/main/utils-temp.ts index a52daf619..2e416bd65 100644 --- a/desktop/src/main/utils-temp.ts +++ b/desktop/src/main/utils-temp.ts @@ -1,7 +1,9 @@ import { app } from "electron/main"; +import StreamZip from "node-stream-zip"; import { existsSync } from "node:fs"; import fs from "node:fs/promises"; import path from "path"; +import type { ZipEntry } from "../types/ipc"; /** * Our very own directory within the system temp directory. Go crazy, but @@ -61,3 +63,62 @@ export const deleteTempFile = async (tempFilePath: string) => { throw new Error(`Attempting to delete a non-temp file ${tempFilePath}`); await fs.rm(tempFilePath, { force: true }); }; + +/** The result of {@link makeFileForDataOrPathOrZipEntry}. */ +interface FileForDataOrPathOrZipEntry { + /** The path to the file (possibly temporary) */ + path: string; + /** + * `true` if {@link path} points to a temporary file which should be deleted + * once we are done processing. + */ + isFileTemporary: boolean; + /** + * If set, this'll be a function that can be called to actually write the + * contents of the source `Uint8Array | string | ZipEntry` into the file at + * {@link path}. + * + * It will be undefined if the source is already a path since nothing needs + * to be written in that case. In the other two cases this function will + * write the data or zip entry into the file at {@link path}. + */ + writeToTemporaryFile?: () => Promise; +} + +/** + * Return the path to a file, a boolean indicating if this is a temporary path + * that needs to be deleted after processing, and a function to write the given + * {@link dataOrPathOrZipEntry} into that temporary file if needed. + * + * @param dataOrPathOrZipEntry The contents of the file, or the path to an + * existing file, or a (path to a zip file, name of an entry within that zip + * file) tuple. + */ +export const makeFileForDataOrPathOrZipEntry = async ( + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, +): Promise => { + let path: string; + let isFileTemporary: boolean; + let writeToTemporaryFile: () => Promise | undefined; + + if (typeof dataOrPathOrZipEntry == "string") { + path = dataOrPathOrZipEntry; + isFileTemporary = false; + } else { + path = await makeTempFilePath(); + isFileTemporary = true; + if (dataOrPathOrZipEntry instanceof Uint8Array) { + writeToTemporaryFile = () => + fs.writeFile(path, dataOrPathOrZipEntry); + } else { + writeToTemporaryFile = async () => { + const [zipPath, entryName] = dataOrPathOrZipEntry; + const zip = new StreamZip.async({ file: zipPath }); + await zip.extract(entryName, path); + zip.close(); + }; + } + } + + return { path, isFileTemporary, writeToTemporaryFile }; +};