Move file related functions
This commit is contained in:
parent
4261624da5
commit
f53b1361e8
6 changed files with 239 additions and 248 deletions
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue