Manav Rathi 1 год назад
Родитель
Сommit
f53b1361e8

+ 141 - 1
desktop/src/main/fs.ts

@@ -1,12 +1,152 @@
 /**
  * @file file system related functions exposed over the context bridge.
  */
-import { existsSync } from "node:fs";
+import { createWriteStream, existsSync } from "node:fs";
 import * as fs from "node:fs/promises";
+import * as path from "node:path";
+import { Readable } from "node:stream";
+import { logError } from "./log";
 
 export const fsExists = (path: string) => existsSync(path);
 
+/**
+ * Write a (web) ReadableStream to a file at the given {@link filePath}.
+ *
+ * The returned promise resolves when the write completes.
+ *
+ * @param filePath The local filesystem path where the file should be written.
+ * @param readableStream A [web
+ * ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
+ */
+export const writeStream = (filePath: string, readableStream: ReadableStream) =>
+    writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
+
+/**
+ * Convert a Web ReadableStream into a Node.js ReadableStream
+ *
+ * This can be used to, for example, write a ReadableStream obtained via
+ * `net.fetch` into a file using the Node.js `fs` APIs
+ */
+const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
+    const reader = readableStream.getReader();
+    const rs = new Readable();
+
+    rs._read = async () => {
+        try {
+            const result = await reader.read();
+
+            if (!result.done) {
+                rs.push(Buffer.from(result.value));
+            } else {
+                rs.push(null);
+                return;
+            }
+        } catch (e) {
+            rs.emit("error", e);
+        }
+    };
+
+    return rs;
+};
+
+const writeNodeStream = async (
+    filePath: string,
+    fileStream: NodeJS.ReadableStream,
+) => {
+    const writeable = createWriteStream(filePath);
+
+    fileStream.on("error", (error) => {
+        writeable.destroy(error); // Close the writable stream with an error
+    });
+
+    fileStream.pipe(writeable);
+
+    await new Promise((resolve, reject) => {
+        writeable.on("finish", resolve);
+        writeable.on("error", async (e: unknown) => {
+            if (existsSync(filePath)) {
+                await fs.unlink(filePath);
+            }
+            reject(e);
+        });
+    });
+};
+
 /* TODO: Audit below this  */
 
 export const checkExistsAndCreateDir = (dirPath: string) =>
     fs.mkdir(dirPath, { recursive: true });
+
+export const saveStreamToDisk = writeStream;
+
+export const saveFileToDisk = (path: string, contents: string) =>
+    fs.writeFile(path, contents);
+
+export const readTextFile = async (filePath: string) =>
+    fs.readFile(filePath, "utf-8");
+
+export const moveFile = async (sourcePath: string, destinationPath: string) => {
+    if (!existsSync(sourcePath)) {
+        throw new Error("File does not exist");
+    }
+    if (existsSync(destinationPath)) {
+        throw new Error("Destination file already exists");
+    }
+    // check if destination folder exists
+    const destinationFolder = path.dirname(destinationPath);
+    await fs.mkdir(destinationFolder, { recursive: true });
+    await fs.rename(sourcePath, destinationPath);
+};
+
+export const isFolder = async (dirPath: string) => {
+    try {
+        const stats = await fs.stat(dirPath);
+        return stats.isDirectory();
+    } catch (e) {
+        let err = e;
+        // if code is defined, it's an error from fs.stat
+        if (typeof e.code !== "undefined") {
+            // ENOENT means the file does not exist
+            if (e.code === "ENOENT") {
+                return false;
+            }
+            err = Error(`fs error code: ${e.code}`);
+        }
+        logError(err, "isFolder failed");
+        return false;
+    }
+};
+
+export const deleteFolder = async (folderPath: string) => {
+    if (!existsSync(folderPath)) {
+        return;
+    }
+    const stat = await fs.stat(folderPath);
+    if (!stat.isDirectory()) {
+        throw new Error("Path is not a folder");
+    }
+    // check if folder is empty
+    const files = await fs.readdir(folderPath);
+    if (files.length > 0) {
+        throw new Error("Folder is not empty");
+    }
+    await fs.rmdir(folderPath);
+};
+
+export const rename = async (oldPath: string, newPath: string) => {
+    if (!existsSync(oldPath)) {
+        throw new Error("Path does not exist");
+    }
+    await fs.rename(oldPath, newPath);
+};
+
+export const deleteFile = async (filePath: string) => {
+    if (!existsSync(filePath)) {
+        return;
+    }
+    const stat = await fs.stat(filePath);
+    if (!stat.isFile()) {
+        throw new Error("Path is not a file");
+    }
+    return fs.rm(filePath);
+};

+ 38 - 1
desktop/src/main/ipc.ts

@@ -39,7 +39,18 @@ import {
     showUploadFilesDialog,
     showUploadZipDialog,
 } from "./dialogs";
-import { checkExistsAndCreateDir, fsExists } from "./fs";
+import {
+    checkExistsAndCreateDir,
+    deleteFile,
+    deleteFolder,
+    fsExists,
+    isFolder,
+    moveFile,
+    readTextFile,
+    rename,
+    saveFileToDisk,
+    saveStreamToDisk,
+} from "./fs";
 import { openDirectory, openLogDirectory } from "./general";
 import { logToDisk } from "./log";
 
@@ -137,6 +148,32 @@ export const attachIPCHandlers = () => {
     ipcMain.handle("checkExistsAndCreateDir", (_, dirPath) =>
         checkExistsAndCreateDir(dirPath),
     );
+
+    ipcMain.handle(
+        "saveStreamToDisk",
+        (_, path: string, fileStream: ReadableStream<any>) =>
+            saveStreamToDisk(path, fileStream),
+    );
+
+    ipcMain.handle("saveFileToDisk", (_, path: string, file: any) =>
+        saveFileToDisk(path, file),
+    );
+
+    ipcMain.handle("readTextFile", (_, path: string) => readTextFile(path));
+
+    ipcMain.handle("isFolder", (_, dirPath: string) => isFolder(dirPath));
+
+    ipcMain.handle("moveFile", (_, oldPath: string, newPath: string) =>
+        moveFile(oldPath, newPath),
+    );
+
+    ipcMain.handle("deleteFolder", (_, path: string) => deleteFolder(path));
+
+    ipcMain.handle("deleteFile", (_, path: string) => deleteFile(path));
+
+    ipcMain.handle("rename", (_, oldPath: string, newPath: string) =>
+        rename(oldPath, newPath),
+    );
 };
 
 /**

+ 44 - 179
desktop/src/preload.ts

@@ -27,10 +27,6 @@
  */
 
 import { contextBridge, ipcRenderer } from "electron";
-import { createWriteStream, existsSync } from "node:fs";
-import * as fs from "node:fs/promises";
-import { Readable } from "node:stream";
-import path from "path";
 import { getDirFiles } from "./api/fs";
 import {
     getElectronFilesFromGoogleZip,
@@ -38,7 +34,7 @@ import {
     setToUploadCollection,
     setToUploadFiles,
 } from "./api/upload";
-import { logErrorSentry, setupLogging } from "./main/log";
+import { setupLogging } from "./main/log";
 import type { ElectronFile } from "./types";
 
 setupLogging();
@@ -80,24 +76,6 @@ const fsExists = (path: string): Promise<boolean> =>
 
 // - AUDIT below this
 
-const checkExistsAndCreateDir = (dirPath: string): Promise<void> =>
-    ipcRenderer.invoke("checkExistsAndCreateDir", dirPath);
-
-/* preload: duplicated */
-interface AppUpdateInfo {
-    autoUpdatable: boolean;
-    version: string;
-}
-
-const registerUpdateEventListener = (
-    showUpdateDialog: (updateInfo: AppUpdateInfo) => void,
-) => {
-    ipcRenderer.removeAllListeners("show-update-dialog");
-    ipcRenderer.on("show-update-dialog", (_, updateInfo: AppUpdateInfo) => {
-        showUpdateDialog(updateInfo);
-    });
-};
-
 const registerForegroundEventListener = (onForeground: () => void) => {
     ipcRenderer.removeAllListeners("app-in-foreground");
     ipcRenderer.on("app-in-foreground", () => {
@@ -117,6 +95,21 @@ const getEncryptionKey = (): Promise<string> =>
 
 // - App update
 
+/* preload: duplicated */
+interface AppUpdateInfo {
+    autoUpdatable: boolean;
+    version: string;
+}
+
+const registerUpdateEventListener = (
+    showUpdateDialog: (updateInfo: AppUpdateInfo) => void,
+) => {
+    ipcRenderer.removeAllListeners("show-update-dialog");
+    ipcRenderer.on("show-update-dialog", (_, updateInfo: AppUpdateInfo) => {
+        showUpdateDialog(updateInfo);
+    });
+};
+
 const updateAndRestart = () => {
     ipcRenderer.send("update-and-restart");
 };
@@ -266,163 +259,36 @@ const updateWatchMappingIgnoredFiles = (
 ): Promise<void> =>
     ipcRenderer.invoke("updateWatchMappingIgnoredFiles", folderPath, files);
 
-// - FIXME below this
-
-/* preload: duplicated logError */
-const logError = (error: Error, message: string, info?: any) => {
-    logErrorSentry(error, message, info);
-};
-
-/* preload: duplicated writeStream */
-/**
- * Write a (web) ReadableStream to a file at the given {@link filePath}.
- *
- * The returned promise resolves when the write completes.
- *
- * @param filePath The local filesystem path where the file should be written.
- * @param readableStream A [web
- * ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
- */
-const writeStream = (filePath: string, readableStream: ReadableStream) =>
-    writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
-
-/**
- * Convert a Web ReadableStream into a Node.js ReadableStream
- *
- * This can be used to, for example, write a ReadableStream obtained via
- * `net.fetch` into a file using the Node.js `fs` APIs
- */
-const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
-    const reader = readableStream.getReader();
-    const rs = new Readable();
-
-    rs._read = async () => {
-        try {
-            const result = await reader.read();
-
-            if (!result.done) {
-                rs.push(Buffer.from(result.value));
-            } else {
-                rs.push(null);
-                return;
-            }
-        } catch (e) {
-            rs.emit("error", e);
-        }
-    };
-
-    return rs;
-};
-
-const writeNodeStream = async (
-    filePath: string,
-    fileStream: NodeJS.ReadableStream,
-) => {
-    const writeable = createWriteStream(filePath);
-
-    fileStream.on("error", (error) => {
-        writeable.destroy(error); // Close the writable stream with an error
-    });
-
-    fileStream.pipe(writeable);
+// - FS Legacy
 
-    await new Promise((resolve, reject) => {
-        writeable.on("finish", resolve);
-        writeable.on("error", async (e: unknown) => {
-            if (existsSync(filePath)) {
-                await fs.unlink(filePath);
-            }
-            reject(e);
-        });
-    });
-};
+const checkExistsAndCreateDir = (dirPath: string): Promise<void> =>
+    ipcRenderer.invoke("checkExistsAndCreateDir", dirPath);
 
-// - Export
+const saveStreamToDisk = (
+    path: string,
+    fileStream: ReadableStream<any>,
+): Promise<void> => ipcRenderer.invoke("saveStreamToDisk", path, fileStream);
 
-const saveStreamToDisk = writeStream;
+const saveFileToDisk = (path: string, file: any): Promise<void> =>
+    ipcRenderer.invoke("saveFileToDisk", path, file);
 
-const saveFileToDisk = (path: string, contents: string) =>
-    fs.writeFile(path, contents);
+const readTextFile = (path: string): Promise<string> =>
+    ipcRenderer.invoke("readTextFile", path);
 
-// -
+const isFolder = (dirPath: string): Promise<boolean> =>
+    ipcRenderer.invoke("isFolder", dirPath);
 
-async function readTextFile(filePath: string) {
-    if (!existsSync(filePath)) {
-        throw new Error("File does not exist");
-    }
-    return await fs.readFile(filePath, "utf-8");
-}
+const moveFile = (oldPath: string, newPath: string): Promise<void> =>
+    ipcRenderer.invoke("moveFile", oldPath, newPath);
 
-async function moveFile(
-    sourcePath: string,
-    destinationPath: string,
-): Promise<void> {
-    if (!existsSync(sourcePath)) {
-        throw new Error("File does not exist");
-    }
-    if (existsSync(destinationPath)) {
-        throw new Error("Destination file already exists");
-    }
-    // check if destination folder exists
-    const destinationFolder = path.dirname(destinationPath);
-    await fs.mkdir(destinationFolder, { recursive: true });
-    await fs.rename(sourcePath, destinationPath);
-}
+const deleteFolder = (path: string): Promise<void> =>
+    ipcRenderer.invoke("deleteFolder", path);
 
-export async function isFolder(dirPath: string) {
-    try {
-        const stats = await fs.stat(dirPath);
-        return stats.isDirectory();
-    } catch (e) {
-        let err = e;
-        // if code is defined, it's an error from fs.stat
-        if (typeof e.code !== "undefined") {
-            // ENOENT means the file does not exist
-            if (e.code === "ENOENT") {
-                return false;
-            }
-            err = Error(`fs error code: ${e.code}`);
-        }
-        logError(err, "isFolder failed");
-        return false;
-    }
-}
+const deleteFile = (path: string): Promise<void> =>
+    ipcRenderer.invoke("deleteFile", path);
 
-async function deleteFolder(folderPath: string): Promise<void> {
-    if (!existsSync(folderPath)) {
-        return;
-    }
-    const stat = await fs.stat(folderPath);
-    if (!stat.isDirectory()) {
-        throw new Error("Path is not a folder");
-    }
-    // check if folder is empty
-    const files = await fs.readdir(folderPath);
-    if (files.length > 0) {
-        throw new Error("Folder is not empty");
-    }
-    await fs.rmdir(folderPath);
-}
-
-async function rename(oldPath: string, newPath: string) {
-    if (!existsSync(oldPath)) {
-        throw new Error("Path does not exist");
-    }
-    await fs.rename(oldPath, newPath);
-}
-
-const deleteFile = async (filePath: string) => {
-    if (!existsSync(filePath)) {
-        return;
-    }
-    const stat = await fs.stat(filePath);
-    if (!stat.isFile()) {
-        throw new Error("Path is not a file");
-    }
-    return fs.rm(filePath);
-};
-
-// -
+const rename = (oldPath: string, newPath: string): Promise<void> =>
+    ipcRenderer.invoke("rename", oldPath, newPath);
 
 // These objects exposed here will become available to the JS code in our
 // renderer (the web/ code) as `window.ElectronAPIs.*`
@@ -506,21 +372,20 @@ contextBridge.exposeInMainWorld("ElectronAPIs", {
     // - FS legacy
     // TODO: Move these into fs + document + rename if needed
     checkExistsAndCreateDir,
-
-    // - Export
     saveStreamToDisk,
     saveFileToDisk,
     readTextFile,
+    isFolder,
+    moveFile,
+    deleteFolder,
+    deleteFile,
+    rename,
+
+    // - Export
 
     getPendingUploads,
     setToUploadFiles,
     getElectronFilesFromGoogleZip,
     setToUploadCollection,
     getDirFiles,
-
-    isFolder,
-    moveFile,
-    deleteFolder,
-    rename,
-    deleteFile,
 });

+ 1 - 1
desktop/src/services/ffmpeg.ts

@@ -4,8 +4,8 @@ import { existsSync } from "node:fs";
 import * as fs from "node:fs/promises";
 import util from "util";
 import { CustomErrors } from "../constants/errors";
+import { writeStream } from "../main/fs";
 import { logError, logErrorSentry } from "../main/log";
-import { writeStream } from "../services/fs";
 import { ElectronFile } from "../types";
 import { generateTempFilePath, getTempDirPath } from "../utils/temp";
 

+ 2 - 58
desktop/src/services/fs.ts

@@ -1,10 +1,9 @@
 import StreamZip from "node-stream-zip";
-import { createWriteStream, existsSync } from "node:fs";
+import { existsSync } from "node:fs";
 import * as fs from "node:fs/promises";
 import * as path from "node:path";
-import { Readable } from "stream";
-import { ElectronFile } from "../types";
 import { logError } from "../main/log";
+import { ElectronFile } from "../types";
 
 const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024;
 
@@ -181,58 +180,3 @@ export const getZipFileStream = async (
     });
     return readableStream;
 };
-
-export const convertBrowserStreamToNode = (
-    fileStream: ReadableStream<Uint8Array>,
-) => {
-    const reader = fileStream.getReader();
-    const rs = new Readable();
-
-    rs._read = async () => {
-        try {
-            const result = await reader.read();
-
-            if (!result.done) {
-                rs.push(Buffer.from(result.value));
-            } else {
-                rs.push(null);
-                return;
-            }
-        } catch (e) {
-            rs.emit("error", e);
-        }
-    };
-
-    return rs;
-};
-
-export async function writeNodeStream(
-    filePath: string,
-    fileStream: NodeJS.ReadableStream,
-) {
-    const writeable = createWriteStream(filePath);
-
-    fileStream.on("error", (error) => {
-        writeable.destroy(error); // Close the writable stream with an error
-    });
-
-    fileStream.pipe(writeable);
-
-    await new Promise((resolve, reject) => {
-        writeable.on("finish", resolve);
-        writeable.on("error", async (e: unknown) => {
-            if (existsSync(filePath)) {
-                await fs.unlink(filePath);
-            }
-            reject(e);
-        });
-    });
-}
-
-export async function writeStream(
-    filePath: string,
-    fileStream: ReadableStream<Uint8Array>,
-) {
-    const readable = convertBrowserStreamToNode(fileStream);
-    await writeNodeStream(filePath, readable);
-}

+ 13 - 8
web/packages/shared/electron/types.ts

@@ -78,7 +78,12 @@ export interface ElectronAPIsType {
         exists: (path: string) => Promise<boolean>;
     };
 
-    /** TODO: AUDIT below this */
+    /*
+     * TODO: AUDIT below this - Some of the types we use below are not copyable
+     * across process boundaries, and such functions will (expectedly) fail at
+     * runtime. For such functions, find an efficient alternative or refactor
+     * the dataflow.
+     */
 
     // - General
 
@@ -175,14 +180,19 @@ export interface ElectronAPIsType {
 
     // - FS legacy
     checkExistsAndCreateDir: (dirPath: string) => Promise<void>;
-
-    /** TODO: FIXME or migrate below this */
     saveStreamToDisk: (
         path: string,
         fileStream: ReadableStream<any>,
     ) => Promise<void>;
     saveFileToDisk: (path: string, file: any) => Promise<void>;
     readTextFile: (path: string) => Promise<string>;
+    isFolder: (dirPath: string) => Promise<boolean>;
+    moveFile: (oldPath: string, newPath: string) => Promise<void>;
+    deleteFolder: (path: string) => Promise<void>;
+    deleteFile: (path: string) => Promise<void>;
+    rename: (oldPath: string, newPath: string) => Promise<void>;
+
+    /** TODO: FIXME or migrate below this */
 
     getPendingUploads: () => Promise<{
         files: ElectronFile[];
@@ -195,9 +205,4 @@ export interface ElectronAPIsType {
     ) => Promise<ElectronFile[]>;
     setToUploadCollection: (collectionName: string) => void;
     getDirFiles: (dirPath: string) => Promise<ElectronFile[]>;
-    isFolder: (dirPath: string) => Promise<boolean>;
-    moveFile: (oldPath: string, newPath: string) => Promise<void>;
-    deleteFolder: (path: string) => Promise<void>;
-    deleteFile: (path: string) => Promise<void>;
-    rename: (oldPath: string, newPath: string) => Promise<void>;
 }