Move file related functions

This commit is contained in:
Manav Rathi 2024-03-25 15:09:51 +05:30
parent 4261624da5
commit f53b1361e8
No known key found for this signature in database
6 changed files with 239 additions and 248 deletions

View file

@ -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);
};

View file

@ -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),
);
};
/**

View file

@ -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
// - FS Legacy
/* preload: duplicated logError */
const logError = (error: Error, message: string, info?: any) => {
logErrorSentry(error, message, info);
};
const checkExistsAndCreateDir = (dirPath: string): Promise<void> =>
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<any>,
): Promise<void> => 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<void> =>
ipcRenderer.invoke("saveFileToDisk", path, file);
rs._read = async () => {
try {
const result = await reader.read();
const readTextFile = (path: string): Promise<string> =>
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<boolean> =>
ipcRenderer.invoke("isFolder", dirPath);
return rs;
};
const moveFile = (oldPath: string, newPath: string): Promise<void> =>
ipcRenderer.invoke("moveFile", oldPath, newPath);
const writeNodeStream = async (
filePath: string,
fileStream: NodeJS.ReadableStream,
) => {
const writeable = createWriteStream(filePath);
const deleteFolder = (path: string): Promise<void> =>
ipcRenderer.invoke("deleteFolder", path);
fileStream.on("error", (error) => {
writeable.destroy(error); // Close the writable stream with an error
});
const deleteFile = (path: string): Promise<void> =>
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<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);
}
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<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,
});

View file

@ -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";

View file

@ -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);
}

View file

@ -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>;
}