فهرست منبع

[desktop] Fix export related IPC - Part 4/x (#1441)

Manav Rathi 1 سال پیش
والد
کامیت
2ae119b2ec

+ 0 - 11
desktop/src/main/fs.ts

@@ -3,7 +3,6 @@
  */
  */
 import { createWriteStream, existsSync } from "node:fs";
 import { createWriteStream, existsSync } from "node:fs";
 import fs from "node:fs/promises";
 import fs from "node:fs/promises";
-import path from "node:path";
 import { Readable } from "node:stream";
 import { Readable } from "node:stream";
 
 
 export const fsExists = (path: string) => existsSync(path);
 export const fsExists = (path: string) => existsSync(path);
@@ -91,16 +90,6 @@ export const saveFileToDisk = (path: string, contents: string) =>
 export const readTextFile = async (filePath: string) =>
 export const readTextFile = async (filePath: string) =>
     fs.readFile(filePath, "utf-8");
     fs.readFile(filePath, "utf-8");
 
 
-export const moveFile = async (sourcePath: string, destinationPath: string) => {
-    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) => {
 export const isFolder = async (dirPath: string) => {
     if (!existsSync(dirPath)) return false;
     if (!existsSync(dirPath)) return false;
     const stats = await fs.stat(dirPath);
     const stats = await fs.stat(dirPath);

+ 2 - 7
desktop/src/main/ipc.ts

@@ -18,13 +18,12 @@ import {
     showUploadZipDialog,
     showUploadZipDialog,
 } from "./dialogs";
 } from "./dialogs";
 import {
 import {
-    fsRm,
-    fsRmdir,
     fsExists,
     fsExists,
     fsMkdirIfNeeded,
     fsMkdirIfNeeded,
     fsRename,
     fsRename,
+    fsRm,
+    fsRmdir,
     isFolder,
     isFolder,
-    moveFile,
     readTextFile,
     readTextFile,
     saveFileToDisk,
     saveFileToDisk,
     saveStreamToDisk,
     saveStreamToDisk,
@@ -195,10 +194,6 @@ export const attachIPCHandlers = () => {
 
 
     ipcMain.handle("isFolder", (_, dirPath: string) => isFolder(dirPath));
     ipcMain.handle("isFolder", (_, dirPath: string) => isFolder(dirPath));
 
 
-    ipcMain.handle("moveFile", (_, oldPath: string, newPath: string) =>
-        moveFile(oldPath, newPath),
-    );
-
     // - Upload
     // - Upload
 
 
     ipcMain.handle("getPendingUploads", () => getPendingUploads());
     ipcMain.handle("getPendingUploads", () => getPendingUploads());

+ 5 - 9
desktop/src/preload.ts

@@ -105,6 +105,11 @@ const fsMkdirIfNeeded = (dirPath: string): Promise<void> =>
 const fsRename = (oldPath: string, newPath: string): Promise<void> =>
 const fsRename = (oldPath: string, newPath: string): Promise<void> =>
     ipcRenderer.invoke("fsRename", oldPath, newPath);
     ipcRenderer.invoke("fsRename", oldPath, newPath);
 
 
+const fsRmdir = (path: string): Promise<void> =>
+    ipcRenderer.invoke("fsRmdir", path);
+
+const fsRm = (path: string): Promise<void> => ipcRenderer.invoke("fsRm", path);
+
 // - AUDIT below this
 // - AUDIT below this
 
 
 // - Conversion
 // - Conversion
@@ -238,14 +243,6 @@ const readTextFile = (path: string): Promise<string> =>
 const isFolder = (dirPath: string): Promise<boolean> =>
 const isFolder = (dirPath: string): Promise<boolean> =>
     ipcRenderer.invoke("isFolder", dirPath);
     ipcRenderer.invoke("isFolder", dirPath);
 
 
-const moveFile = (oldPath: string, newPath: string): Promise<void> =>
-    ipcRenderer.invoke("moveFile", oldPath, newPath);
-
-const fsRmdir = (path: string): Promise<void> =>
-    ipcRenderer.invoke("fsRmdir", path);
-
-const fsRm = (path: string): Promise<void> => ipcRenderer.invoke("fsRm", path);
-
 // - Upload
 // - Upload
 
 
 const getPendingUploads = (): Promise<{
 const getPendingUploads = (): Promise<{
@@ -359,7 +356,6 @@ contextBridge.exposeInMainWorld("electron", {
     saveFileToDisk,
     saveFileToDisk,
     readTextFile,
     readTextFile,
     isFolder,
     isFolder,
-    moveFile,
 
 
     // - Upload
     // - Upload
 
 

+ 3 - 2
web/apps/cast/src/services/typeDetectionService.ts

@@ -1,3 +1,4 @@
+import { nameAndExtension } from "@/next/file";
 import log from "@/next/log";
 import log from "@/next/log";
 import { CustomError } from "@ente/shared/error";
 import { CustomError } from "@ente/shared/error";
 import { FILE_TYPE } from "constants/file";
 import { FILE_TYPE } from "constants/file";
@@ -7,7 +8,6 @@ import {
 } from "constants/upload";
 } from "constants/upload";
 import FileType from "file-type";
 import FileType from "file-type";
 import { FileTypeInfo } from "types/upload";
 import { FileTypeInfo } from "types/upload";
-import { getFileExtension } from "utils/file";
 import { getUint8ArrayView } from "./readerService";
 import { getUint8ArrayView } from "./readerService";
 
 
 const TYPE_VIDEO = "video";
 const TYPE_VIDEO = "video";
@@ -40,7 +40,8 @@ export async function getFileType(receivedFile: File): Promise<FileTypeInfo> {
             mimeType: typeResult.mime,
             mimeType: typeResult.mime,
         };
         };
     } catch (e) {
     } catch (e) {
-        const fileFormat = getFileExtension(receivedFile.name);
+        const ne = nameAndExtension(receivedFile.name);
+        const fileFormat = ne[1].toLowerCase();
         const whiteListedFormat = WHITELISTED_FILE_FORMATS.find(
         const whiteListedFormat = WHITELISTED_FILE_FORMATS.find(
             (a) => a.exactType === fileFormat,
             (a) => a.exactType === fileFormat,
         );
         );

+ 0 - 14
web/apps/cast/src/utils/file/index.ts

@@ -97,20 +97,6 @@ export function getFileExtensionWithDot(filename: string) {
     else return filename.slice(lastDotPosition);
     else return filename.slice(lastDotPosition);
 }
 }
 
 
-export function splitFilenameAndExtension(filename: string): [string, string] {
-    const lastDotPosition = filename.lastIndexOf(".");
-    if (lastDotPosition === -1) return [filename, null];
-    else
-        return [
-            filename.slice(0, lastDotPosition),
-            filename.slice(lastDotPosition + 1),
-        ];
-}
-
-export function getFileExtension(filename: string) {
-    return splitFilenameAndExtension(filename)[1]?.toLocaleLowerCase();
-}
-
 export function generateStreamFromArrayBuffer(data: Uint8Array) {
 export function generateStreamFromArrayBuffer(data: Uint8Array) {
     return new ReadableStream({
     return new ReadableStream({
         async start(controller: ReadableStreamDefaultController) {
         async start(controller: ReadableStreamDefaultController) {

+ 27 - 46
web/apps/photos/src/components/ExportModal.tsx

@@ -18,7 +18,10 @@ import { t } from "i18next";
 import isElectron from "is-electron";
 import isElectron from "is-electron";
 import { AppContext } from "pages/_app";
 import { AppContext } from "pages/_app";
 import { useContext, useEffect, useState } from "react";
 import { useContext, useEffect, useState } from "react";
-import exportService, { ExportStage } from "services/export";
+import exportService, {
+    ExportStage,
+    selectAndPrepareExportDirectory,
+} from "services/export";
 import { ExportProgress, ExportSettings } from "types/export";
 import { ExportProgress, ExportSettings } from "types/export";
 import { EnteFile } from "types/file";
 import { EnteFile } from "types/file";
 import { getExportDirectoryDoesNotExistMessage } from "utils/ui";
 import { getExportDirectoryDoesNotExistMessage } from "utils/ui";
@@ -77,21 +80,6 @@ export default function ExportModal(props: Props) {
         void syncExportRecord(exportFolder);
         void syncExportRecord(exportFolder);
     }, [props.show]);
     }, [props.show]);
 
 
-    // =============
-    // STATE UPDATERS
-    // ==============
-    const updateExportFolder = (newFolder: string) => {
-        exportService.updateExportSettings({ folder: newFolder });
-        setExportFolder(newFolder);
-    };
-
-    const updateContinuousExport = (updatedContinuousExport: boolean) => {
-        exportService.updateExportSettings({
-            continuousExport: updatedContinuousExport,
-        });
-        setContinuousExport(updatedContinuousExport);
-    };
-
     // ======================
     // ======================
     // HELPER FUNCTIONS
     // HELPER FUNCTIONS
     // =======================
     // =======================
@@ -101,8 +89,9 @@ export default function ExportModal(props: Props) {
             appContext.setDialogMessage(
             appContext.setDialogMessage(
                 getExportDirectoryDoesNotExistMessage(),
                 getExportDirectoryDoesNotExistMessage(),
             );
             );
-            throw Error(CustomError.EXPORT_FOLDER_DOES_NOT_EXIST);
+            return false;
         }
         }
+        return true;
     };
     };
 
 
     const syncExportRecord = async (exportFolder: string): Promise<void> => {
     const syncExportRecord = async (exportFolder: string): Promise<void> => {
@@ -131,42 +120,34 @@ export default function ExportModal(props: Props) {
     // =============
     // =============
 
 
     const handleChangeExportDirectoryClick = async () => {
     const handleChangeExportDirectoryClick = async () => {
-        try {
-            const newFolder = await exportService.changeExportDirectory();
-            log.info(`Export folder changed to ${newFolder}`);
-            updateExportFolder(newFolder);
-            void syncExportRecord(newFolder);
-        } catch (e) {
-            if (e.message !== CustomError.SELECT_FOLDER_ABORTED) {
-                log.error("handleChangeExportDirectoryClick failed", e);
-            }
-        }
+        const newFolder = await selectAndPrepareExportDirectory();
+        if (!newFolder) return;
+
+        log.info(`Export folder changed to ${newFolder}`);
+        exportService.updateExportSettings({ folder: newFolder });
+        setExportFolder(newFolder);
+        await syncExportRecord(newFolder);
     };
     };
 
 
     const toggleContinuousExport = async () => {
     const toggleContinuousExport = async () => {
-        try {
-            await verifyExportFolderExists();
-            const newContinuousExport = !continuousExport;
-            if (newContinuousExport) {
-                exportService.enableContinuousExport();
-            } else {
-                exportService.disableContinuousExport();
-            }
-            updateContinuousExport(newContinuousExport);
-        } catch (e) {
-            log.error("onContinuousExportChange failed", e);
+        if (!(await verifyExportFolderExists())) return;
+
+        const newContinuousExport = !continuousExport;
+        if (newContinuousExport) {
+            exportService.enableContinuousExport();
+        } else {
+            exportService.disableContinuousExport();
         }
         }
+        exportService.updateExportSettings({
+            continuousExport: newContinuousExport,
+        });
+        setContinuousExport(newContinuousExport);
     };
     };
 
 
     const startExport = async () => {
     const startExport = async () => {
-        try {
-            await verifyExportFolderExists();
-            await exportService.scheduleExport();
-        } catch (e) {
-            if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
-                log.error("scheduleExport failed", e);
-            }
-        }
+        if (!(await verifyExportFolderExists())) return;
+
+        await exportService.scheduleExport();
     };
     };
 
 
     const stopExport = () => {
     const stopExport = () => {

+ 72 - 121
web/apps/photos/src/services/export/index.ts

@@ -32,7 +32,6 @@ import {
     getPersonalFiles,
     getPersonalFiles,
     getUpdatedEXIFFileForDownload,
     getUpdatedEXIFFileForDownload,
     mergeMetadata,
     mergeMetadata,
-    splitFilenameAndExtension,
 } from "utils/file";
 } from "utils/file";
 import { safeDirectoryName, safeFileName } from "utils/native-fs";
 import { safeDirectoryName, safeFileName } from "utils/native-fs";
 import { getAllLocalCollections } from "../collectionService";
 import { getAllLocalCollections } from "../collectionService";
@@ -165,24 +164,6 @@ class ExportService {
         this.uiUpdater.setLastExportTime(exportTime);
         this.uiUpdater.setLastExportTime(exportTime);
     }
     }
 
 
-    async changeExportDirectory() {
-        const electron = ensureElectron();
-        try {
-            const newRootDir = await electron.selectDirectory();
-            if (!newRootDir) {
-                throw Error(CustomError.SELECT_FOLDER_ABORTED);
-            }
-            const newExportDir = `${newRootDir}/${exportDirectoryName}`;
-            await electron.fs.mkdirIfNeeded(newExportDir);
-            return newExportDir;
-        } catch (e) {
-            if (e.message !== CustomError.SELECT_FOLDER_ABORTED) {
-                log.error("changeExportDirectory failed", e);
-            }
-            throw e;
-        }
-    }
-
     enableContinuousExport() {
     enableContinuousExport() {
         try {
         try {
             if (this.continuousExportEventHandler) {
             if (this.continuousExportEventHandler) {
@@ -732,8 +713,6 @@ class ExportService {
         removedFileUIDs: string[],
         removedFileUIDs: string[],
         isCanceled: CancellationStatus,
         isCanceled: CancellationStatus,
     ): Promise<void> {
     ): Promise<void> {
-        const electron = ensureElectron();
-        const fs = electron.fs;
         try {
         try {
             const exportRecord = await this.getExportRecord(exportDir);
             const exportRecord = await this.getExportRecord(exportDir);
             const fileIDExportNameMap = convertFileIDExportNameObjectToMap(
             const fileIDExportNameMap = convertFileIDExportNameObjectToMap(
@@ -750,71 +729,30 @@ class ExportService {
                     const collectionID = getCollectionIDFromFileUID(fileUID);
                     const collectionID = getCollectionIDFromFileUID(fileUID);
                     const collectionExportName =
                     const collectionExportName =
                         collectionIDExportNameMap.get(collectionID);
                         collectionIDExportNameMap.get(collectionID);
-                    const collectionExportPath = `${exportDir}/${collectionExportName}`;
 
 
                     await this.removeFileExportedRecord(exportDir, fileUID);
                     await this.removeFileExportedRecord(exportDir, fileUID);
                     try {
                     try {
                         if (isLivePhotoExportName(fileExportName)) {
                         if (isLivePhotoExportName(fileExportName)) {
-                            const {
-                                image: imageExportName,
-                                video: videoExportName,
-                            } = parseLivePhotoExportName(fileExportName);
-                            const imageExportPath = `${collectionExportPath}/${imageExportName}`;
-                            log.info(
-                                `moving image file ${imageExportPath} to trash folder`,
+                            const { image, video } =
+                                parseLivePhotoExportName(fileExportName);
+
+                            await moveToTrash(
+                                exportDir,
+                                collectionExportName,
+                                image,
+                            );
+
+                            await moveToTrash(
+                                exportDir,
+                                collectionExportName,
+                                video,
                             );
                             );
-                            if (await fs.exists(imageExportPath)) {
-                                await electron.moveFile(
-                                    imageExportPath,
-                                    await getTrashedFileExportPath(
-                                        exportDir,
-                                        imageExportPath,
-                                    ),
-                                );
-                            }
-
-                            const imageMetadataFileExportPath =
-                                getMetadataFileExportPath(imageExportPath);
-
-                            if (await fs.exists(imageMetadataFileExportPath)) {
-                                await electron.moveFile(
-                                    imageMetadataFileExportPath,
-                                    await getTrashedFileExportPath(
-                                        exportDir,
-                                        imageMetadataFileExportPath,
-                                    ),
-                                );
-                            }
-
-                            const videoExportPath = `${collectionExportPath}/${videoExportName}`;
-                            await moveToTrash(exportDir, videoExportPath);
                         } else {
                         } else {
-                            const fileExportPath = `${collectionExportPath}/${fileExportName}`;
-                            const trashedFilePath =
-                                await getTrashedFileExportPath(
-                                    exportDir,
-                                    fileExportPath,
-                                );
-                            log.info(
-                                `moving file ${fileExportPath} to ${trashedFilePath} trash folder`,
+                            await moveToTrash(
+                                exportDir,
+                                collectionExportName,
+                                fileExportName,
                             );
                             );
-                            if (await fs.exists(fileExportPath)) {
-                                await electron.moveFile(
-                                    fileExportPath,
-                                    trashedFilePath,
-                                );
-                            }
-                            const metadataFileExportPath =
-                                getMetadataFileExportPath(fileExportPath);
-                            if (await fs.exists(metadataFileExportPath)) {
-                                await electron.moveFile(
-                                    metadataFileExportPath,
-                                    await getTrashedFileExportPath(
-                                        exportDir,
-                                        metadataFileExportPath,
-                                    ),
-                                );
-                            }
                         }
                         }
                     } catch (e) {
                     } catch (e) {
                         await this.addFileExportedRecord(
                         await this.addFileExportedRecord(
@@ -824,7 +762,7 @@ class ExportService {
                         );
                         );
                         throw e;
                         throw e;
                     }
                     }
-                    log.info(`trashing file with id ${fileUID} successful`);
+                    log.info(`Moved file id ${fileUID} to Trash`);
                 } catch (e) {
                 } catch (e) {
                     log.error("trashing failed for a file", e);
                     log.error("trashing failed for a file", e);
                     if (
                     if (
@@ -1201,6 +1139,24 @@ export const resumeExportsIfNeeded = async () => {
     }
     }
 };
 };
 
 
+/**
+ * Prompt the user to select a directory and create an export directory in it.
+ *
+ * If the user cancels the selection, return undefined.
+ */
+export const selectAndPrepareExportDirectory = async (): Promise<
+    string | undefined
+> => {
+    const electron = ensureElectron();
+
+    const rootDir = await electron.selectDirectory();
+    if (!rootDir) return undefined;
+
+    const exportDir = `${rootDir}/${exportDirectoryName}`;
+    await electron.fs.mkdirIfNeeded(exportDir);
+    return exportDir;
+};
+
 export const getExportRecordFileUID = (file: EnteFile) =>
 export const getExportRecordFileUID = (file: EnteFile) =>
     `${file.id}_${file.collectionID}_${file.updationTime}`;
     `${file.id}_${file.collectionID}_${file.updationTime}`;
 
 
@@ -1376,37 +1332,14 @@ const getGoogleLikeMetadataFile = (fileExportName: string, file: EnteFile) => {
 export const getMetadataFolderExportPath = (collectionExportPath: string) =>
 export const getMetadataFolderExportPath = (collectionExportPath: string) =>
     `${collectionExportPath}/${exportMetadataDirectoryName}`;
     `${collectionExportPath}/${exportMetadataDirectoryName}`;
 
 
+// if filepath is /home/user/Ente/Export/Collection1/1.jpg
+// then metadata path is /home/user/Ente/Export/Collection1/ENTE_METADATA_FOLDER/1.jpg.json
 const getFileMetadataExportPath = (
 const getFileMetadataExportPath = (
     collectionExportPath: string,
     collectionExportPath: string,
     fileExportName: string,
     fileExportName: string,
 ) =>
 ) =>
     `${collectionExportPath}/${exportMetadataDirectoryName}/${fileExportName}.json`;
     `${collectionExportPath}/${exportMetadataDirectoryName}/${fileExportName}.json`;
 
 
-const getTrashedFileExportPath = async (exportDir: string, path: string) => {
-    const fileRelativePath = path.replace(`${exportDir}/`, "");
-    let trashedFilePath = `${exportDir}/${exportTrashDirectoryName}/${fileRelativePath}`;
-    let count = 1;
-    while (await ensureElectron().fs.exists(trashedFilePath)) {
-        const trashedFilePathParts = splitFilenameAndExtension(trashedFilePath);
-        if (trashedFilePathParts[1]) {
-            trashedFilePath = `${trashedFilePathParts[0]}(${count}).${trashedFilePathParts[1]}`;
-        } else {
-            trashedFilePath = `${trashedFilePathParts[0]}(${count})`;
-        }
-        count++;
-    }
-    return trashedFilePath;
-};
-
-// if filepath is /home/user/Ente/Export/Collection1/1.jpg
-// then metadata path is /home/user/Ente/Export/Collection1/ENTE_METADATA_FOLDER/1.jpg.json
-const getMetadataFileExportPath = (filePath: string) => {
-    // extract filename and collection folder path
-    const filename = filePath.split("/").pop();
-    const collectionExportPath = filePath.replace(`/${filename}`, "");
-    return `${collectionExportPath}/${exportMetadataDirectoryName}/${filename}.json`;
-};
-
 export const getLivePhotoExportName = (
 export const getLivePhotoExportName = (
     imageExportName: string,
     imageExportName: string,
     videoExportName: string,
     videoExportName: string,
@@ -1435,24 +1368,42 @@ const parseLivePhotoExportName = (
 const isExportInProgress = (exportStage: ExportStage) =>
 const isExportInProgress = (exportStage: ExportStage) =>
     exportStage > ExportStage.INIT && exportStage < ExportStage.FINISHED;
     exportStage > ExportStage.INIT && exportStage < ExportStage.FINISHED;
 
 
-const moveToTrash = async (exportDir: string, videoExportPath: string) => {
+/**
+ * Move {@link fileName} in {@link collectionName} to Trash.
+ *
+ * Also move its associated metadata JSON to Trash.
+ *
+ * @param exportDir The root directory on the user's filesystem where we are
+ * exporting to.
+ * */
+const moveToTrash = async (
+    exportDir: string,
+    collectionName: string,
+    fileName: string,
+) => {
     const fs = ensureElectron().fs;
     const fs = ensureElectron().fs;
-    log.info(`moving video file ${videoExportPath} to trash folder`);
-    if (await fs.exists(videoExportPath)) {
-        await electron.moveFile(
-            videoExportPath,
-            await getTrashedFileExportPath(exportDir, videoExportPath),
-        );
+
+    const filePath = `${exportDir}/${collectionName}/${fileName}`;
+    const trashDir = `${exportDir}/${exportTrashDirectoryName}/${collectionName}`;
+    const metadataFileName = `${fileName}.json`;
+    const metadataFilePath = `${exportDir}/${collectionName}/${exportMetadataDirectoryName}/${metadataFileName}`;
+    const metadataTrashDir = `${exportDir}/${exportTrashDirectoryName}/${collectionName}/${exportMetadataDirectoryName}`;
+
+    log.info(`Moving file ${filePath} and its metadata to trash folder`);
+
+    if (await fs.exists(filePath)) {
+        await fs.mkdirIfNeeded(trashDir);
+        const trashFilePath = await safeFileName(trashDir, fileName, fs.exists);
+        await fs.rename(filePath, trashFilePath);
     }
     }
-    const videoMetadataFileExportPath =
-        getMetadataFileExportPath(videoExportPath);
-    if (await fs.exists(videoMetadataFileExportPath)) {
-        await electron.moveFile(
-            videoMetadataFileExportPath,
-            await getTrashedFileExportPath(
-                exportDir,
-                videoMetadataFileExportPath,
-            ),
+
+    if (await fs.exists(metadataFilePath)) {
+        await fs.mkdirIfNeeded(metadataTrashDir);
+        const metadataTrashFilePath = await safeFileName(
+            metadataTrashDir,
+            metadataFileName,
+            fs.exists,
         );
         );
+        await fs.rename(filePath, metadataTrashFilePath);
     }
     }
 };
 };

+ 0 - 1
web/packages/next/types/ipc.ts

@@ -307,7 +307,6 @@ export interface Electron {
     saveFileToDisk: (path: string, contents: string) => Promise<void>;
     saveFileToDisk: (path: string, contents: string) => Promise<void>;
     readTextFile: (path: string) => Promise<string>;
     readTextFile: (path: string) => Promise<string>;
     isFolder: (dirPath: string) => Promise<boolean>;
     isFolder: (dirPath: string) => Promise<boolean>;
-    moveFile: (oldPath: string, newPath: string) => Promise<void>;
 
 
     // - Upload
     // - Upload