From f53b1361e81c2b8c6b8fce40ca3036441c508a18 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 25 Mar 2024 15:09:51 +0530 Subject: [PATCH] Move file related functions --- desktop/src/main/fs.ts | 142 +++++++++++++++- desktop/src/main/ipc.ts | 39 ++++- desktop/src/preload.ts | 223 +++++--------------------- desktop/src/services/ffmpeg.ts | 2 +- desktop/src/services/fs.ts | 60 +------ web/packages/shared/electron/types.ts | 21 ++- 6 files changed, 239 insertions(+), 248 deletions(-) diff --git a/desktop/src/main/fs.ts b/desktop/src/main/fs.ts index 12527d551..f56e765c0 100644 --- a/desktop/src/main/fs.ts +++ b/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); +}; diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index ccee028ba..f4798c3c4 100644 --- a/desktop/src/main/ipc.ts +++ b/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) => + 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), + ); }; /** diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 7c539a276..2ea1c2bca 100644 --- a/desktop/src/preload.ts +++ b/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 => // - AUDIT below this -const checkExistsAndCreateDir = (dirPath: string): Promise => - 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 => // - 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 => ipcRenderer.invoke("updateWatchMappingIgnoredFiles", folderPath, files); -// - FIXME below this +// - FS Legacy -/* preload: duplicated logError */ -const logError = (error: Error, message: string, info?: any) => { - logErrorSentry(error, message, info); -}; +const checkExistsAndCreateDir = (dirPath: string): Promise => + ipcRenderer.invoke("checkExistsAndCreateDir", dirPath); -/* 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)); +const saveStreamToDisk = ( + path: string, + fileStream: ReadableStream, +): Promise => ipcRenderer.invoke("saveStreamToDisk", path, fileStream); -/** - * 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(); +const saveFileToDisk = (path: string, file: any): Promise => + ipcRenderer.invoke("saveFileToDisk", path, file); - rs._read = async () => { - try { - const result = await reader.read(); +const readTextFile = (path: string): Promise => + ipcRenderer.invoke("readTextFile", path); - if (!result.done) { - rs.push(Buffer.from(result.value)); - } else { - rs.push(null); - return; - } - } catch (e) { - rs.emit("error", e); - } - }; +const isFolder = (dirPath: string): Promise => + ipcRenderer.invoke("isFolder", dirPath); - return rs; -}; +const moveFile = (oldPath: string, newPath: string): Promise => + ipcRenderer.invoke("moveFile", oldPath, newPath); -const writeNodeStream = async ( - filePath: string, - fileStream: NodeJS.ReadableStream, -) => { - const writeable = createWriteStream(filePath); +const deleteFolder = (path: string): Promise => + ipcRenderer.invoke("deleteFolder", path); - fileStream.on("error", (error) => { - writeable.destroy(error); // Close the writable stream with an error - }); +const deleteFile = (path: string): Promise => + ipcRenderer.invoke("deleteFile", path); - 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 - -const saveStreamToDisk = writeStream; - -const saveFileToDisk = (path: string, contents: string) => - fs.writeFile(path, contents); - -// - - -async function readTextFile(filePath: string) { - if (!existsSync(filePath)) { - throw new Error("File does not exist"); - } - return await fs.readFile(filePath, "utf-8"); -} - -async function moveFile( - sourcePath: string, - destinationPath: string, -): Promise { - 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 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; - } -} - -async function deleteFolder(folderPath: string): Promise { - 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 => + 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, }); diff --git a/desktop/src/services/ffmpeg.ts b/desktop/src/services/ffmpeg.ts index 7b1564764..50fcfd54f 100644 --- a/desktop/src/services/ffmpeg.ts +++ b/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"; diff --git a/desktop/src/services/fs.ts b/desktop/src/services/fs.ts index 54dc6082c..50848d6c1 100644 --- a/desktop/src/services/fs.ts +++ b/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, -) => { - 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, -) { - const readable = convertBrowserStreamToNode(fileStream); - await writeNodeStream(filePath, readable); -} diff --git a/web/packages/shared/electron/types.ts b/web/packages/shared/electron/types.ts index 9902481b1..0949c18ba 100644 --- a/web/packages/shared/electron/types.ts +++ b/web/packages/shared/electron/types.ts @@ -78,7 +78,12 @@ export interface ElectronAPIsType { exists: (path: string) => Promise; }; - /** 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; - - /** TODO: FIXME or migrate below this */ saveStreamToDisk: ( path: string, fileStream: ReadableStream, ) => Promise; saveFileToDisk: (path: string, file: any) => Promise; readTextFile: (path: string) => Promise; + isFolder: (dirPath: string) => Promise; + moveFile: (oldPath: string, newPath: string) => Promise; + deleteFolder: (path: string) => Promise; + deleteFile: (path: string) => Promise; + rename: (oldPath: string, newPath: string) => Promise; + + /** TODO: FIXME or migrate below this */ getPendingUploads: () => Promise<{ files: ElectronFile[]; @@ -195,9 +205,4 @@ export interface ElectronAPIsType { ) => Promise; setToUploadCollection: (collectionName: string) => void; getDirFiles: (dirPath: string) => Promise; - isFolder: (dirPath: string) => Promise; - moveFile: (oldPath: string, newPath: string) => Promise; - deleteFolder: (path: string) => Promise; - deleteFile: (path: string) => Promise; - rename: (oldPath: string, newPath: string) => Promise; }