浏览代码

Bye ElectronFile

Manav Rathi 1 年之前
父节点
当前提交
f84937f8c1

+ 0 - 72
desktop/src/main/dialogs.ts

@@ -1,72 +0,0 @@
-import { dialog } from "electron/main";
-import fs from "node:fs/promises";
-import path from "node:path";
-import type { ElectronFile } from "../types/ipc";
-import { getElectronFile } from "./services/fs";
-import { getElectronFilesFromGoogleZip } from "./services/upload";
-
-export const selectDirectory = async () => {
-    const result = await dialog.showOpenDialog({
-        properties: ["openDirectory"],
-    });
-    if (result.filePaths && result.filePaths.length > 0) {
-        return result.filePaths[0]?.split(path.sep)?.join(path.posix.sep);
-    }
-};
-
-export const showUploadFilesDialog = async () => {
-    const selectedFiles = await dialog.showOpenDialog({
-        properties: ["openFile", "multiSelections"],
-    });
-    const filePaths = selectedFiles.filePaths;
-    return await Promise.all(filePaths.map(getElectronFile));
-};
-
-export const showUploadDirsDialog = async () => {
-    const dir = await dialog.showOpenDialog({
-        properties: ["openDirectory", "multiSelections"],
-    });
-
-    let filePaths: string[] = [];
-    for (const dirPath of dir.filePaths) {
-        filePaths = [...filePaths, ...(await getDirFilePaths(dirPath))];
-    }
-
-    return await Promise.all(filePaths.map(getElectronFile));
-};
-
-// https://stackoverflow.com/a/63111390
-const getDirFilePaths = async (dirPath: string) => {
-    if (!(await fs.stat(dirPath)).isDirectory()) {
-        return [dirPath];
-    }
-
-    let files: string[] = [];
-    const filePaths = await fs.readdir(dirPath);
-
-    for (const filePath of filePaths) {
-        const absolute = path.join(dirPath, filePath);
-        files = [...files, ...(await getDirFilePaths(absolute))];
-    }
-
-    return files;
-};
-
-export const showUploadZipDialog = async () => {
-    const selectedFiles = await dialog.showOpenDialog({
-        properties: ["openFile", "multiSelections"],
-        filters: [{ name: "Zip File", extensions: ["zip"] }],
-    });
-    const filePaths = selectedFiles.filePaths;
-
-    let files: ElectronFile[] = [];
-
-    for (const filePath of filePaths) {
-        files = [...files, ...(await getElectronFilesFromGoogleZip(filePath))];
-    }
-
-    return {
-        zipPaths: filePaths,
-        files,
-    };
-};

+ 3 - 16
desktop/src/main/ipc.ts

@@ -16,12 +16,6 @@ import type {
     PendingUploads,
     PendingUploads,
     ZipItem,
     ZipItem,
 } from "../types/ipc";
 } from "../types/ipc";
-import {
-    selectDirectory,
-    showUploadDirsDialog,
-    showUploadFilesDialog,
-    showUploadZipDialog,
-} from "./dialogs";
 import {
 import {
     fsExists,
     fsExists,
     fsIsDir,
     fsIsDir,
@@ -39,6 +33,7 @@ import {
     updateAndRestart,
     updateAndRestart,
     updateOnNextRestart,
     updateOnNextRestart,
 } from "./services/app-update";
 } from "./services/app-update";
+import { selectDirectory } from "./services/dialog";
 import { ffmpegExec } from "./services/ffmpeg";
 import { ffmpegExec } from "./services/ffmpeg";
 import { convertToJPEG, generateImageThumbnail } from "./services/image";
 import { convertToJPEG, generateImageThumbnail } from "./services/image";
 import {
 import {
@@ -102,6 +97,8 @@ export const attachIPCHandlers = () => {
     // See [Note: Catching exception during .send/.on]
     // See [Note: Catching exception during .send/.on]
     ipcMain.on("logToDisk", (_, message) => logToDisk(message));
     ipcMain.on("logToDisk", (_, message) => logToDisk(message));
 
 
+    ipcMain.handle("selectDirectory", () => selectDirectory());
+
     ipcMain.on("clearStores", () => clearStores());
     ipcMain.on("clearStores", () => clearStores());
 
 
     ipcMain.handle("saveEncryptionKey", (_, encryptionKey) =>
     ipcMain.handle("saveEncryptionKey", (_, encryptionKey) =>
@@ -193,16 +190,6 @@ export const attachIPCHandlers = () => {
         faceEmbedding(input),
         faceEmbedding(input),
     );
     );
 
 
-    // - File selection
-
-    ipcMain.handle("selectDirectory", () => selectDirectory());
-
-    ipcMain.handle("showUploadFilesDialog", () => showUploadFilesDialog());
-
-    ipcMain.handle("showUploadDirsDialog", () => showUploadDirsDialog());
-
-    ipcMain.handle("showUploadZipDialog", () => showUploadZipDialog());
-
     // - Upload
     // - Upload
 
 
     ipcMain.handle("listZipItems", (_, zipPath: string) =>
     ipcMain.handle("listZipItems", (_, zipPath: string) =>

+ 10 - 0
desktop/src/main/services/dialog.ts

@@ -0,0 +1,10 @@
+import { dialog } from "electron/main";
+import { posixPath } from "../utils-path";
+
+export const selectDirectory = async () => {
+    const result = await dialog.showOpenDialog({
+        properties: ["openDirectory"],
+    });
+    const dirPath = result.filePaths[0];
+    return dirPath ? posixPath(dirPath) : undefined;
+};

+ 0 - 154
desktop/src/main/services/fs.ts

@@ -1,154 +0,0 @@
-import StreamZip from "node-stream-zip";
-import { existsSync } from "node:fs";
-import fs from "node:fs/promises";
-import path from "node:path";
-import { ElectronFile } from "../../types/ipc";
-import log from "../log";
-
-const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024;
-
-const getFileStream = async (filePath: string) => {
-    const file = await fs.open(filePath, "r");
-    let offset = 0;
-    const readableStream = new ReadableStream<Uint8Array>({
-        async pull(controller) {
-            try {
-                const buff = new Uint8Array(FILE_STREAM_CHUNK_SIZE);
-                const bytesRead = (await file.read(
-                    buff,
-                    0,
-                    FILE_STREAM_CHUNK_SIZE,
-                    offset,
-                )) as unknown as number;
-                offset += bytesRead;
-                if (bytesRead === 0) {
-                    controller.close();
-                    await file.close();
-                } else {
-                    controller.enqueue(buff.slice(0, bytesRead));
-                }
-            } catch (e) {
-                await file.close();
-            }
-        },
-        async cancel() {
-            await file.close();
-        },
-    });
-    return readableStream;
-};
-
-export async function getElectronFile(filePath: string): Promise<ElectronFile> {
-    const fileStats = await fs.stat(filePath);
-    return {
-        path: filePath.split(path.sep).join(path.posix.sep),
-        name: path.basename(filePath),
-        size: fileStats.size,
-        lastModified: fileStats.mtime.valueOf(),
-        stream: async () => {
-            if (!existsSync(filePath)) {
-                throw new Error("electronFile does not exist");
-            }
-            return await getFileStream(filePath);
-        },
-        blob: async () => {
-            if (!existsSync(filePath)) {
-                throw new Error("electronFile does not exist");
-            }
-            const blob = await fs.readFile(filePath);
-            return new Blob([new Uint8Array(blob)]);
-        },
-        arrayBuffer: async () => {
-            if (!existsSync(filePath)) {
-                throw new Error("electronFile does not exist");
-            }
-            const blob = await fs.readFile(filePath);
-            return new Uint8Array(blob);
-        },
-    };
-}
-
-export const getZipFileStream = async (
-    zip: StreamZip.StreamZipAsync,
-    filePath: string,
-) => {
-    const stream = await zip.stream(filePath);
-    const done = {
-        current: false,
-    };
-    const inProgress = {
-        current: false,
-    };
-    // eslint-disable-next-line no-unused-vars
-    let resolveObj: (value?: any) => void = null;
-    // eslint-disable-next-line no-unused-vars
-    let rejectObj: (reason?: any) => void = null;
-    stream.on("readable", () => {
-        try {
-            if (resolveObj) {
-                inProgress.current = true;
-                const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer;
-                if (chunk) {
-                    resolveObj(new Uint8Array(chunk));
-                    resolveObj = null;
-                }
-                inProgress.current = false;
-            }
-        } catch (e) {
-            rejectObj(e);
-        }
-    });
-    stream.on("end", () => {
-        try {
-            done.current = true;
-            if (resolveObj && !inProgress.current) {
-                resolveObj(null);
-                resolveObj = null;
-            }
-        } catch (e) {
-            rejectObj(e);
-        }
-    });
-    stream.on("error", (e) => {
-        try {
-            done.current = true;
-            if (rejectObj) {
-                rejectObj(e);
-                rejectObj = null;
-            }
-        } catch (e) {
-            rejectObj(e);
-        }
-    });
-
-    const readStreamData = async () => {
-        return new Promise<Uint8Array>((resolve, reject) => {
-            const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer;
-
-            if (chunk || done.current) {
-                resolve(chunk);
-            } else {
-                resolveObj = resolve;
-                rejectObj = reject;
-            }
-        });
-    };
-
-    const readableStream = new ReadableStream<Uint8Array>({
-        async pull(controller) {
-            try {
-                const data = await readStreamData();
-
-                if (data) {
-                    controller.enqueue(data);
-                } else {
-                    controller.close();
-                }
-            } catch (e) {
-                log.error("Failed to pull from readableStream", e);
-                controller.close();
-            }
-        },
-    });
-    return readableStream;
-};

+ 1 - 1
desktop/src/main/services/image.ts

@@ -1,7 +1,7 @@
 /** @file Image format conversions and thumbnail generation */
 /** @file Image format conversions and thumbnail generation */
 
 
 import fs from "node:fs/promises";
 import fs from "node:fs/promises";
-import path from "path";
+import path from "node:path";
 import { CustomErrorMessage, type ZipItem } from "../../types/ipc";
 import { CustomErrorMessage, type ZipItem } from "../../types/ipc";
 import log from "../log";
 import log from "../log";
 import { execAsync, isDev } from "../utils-electron";
 import { execAsync, isDev } from "../utils-electron";

+ 2 - 51
desktop/src/main/services/upload.ts

@@ -1,10 +1,9 @@
 import StreamZip from "node-stream-zip";
 import StreamZip from "node-stream-zip";
 import fs from "node:fs/promises";
 import fs from "node:fs/promises";
+import path from "node:path";
 import { existsSync } from "original-fs";
 import { existsSync } from "original-fs";
-import path from "path";
-import type { ElectronFile, PendingUploads, ZipItem } from "../../types/ipc";
+import type { PendingUploads, ZipItem } from "../../types/ipc";
 import { uploadStatusStore } from "../stores/upload-status";
 import { uploadStatusStore } from "../stores/upload-status";
-import { getZipFileStream } from "./fs";
 
 
 export const listZipItems = async (zipPath: string): Promise<ZipItem[]> => {
 export const listZipItems = async (zipPath: string): Promise<ZipItem[]> => {
     const zip = new StreamZip.async({ file: zipPath });
     const zip = new StreamZip.async({ file: zipPath });
@@ -99,51 +98,3 @@ export const markUploadedZipItems = async (
 };
 };
 
 
 export const clearPendingUploads = () => uploadStatusStore.clear();
 export const clearPendingUploads = () => uploadStatusStore.clear();
-
-export const getElectronFilesFromGoogleZip = async (filePath: string) => {
-    const zip = new StreamZip.async({
-        file: filePath,
-    });
-    const zipName = path.basename(filePath, ".zip");
-
-    const entries = await zip.entries();
-    const files: ElectronFile[] = [];
-
-    for (const entry of Object.values(entries)) {
-        const basename = path.basename(entry.name);
-        if (entry.isFile && basename.length > 0 && basename[0] !== ".") {
-            files.push(await getZipEntryAsElectronFile(zipName, zip, entry));
-        }
-    }
-
-    zip.close();
-
-    return files;
-};
-
-export async function getZipEntryAsElectronFile(
-    zipName: string,
-    zip: StreamZip.StreamZipAsync,
-    entry: StreamZip.ZipEntry,
-): Promise<ElectronFile> {
-    return {
-        path: path
-            .join(zipName, entry.name)
-            .split(path.sep)
-            .join(path.posix.sep),
-        name: path.basename(entry.name),
-        size: entry.size,
-        lastModified: entry.time,
-        stream: async () => {
-            return await getZipFileStream(zip, entry.name);
-        },
-        blob: async () => {
-            const buffer = await zip.entryData(entry.name);
-            return new Blob([new Uint8Array(buffer)]);
-        },
-        arrayBuffer: async () => {
-            const buffer = await zip.entryData(entry.name);
-            return new Uint8Array(buffer);
-        },
-    };
-}

+ 1 - 7
desktop/src/main/services/watch.ts

@@ -6,6 +6,7 @@ import { FolderWatch, type CollectionMapping } from "../../types/ipc";
 import { fsIsDir } from "../fs";
 import { fsIsDir } from "../fs";
 import log from "../log";
 import log from "../log";
 import { watchStore } from "../stores/watch";
 import { watchStore } from "../stores/watch";
+import { posixPath } from "../utils-path";
 
 
 /**
 /**
  * Create and return a new file system watcher.
  * Create and return a new file system watcher.
@@ -46,13 +47,6 @@ const eventData = (path: string): [string, FolderWatch] => {
     return [path, watch];
     return [path, watch];
 };
 };
 
 
-/**
- * Convert a file system {@link filePath} that uses the local system specific
- * path separators into a path that uses POSIX file separators.
- */
-const posixPath = (filePath: string) =>
-    filePath.split(path.sep).join(path.posix.sep);
-
 export const watchGet = (watcher: FSWatcher) => {
 export const watchGet = (watcher: FSWatcher) => {
     const [valid, deleted] = folderWatches().reduce(
     const [valid, deleted] = folderWatches().reduce(
         ([valid, deleted], watch) => {
         ([valid, deleted], watch) => {

+ 8 - 0
desktop/src/main/utils-path.ts

@@ -0,0 +1,8 @@
+import path from "node:path";
+
+/**
+ * Convert a file system {@link filePath} that uses the local system specific
+ * path separators into a path that uses POSIX file separators.
+ */
+export const posixPath = (filePath: string) =>
+    filePath.split(path.sep).join(path.posix.sep);

+ 1 - 1
desktop/src/main/utils-temp.ts

@@ -2,7 +2,7 @@ import { app } from "electron/main";
 import StreamZip from "node-stream-zip";
 import StreamZip from "node-stream-zip";
 import { existsSync } from "node:fs";
 import { existsSync } from "node:fs";
 import fs from "node:fs/promises";
 import fs from "node:fs/promises";
-import path from "path";
+import path from "node:path";
 import type { ZipItem } from "../types/ipc";
 import type { ZipItem } from "../types/ipc";
 
 
 /**
 /**

+ 4 - 10
desktop/src/preload.ts

@@ -63,6 +63,9 @@ const openDirectory = (dirPath: string): Promise<void> =>
 const openLogDirectory = (): Promise<void> =>
 const openLogDirectory = (): Promise<void> =>
     ipcRenderer.invoke("openLogDirectory");
     ipcRenderer.invoke("openLogDirectory");
 
 
+const selectDirectory = (): Promise<string | undefined> =>
+    ipcRenderer.invoke("selectDirectory");
+
 const clearStores = () => ipcRenderer.send("clearStores");
 const clearStores = () => ipcRenderer.send("clearStores");
 
 
 const encryptionKey = (): Promise<string | undefined> =>
 const encryptionKey = (): Promise<string | undefined> =>
@@ -174,9 +177,6 @@ const faceEmbedding = (input: Float32Array): Promise<Float32Array> =>
 
 
 // TODO: Deprecated - use dialogs on the renderer process itself
 // TODO: Deprecated - use dialogs on the renderer process itself
 
 
-const selectDirectory = (): Promise<string> =>
-    ipcRenderer.invoke("selectDirectory");
-
 const showUploadFilesDialog = (): Promise<ElectronFile[]> =>
 const showUploadFilesDialog = (): Promise<ElectronFile[]> =>
     ipcRenderer.invoke("showUploadFilesDialog");
     ipcRenderer.invoke("showUploadFilesDialog");
 
 
@@ -310,6 +310,7 @@ contextBridge.exposeInMainWorld("electron", {
     logToDisk,
     logToDisk,
     openDirectory,
     openDirectory,
     openLogDirectory,
     openLogDirectory,
+    selectDirectory,
     clearStores,
     clearStores,
     encryptionKey,
     encryptionKey,
     saveEncryptionKey,
     saveEncryptionKey,
@@ -348,13 +349,6 @@ contextBridge.exposeInMainWorld("electron", {
     detectFaces,
     detectFaces,
     faceEmbedding,
     faceEmbedding,
 
 
-    // - File selection
-
-    selectDirectory,
-    showUploadFilesDialog,
-    showUploadDirsDialog,
-    showUploadZipDialog,
-
     // - Watch
     // - Watch
 
 
     watch: {
     watch: {

+ 14 - 16
web/packages/next/types/ipc.ts

@@ -3,8 +3,6 @@
 //
 //
 // See [Note: types.ts <-> preload.ts <-> ipc.ts]
 // See [Note: types.ts <-> preload.ts <-> ipc.ts]
 
 
-import type { ElectronFile } from "./file";
-
 /**
 /**
  * Extra APIs provided by our Node.js layer when our code is running inside our
  * Extra APIs provided by our Node.js layer when our code is running inside our
  * desktop (Electron) app.
  * desktop (Electron) app.
@@ -51,6 +49,18 @@ export interface Electron {
      */
      */
     openLogDirectory: () => Promise<void>;
     openLogDirectory: () => Promise<void>;
 
 
+    /**
+     * Ask the user to select a directory on their local file system, and return
+     * it path.
+     *
+     * We don't strictly need IPC for this, we can use a hidden <input> element
+     * and trigger its click for the same behaviour (as we do for the
+     * `useFileInput` hook that we use for uploads). However, it's a bit
+     * cumbersome, and we anyways will need to IPC to get back its full path, so
+     * it is just convenient to expose this direct method.
+     */
+    selectDirectory: () => Promise<string | undefined>;
+
     /**
     /**
      * Clear any stored data.
      * Clear any stored data.
      *
      *
@@ -122,6 +132,8 @@ export interface Electron {
      */
      */
     skipAppUpdate: (version: string) => void;
     skipAppUpdate: (version: string) => void;
 
 
+    // - FS
+
     /**
     /**
      * A subset of file system access APIs.
      * A subset of file system access APIs.
      *
      *
@@ -332,20 +344,6 @@ export interface Electron {
      */
      */
     faceEmbedding: (input: Float32Array) => Promise<Float32Array>;
     faceEmbedding: (input: Float32Array) => Promise<Float32Array>;
 
 
-    // - File selection
-    // TODO: Deprecated - use dialogs on the renderer process itself
-
-    selectDirectory: () => Promise<string>;
-
-    showUploadFilesDialog: () => Promise<ElectronFile[]>;
-
-    showUploadDirsDialog: () => Promise<ElectronFile[]>;
-
-    showUploadZipDialog: () => Promise<{
-        zipPaths: string[];
-        files: ElectronFile[];
-    }>;
-
     // - Watch
     // - Watch
 
 
     /**
     /**

+ 0 - 23
web/packages/shared/hooks/useFileInput.tsx

@@ -1,28 +1,5 @@
 import { useCallback, useRef, useState } from "react";
 import { useCallback, useRef, useState } from "react";
 
 
-/**
- * [Note: File paths when running under Electron]
- *
- * We have access to the absolute path of the web {@link File} object when we
- * are running in the context of our desktop app.
- *
- * https://www.electronjs.org/docs/latest/api/file-object
- *
- * This is in contrast to the `webkitRelativePath` that we get when we're
- * running in the browser, which is the relative path to the directory that the
- * user selected (or just the name of the file if the user selected or
- * drag/dropped a single one).
- *
- * Note that this is a deprecated approach. From Electron docs:
- *
- * > Warning: The path property that Electron adds to the File interface is
- * > deprecated and will be removed in a future Electron release. We recommend
- * > you use `webUtils.getPathForFile` instead.
- */
-export interface FileWithPath extends File {
-    readonly path?: string;
-}
-
 interface UseFileInputParams {
 interface UseFileInputParams {
     directory?: boolean;
     directory?: boolean;
     accept?: string;
     accept?: string;