[desktop] Fix export related IPC - Part 4/x (#1441)
This commit is contained in:
commit
2ae119b2ec
8 changed files with 108 additions and 210 deletions
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue