diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 01f481f8e..1a9582862 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -54,9 +54,9 @@ import { import { clearPendingUploads, listZipEntries, - pathOrZipEntrySize, markUploadedFiles, markUploadedZipEntries, + pathOrZipEntrySize, pendingUploads, setPendingUploads, } from "./services/upload"; @@ -152,10 +152,11 @@ export const attachIPCHandlers = () => { "generateImageThumbnail", ( _, - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, maxDimension: number, maxSize: number, - ) => generateImageThumbnail(dataOrPath, maxDimension, maxSize), + ) => + generateImageThumbnail(dataOrPathOrZipEntry, maxDimension, maxSize), ); ipcMain.handle( @@ -163,10 +164,16 @@ export const attachIPCHandlers = () => { ( _, command: string[], - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, outputFileExtension: string, timeoutMS: number, - ) => ffmpegExec(command, dataOrPath, outputFileExtension, timeoutMS), + ) => + ffmpegExec( + command, + dataOrPathOrZipEntry, + outputFileExtension, + timeoutMS, + ), ); // - ML diff --git a/desktop/src/main/services/image.ts b/desktop/src/main/services/image.ts index 26b4b351e..894ff3404 100644 --- a/desktop/src/main/services/image.ts +++ b/desktop/src/main/services/image.ts @@ -1,8 +1,9 @@ /** @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 } from "../../types/ipc"; +import { CustomErrorMessage, type ZipEntry } from "../../types/ipc"; import log from "../log"; import { execAsync, isDev } from "../utils-electron"; import { deleteTempFile, makeTempFilePath } from "../utils-temp"; @@ -63,18 +64,31 @@ const imageMagickPath = () => path.join(isDev ? "build" : process.resourcesPath, "image-magick"); export const generateImageThumbnail = async ( - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, maxDimension: number, maxSize: number, ): Promise => { let inputFilePath: string; let isInputFileTemporary: boolean; - if (dataOrPath instanceof Uint8Array) { + let writeToTemporaryInputFile = async () => {}; + if (typeof dataOrPathOrZipEntry == "string") { + inputFilePath = dataOrPathOrZipEntry; + isInputFileTemporary = false; + } else { inputFilePath = await makeTempFilePath(); isInputFileTemporary = true; - } else { - inputFilePath = dataOrPath; - isInputFileTemporary = false; + 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 outputFilePath = await makeTempFilePath("jpeg"); @@ -89,8 +103,7 @@ export const generateImageThumbnail = async ( ); try { - if (dataOrPath instanceof Uint8Array) - await fs.writeFile(inputFilePath, dataOrPath); + writeToTemporaryInputFile(); let thumbnail: Uint8Array; do { diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index ddd639c30..c51887449 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -97,9 +97,7 @@ const handleRead = async (path: string) => { const handleReadZip = async (zipPath: string, zipEntryPath: string) => { try { - const zip = new StreamZip.async({ - file: zipPath, - }); + const zip = new StreamZip.async({ file: zipPath }); const entry = await zip.entry(zipEntryPath); const stream = await zip.stream(entry); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 226a80767..76d44591e 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -129,27 +129,27 @@ const convertToJPEG = (imageData: Uint8Array): Promise => ipcRenderer.invoke("convertToJPEG", imageData); const generateImageThumbnail = ( - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, maxDimension: number, maxSize: number, ): Promise => ipcRenderer.invoke( "generateImageThumbnail", - dataOrPath, + dataOrPathOrZipEntry, maxDimension, maxSize, ); const ffmpegExec = ( command: string[], - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, outputFileExtension: string, timeoutMS: number, ): Promise => ipcRenderer.invoke( "ffmpegExec", command, - dataOrPath, + dataOrPathOrZipEntry, outputFileExtension, timeoutMS, ); diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 34bb9196a..4b3d97dd3 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -221,22 +221,27 @@ export interface Electron { * not yet possible, this function will throw an error with the * {@link CustomErrorMessage.NotAvailable} message. * - * @param dataOrPath The raw image data (the contents of the image file), or - * the path to the image file, whose thumbnail we want to generate. + * @param dataOrPathOrZipEntry The file whose thumbnail we want to generate. + * It can be provided as raw image data (the contents of the image file), or + * the path to the image file, or a tuple containing the path of the zip + * file along with the name of an entry in it. + * * @param maxDimension The maximum width or height of the generated * thumbnail. + * * @param maxSize Maximum size (in bytes) of the generated thumbnail. * * @returns JPEG data of the generated thumbnail. */ generateImageThumbnail: ( - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, maxDimension: number, maxSize: number, ) => Promise; /** - * Execute a FFmpeg {@link command} on the given {@link dataOrPath}. + * Execute a FFmpeg {@link command} on the given + * {@link dataOrPathOrZipEntry}. * * This executes the command using a FFmpeg executable we bundle with our * desktop app. We also have a wasm FFmpeg wasm implementation that we use @@ -249,10 +254,11 @@ export interface Electron { * (respectively {@link inputPathPlaceholder}, * {@link outputPathPlaceholder}, {@link ffmpegPathPlaceholder}). * - * @param dataOrPath The bytes of the input file, or the path to the input - * file on the user's local disk. In both cases, the data gets serialized to - * a temporary file, and then that path gets substituted in the FFmpeg - * {@link command} in lieu of {@link inputPathPlaceholder}. + * @param dataOrPathOrZipEntry The bytes of the input file, or the path to + * the input file on the user's local disk, or the path to a zip file on the + * user's disk and the name of an entry in it. In all three cases, the data + * gets serialized to a temporary file, and then that path gets substituted + * in the FFmpeg {@link command} in lieu of {@link inputPathPlaceholder}. * * @param outputFileExtension The extension (without the dot, e.g. "jpeg") * to use for the output file that we ask FFmpeg to create in @@ -268,7 +274,7 @@ export interface Electron { */ ffmpegExec: ( command: string[], - dataOrPath: Uint8Array | string, + dataOrPathOrZipEntry: Uint8Array | string | ZipEntry, outputFileExtension: string, timeoutMS: number, ) => Promise;