diff --git a/desktop/src/main/fs.ts b/desktop/src/main/fs.ts index a870a7ab5..36de710c3 100644 --- a/desktop/src/main/fs.ts +++ b/desktop/src/main/fs.ts @@ -3,7 +3,6 @@ */ import { existsSync } from "node:fs"; import fs from "node:fs/promises"; -import { writeStream } from "./stream"; export const fsExists = (path: string) => existsSync(path); @@ -25,8 +24,6 @@ export const fsWriteFile = (path: string, contents: string) => /* TODO: Audit below this */ -export const saveStreamToDisk = writeStream; - export const isFolder = async (dirPath: string) => { if (!existsSync(dirPath)) return false; const stats = await fs.stat(dirPath); diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 2a7480302..bd29057da 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -26,7 +26,6 @@ import { fsRmdir, fsWriteFile, isFolder, - saveStreamToDisk, } from "./fs"; import { logToDisk } from "./log"; import { @@ -186,12 +185,6 @@ export const attachIPCHandlers = () => { // - FS Legacy - ipcMain.handle( - "saveStreamToDisk", - (_, path: string, fileStream: ReadableStream) => - saveStreamToDisk(path, fileStream), - ); - ipcMain.handle("isFolder", (_, dirPath: string) => isFolder(dirPath)); // - Upload diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index 5f9ce5573..8ddb80dc6 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -30,12 +30,14 @@ export const registerStreamProtocol = () => { protocol.handle("stream", async (request: Request) => { const url = request.url; const { host, pathname } = new URL(url); + // Convert e.g. "%20" to spaces. + const path = decodeURIComponent(pathname); switch (host) { - /* stream://write//path/to/file */ - /* -host/pathname----- */ + /* stream://write/path/to/file */ + /* host-pathname----- */ case "write": try { - await writeStream(pathname, request.body); + await writeStream(path, request.body); return new Response("", { status: 200 }); } catch (e) { log.error(`Failed to write stream for ${url}`, e); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 5603d49a4..ff2cf505a 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -237,11 +237,6 @@ const updateWatchMappingIgnoredFiles = ( // - FS Legacy -const saveStreamToDisk = ( - path: string, - fileStream: ReadableStream, -): Promise => ipcRenderer.invoke("saveStreamToDisk", path, fileStream); - const isFolder = (dirPath: string): Promise => ipcRenderer.invoke("isFolder", dirPath); @@ -357,7 +352,6 @@ contextBridge.exposeInMainWorld("electron", { // - FS legacy // TODO: Move these into fs + document + rename if needed - saveStreamToDisk, isFolder, // - Upload diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index 40bb9345f..7d6279882 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -1,5 +1,4 @@ import { ensureElectron } from "@/next/electron"; -import { isDevBuild } from "@/next/env"; import log from "@/next/log"; import { CustomError } from "@ente/shared/error"; import { Events, eventBus } from "@ente/shared/events"; @@ -35,6 +34,7 @@ import { mergeMetadata, } from "utils/file"; import { safeDirectoryName, safeFileName } from "utils/native-fs"; +import { writeStream } from "utils/native-stream"; import { getAllLocalCollections } from "../collectionService"; import downloadManager from "../download"; import { getAllLocalFiles } from "../fileService"; @@ -993,47 +993,7 @@ class ExportService { fileExportName, file, ); - // TODO(MR): Productionalize - if (isDevBuild) { - const testStream = new ReadableStream({ - async start(controller) { - await sleep(1000); - controller.enqueue("This "); - await sleep(1000); - controller.enqueue("is "); - await sleep(1000); - controller.enqueue("a "); - await sleep(1000); - controller.enqueue("test"); - controller.close(); - }, - }).pipeThrough(new TextEncoderStream()); - console.log({ a: "will send req", updatedFileStream }); - // The duplex parameter needs to be set to 'half' when - // streaming requests. - // - // Currently browsers, and specifically in our case, - // since this code runs only within our desktop - // (Electron) app, Chromium, don't support 'full' duplex - // mode (i.e. streaming both the request and the - // response). - // - // https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests - // - // In another twist, the TypeScript libdom.d.ts does not - // include the "duplex" parameter, so we need to cast to - // get TypeScript to let this code through. e.g. see - // https://github.com/node-fetch/node-fetch/issues/1769 - const req = new Request("stream://write/tmp/foo.txt", { - method: "POST", - // body: updatedFileStream, - body: testStream, - duplex: "half", - } as unknown as RequestInit); - const res = await fetch(req); - console.log({ a: "got res", res }); - } - await electron.saveStreamToDisk( + await writeStream( `${collectionExportPath}/${fileExportName}`, updatedFileStream, ); @@ -1084,7 +1044,7 @@ class ExportService { imageExportName, file, ); - await electron.saveStreamToDisk( + await writeStream( `${collectionExportPath}/${imageExportName}`, imageStream, ); @@ -1096,7 +1056,7 @@ class ExportService { file, ); try { - await electron.saveStreamToDisk( + await writeStream( `${collectionExportPath}/${videoExportName}`, videoStream, ); diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 8f72cb450..785921cc9 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -53,6 +53,7 @@ import { VISIBILITY_STATE } from "types/magicMetadata"; import { FileTypeInfo } from "types/upload"; import { isArchivedFile, updateMagicMetadata } from "utils/magicMetadata"; import { safeFileName } from "utils/native-fs"; +import { writeStream } from "utils/native-stream"; const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000; @@ -798,55 +799,47 @@ async function downloadFileDesktop( electron: Electron, fileReader: FileReader, file: EnteFile, - downloadPath: string, + downloadDir: string, ) { - const fileStream = (await DownloadManager.getFile( + const fs = electron.fs; + const stream = (await DownloadManager.getFile( file, )) as ReadableStream; - const updatedFileStream = await getUpdatedEXIFFileForDownload( + const updatedStream = await getUpdatedEXIFFileForDownload( fileReader, file, - fileStream, + stream, ); if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { - const fileBlob = await new Response(updatedFileStream).blob(); + const fileBlob = await new Response(updatedStream).blob(); const livePhoto = await decodeLivePhoto(file, fileBlob); const imageExportName = await safeFileName( - downloadPath, + downloadDir, livePhoto.imageNameTitle, - electron.fs.exists, + fs.exists, ); const imageStream = generateStreamFromArrayBuffer(livePhoto.image); - await electron.saveStreamToDisk( - `${downloadPath}/${imageExportName}`, - imageStream, - ); + await writeStream(`${downloadDir}/${imageExportName}`, imageStream); try { const videoExportName = await safeFileName( - downloadPath, + downloadDir, livePhoto.videoNameTitle, - electron.fs.exists, + fs.exists, ); const videoStream = generateStreamFromArrayBuffer(livePhoto.video); - await electron.saveStreamToDisk( - `${downloadPath}/${videoExportName}`, - videoStream, - ); + await writeStream(`${downloadDir}/${videoExportName}`, videoStream); } catch (e) { - await electron.fs.rm(`${downloadPath}/${imageExportName}`); + await fs.rm(`${downloadDir}/${imageExportName}`); throw e; } } else { const fileExportName = await safeFileName( - downloadPath, + downloadDir, file.metadata.title, - electron.fs.exists, - ); - await electron.saveStreamToDisk( - `${downloadPath}/${fileExportName}`, - updatedFileStream, + fs.exists, ); + await writeStream(`${downloadDir}/${fileExportName}`, updatedStream); } } diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts new file mode 100644 index 000000000..809aa9e20 --- /dev/null +++ b/web/apps/photos/src/utils/native-stream.ts @@ -0,0 +1,39 @@ +/** + * @file Streaming IPC communication with the Node.js layer of our desktop app. + * + * NOTE: These functions only work when we're running in our desktop app. + */ + +/** + * Write the given stream to a file on the local machine. + * + * **This only works when we're running in our desktop app**. It uses the + * "stream://" protocol handler exposed by our custom code in the Node.js layer. + * See: [Note: IPC streams]. + * + * @param path The path on the local machine where to write the file to. + * @param stream The stream which should be written into the file. + * */ +export const writeStream = async (path: string, stream: ReadableStream) => { + // The duplex parameter needs to be set to 'half' when streaming requests. + // + // Currently browsers, and specifically in our case, since this code runs + // only within our desktop (Electron) app, Chromium, don't support 'full' + // duplex mode (i.e. streaming both the request and the response). + // https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests + // + // In another twist, the TypeScript libdom.d.ts does not include the + // "duplex" parameter, so we need to cast to get TypeScript to let this code + // through. e.g. see https://github.com/node-fetch/node-fetch/issues/1769 + const req = new Request(`stream://write${path}`, { + // GET can't have a body + method: "POST", + body: stream, + duplex: "half", + } as unknown as RequestInit); + const res = await fetch(req); + if (!res.ok) + throw new Error( + `Failed to write stream to ${path}: HTTP ${res.status}`, + ); +}; diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 60bbb39f8..3477d745e 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -311,10 +311,6 @@ export interface Electron { ) => Promise; // - FS legacy - saveStreamToDisk: ( - path: string, - fileStream: ReadableStream, - ) => Promise; isFolder: (dirPath: string) => Promise; // - Upload