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

This commit is contained in:
Manav Rathi 2024-04-14 20:23:04 +05:30 committed by GitHub
commit 2ae119b2ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 108 additions and 210 deletions

View file

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

View file

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

View file

@ -105,6 +105,11 @@ const fsMkdirIfNeeded = (dirPath: string): Promise<void> =>
const fsRename = (oldPath: string, newPath: string): Promise<void> =>
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
// - Conversion
@ -238,14 +243,6 @@ const readTextFile = (path: string): Promise<string> =>
const isFolder = (dirPath: string): Promise<boolean> =>
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
const getPendingUploads = (): Promise<{
@ -359,7 +356,6 @@ contextBridge.exposeInMainWorld("electron", {
saveFileToDisk,
readTextFile,
isFolder,
moveFile,
// - Upload

View file

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

View file

@ -97,20 +97,6 @@ export function getFileExtensionWithDot(filename: string) {
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) {
return new ReadableStream({
async start(controller: ReadableStreamDefaultController) {

View file

@ -18,7 +18,10 @@ import { t } from "i18next";
import isElectron from "is-electron";
import { AppContext } from "pages/_app";
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 { EnteFile } from "types/file";
import { getExportDirectoryDoesNotExistMessage } from "utils/ui";
@ -77,21 +80,6 @@ export default function ExportModal(props: Props) {
void syncExportRecord(exportFolder);
}, [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
// =======================
@ -101,8 +89,9 @@ export default function ExportModal(props: Props) {
appContext.setDialogMessage(
getExportDirectoryDoesNotExistMessage(),
);
throw Error(CustomError.EXPORT_FOLDER_DOES_NOT_EXIST);
return false;
}
return true;
};
const syncExportRecord = async (exportFolder: string): Promise<void> => {
@ -131,42 +120,34 @@ export default function ExportModal(props: Props) {
// =============
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 () => {
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 () => {
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 = () => {

View file

@ -32,7 +32,6 @@ import {
getPersonalFiles,
getUpdatedEXIFFileForDownload,
mergeMetadata,
splitFilenameAndExtension,
} from "utils/file";
import { safeDirectoryName, safeFileName } from "utils/native-fs";
import { getAllLocalCollections } from "../collectionService";
@ -165,24 +164,6 @@ class ExportService {
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() {
try {
if (this.continuousExportEventHandler) {
@ -732,8 +713,6 @@ class ExportService {
removedFileUIDs: string[],
isCanceled: CancellationStatus,
): Promise<void> {
const electron = ensureElectron();
const fs = electron.fs;
try {
const exportRecord = await this.getExportRecord(exportDir);
const fileIDExportNameMap = convertFileIDExportNameObjectToMap(
@ -750,71 +729,30 @@ class ExportService {
const collectionID = getCollectionIDFromFileUID(fileUID);
const collectionExportName =
collectionIDExportNameMap.get(collectionID);
const collectionExportPath = `${exportDir}/${collectionExportName}`;
await this.removeFileExportedRecord(exportDir, fileUID);
try {
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,
);
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);
await moveToTrash(
exportDir,
collectionExportName,
video,
);
} 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) {
await this.addFileExportedRecord(
@ -824,7 +762,7 @@ class ExportService {
);
throw e;
}
log.info(`trashing file with id ${fileUID} successful`);
log.info(`Moved file id ${fileUID} to Trash`);
} catch (e) {
log.error("trashing failed for a file", e);
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) =>
`${file.id}_${file.collectionID}_${file.updationTime}`;
@ -1376,37 +1332,14 @@ const getGoogleLikeMetadataFile = (fileExportName: string, file: EnteFile) => {
export const getMetadataFolderExportPath = (collectionExportPath: string) =>
`${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 = (
collectionExportPath: string,
fileExportName: string,
) =>
`${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 = (
imageExportName: string,
videoExportName: string,
@ -1435,24 +1368,42 @@ const parseLivePhotoExportName = (
const isExportInProgress = (exportStage: ExportStage) =>
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;
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);
}
};

View file

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