[photos-desktop] Fix preload script (#1196)
The preload script is now self contained, and the app when launched tries to open the renderer correctly. It goes on to fail because of an infinite navigation loop, but that's a different issue. Beyond the navigation loop, there is more pending work to do - not all the ipcMain/ipcRenderer communication would currently be working since the objects being passed around (like "File") are not copyable. Still, this PR is a checkpoint for the preload structure.
This commit is contained in:
commit
37dce2bdb5
27 changed files with 1081 additions and 918 deletions
|
@ -1,44 +0,0 @@
|
|||
import { ipcRenderer } from "electron";
|
||||
import { existsSync } from "fs";
|
||||
import { writeStream } from "../services/fs";
|
||||
import { logError } from "../main/log";
|
||||
import { ElectronFile } from "../types";
|
||||
|
||||
export async function runFFmpegCmd(
|
||||
cmd: string[],
|
||||
inputFile: File | ElectronFile,
|
||||
outputFileName: string,
|
||||
dontTimeout?: boolean,
|
||||
) {
|
||||
let inputFilePath = null;
|
||||
let createdTempInputFile = null;
|
||||
try {
|
||||
if (!existsSync(inputFile.path)) {
|
||||
const tempFilePath = await ipcRenderer.invoke(
|
||||
"get-temp-file-path",
|
||||
inputFile.name,
|
||||
);
|
||||
await writeStream(tempFilePath, await inputFile.stream());
|
||||
inputFilePath = tempFilePath;
|
||||
createdTempInputFile = true;
|
||||
} else {
|
||||
inputFilePath = inputFile.path;
|
||||
}
|
||||
const outputFileData = await ipcRenderer.invoke(
|
||||
"run-ffmpeg-cmd",
|
||||
cmd,
|
||||
inputFilePath,
|
||||
outputFileName,
|
||||
dontTimeout,
|
||||
);
|
||||
return new File([outputFileData], outputFileName);
|
||||
} finally {
|
||||
if (createdTempInputFile) {
|
||||
try {
|
||||
await ipcRenderer.invoke("remove-temp-file", inputFilePath);
|
||||
} catch (e) {
|
||||
logError(e, "failed to deleteTempFile");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { getDirFilePaths, getElectronFile } from "../services/fs";
|
||||
|
||||
export async function getDirFiles(dirPath: string) {
|
||||
const files = await getDirFilePaths(dirPath);
|
||||
const electronFiles = await Promise.all(files.map(getElectronFile));
|
||||
return electronFiles;
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
import { ipcRenderer } from "electron/renderer";
|
||||
import { existsSync } from "fs";
|
||||
import { CustomErrors } from "../constants/errors";
|
||||
import { writeStream } from "../services/fs";
|
||||
import { logError } from "../main/log";
|
||||
import { ElectronFile } from "../types";
|
||||
import { isPlatform } from "../utils/common/platform";
|
||||
|
||||
export async function convertToJPEG(
|
||||
fileData: Uint8Array,
|
||||
filename: string,
|
||||
): Promise<Uint8Array> {
|
||||
if (isPlatform("windows")) {
|
||||
throw Error(CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED);
|
||||
}
|
||||
const convertedFileData = await ipcRenderer.invoke(
|
||||
"convert-to-jpeg",
|
||||
fileData,
|
||||
filename,
|
||||
);
|
||||
return convertedFileData;
|
||||
}
|
||||
|
||||
export async function generateImageThumbnail(
|
||||
inputFile: File | ElectronFile,
|
||||
maxDimension: number,
|
||||
maxSize: number,
|
||||
): Promise<Uint8Array> {
|
||||
let inputFilePath = null;
|
||||
let createdTempInputFile = null;
|
||||
try {
|
||||
if (isPlatform("windows")) {
|
||||
throw Error(
|
||||
CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED,
|
||||
);
|
||||
}
|
||||
if (!existsSync(inputFile.path)) {
|
||||
const tempFilePath = await ipcRenderer.invoke(
|
||||
"get-temp-file-path",
|
||||
inputFile.name,
|
||||
);
|
||||
await writeStream(tempFilePath, await inputFile.stream());
|
||||
inputFilePath = tempFilePath;
|
||||
createdTempInputFile = true;
|
||||
} else {
|
||||
inputFilePath = inputFile.path;
|
||||
}
|
||||
const thumbnail = await ipcRenderer.invoke(
|
||||
"generate-image-thumbnail",
|
||||
inputFilePath,
|
||||
maxDimension,
|
||||
maxSize,
|
||||
);
|
||||
return thumbnail;
|
||||
} finally {
|
||||
if (createdTempInputFile) {
|
||||
try {
|
||||
await ipcRenderer.invoke("remove-temp-file", inputFilePath);
|
||||
} catch (e) {
|
||||
logError(e, "failed to deleteTempFile");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
import { ipcRenderer } from "electron";
|
||||
import { safeStorage } from "electron/main";
|
||||
import { logError } from "../main/log";
|
||||
import { safeStorageStore } from "../stores/safeStorage.store";
|
||||
|
||||
export async function setEncryptionKey(encryptionKey: string) {
|
||||
try {
|
||||
const encryptedKey: Buffer = await ipcRenderer.invoke(
|
||||
"safeStorage-encrypt",
|
||||
encryptionKey,
|
||||
);
|
||||
const encryptedKey: Buffer =
|
||||
await safeStorage.encryptString(encryptionKey);
|
||||
const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64");
|
||||
safeStorageStore.set("encryptionKey", b64EncryptedKey);
|
||||
} catch (e) {
|
||||
|
@ -20,10 +18,8 @@ export async function getEncryptionKey(): Promise<string> {
|
|||
try {
|
||||
const b64EncryptedKey = safeStorageStore.get("encryptionKey");
|
||||
if (b64EncryptedKey) {
|
||||
const keyBuffer = new Uint8Array(
|
||||
Buffer.from(b64EncryptedKey, "base64"),
|
||||
);
|
||||
return await ipcRenderer.invoke("safeStorage-decrypt", keyBuffer);
|
||||
const keyBuffer = Buffer.from(b64EncryptedKey, "base64");
|
||||
return await safeStorage.decryptString(keyBuffer);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, "getEncryptionKey failed");
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { ipcRenderer } from "electron";
|
||||
import { logError } from "../main/log";
|
||||
import {
|
||||
getElectronFilesFromGoogleZip,
|
||||
getSavedFilePaths,
|
||||
|
@ -36,53 +34,6 @@ export const getPendingUploads = async () => {
|
|||
};
|
||||
};
|
||||
|
||||
export const showUploadDirsDialog = async () => {
|
||||
try {
|
||||
const filePaths: string[] = await ipcRenderer.invoke(
|
||||
"show-upload-dirs-dialog",
|
||||
);
|
||||
const files = await Promise.all(filePaths.map(getElectronFile));
|
||||
return files;
|
||||
} catch (e) {
|
||||
logError(e, "error while selecting folders");
|
||||
}
|
||||
};
|
||||
|
||||
export const showUploadFilesDialog = async () => {
|
||||
try {
|
||||
const filePaths: string[] = await ipcRenderer.invoke(
|
||||
"show-upload-files-dialog",
|
||||
);
|
||||
const files = await Promise.all(filePaths.map(getElectronFile));
|
||||
return files;
|
||||
} catch (e) {
|
||||
logError(e, "error while selecting files");
|
||||
}
|
||||
};
|
||||
|
||||
export const showUploadZipDialog = async () => {
|
||||
try {
|
||||
const filePaths: string[] = await ipcRenderer.invoke(
|
||||
"show-upload-zip-dialog",
|
||||
);
|
||||
let files: ElectronFile[] = [];
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
files = [
|
||||
...files,
|
||||
...(await getElectronFilesFromGoogleZip(filePath)),
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
zipPaths: filePaths,
|
||||
files,
|
||||
};
|
||||
} catch (e) {
|
||||
logError(e, "error while selecting zips");
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
getElectronFilesFromGoogleZip,
|
||||
setToUploadCollection,
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
import { ipcRenderer } from "electron";
|
||||
import ElectronLog from "electron-log";
|
||||
import path from "path";
|
||||
import { getElectronFile } from "../services/fs";
|
||||
import { getWatchMappings, setWatchMappings } from "../services/watch";
|
||||
import { ElectronFile, WatchMapping } from "../types";
|
||||
import { isMappingPresent } from "../utils/watch";
|
||||
|
||||
export async function addWatchMapping(
|
||||
rootFolderName: string,
|
||||
folderPath: string,
|
||||
uploadStrategy: number,
|
||||
) {
|
||||
ElectronLog.log(`Adding watch mapping: ${folderPath}`);
|
||||
const watchMappings = getWatchMappings();
|
||||
if (isMappingPresent(watchMappings, folderPath)) {
|
||||
throw new Error(`Watch mapping already exists`);
|
||||
}
|
||||
|
||||
await ipcRenderer.invoke("add-watcher", {
|
||||
dir: folderPath,
|
||||
});
|
||||
|
||||
watchMappings.push({
|
||||
rootFolderName,
|
||||
uploadStrategy,
|
||||
folderPath,
|
||||
syncedFiles: [],
|
||||
ignoredFiles: [],
|
||||
});
|
||||
|
||||
setWatchMappings(watchMappings);
|
||||
}
|
||||
|
||||
export async function removeWatchMapping(folderPath: string) {
|
||||
let watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
|
||||
if (!watchMapping) {
|
||||
throw new Error(`Watch mapping does not exist`);
|
||||
}
|
||||
|
||||
await ipcRenderer.invoke("remove-watcher", {
|
||||
dir: watchMapping.folderPath,
|
||||
});
|
||||
|
||||
watchMappings = watchMappings.filter(
|
||||
(mapping) => mapping.folderPath !== watchMapping.folderPath,
|
||||
);
|
||||
|
||||
setWatchMappings(watchMappings);
|
||||
}
|
||||
|
||||
export function updateWatchMappingSyncedFiles(
|
||||
folderPath: string,
|
||||
files: WatchMapping["syncedFiles"],
|
||||
): void {
|
||||
const watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
|
||||
if (!watchMapping) {
|
||||
throw Error(`Watch mapping not found`);
|
||||
}
|
||||
|
||||
watchMapping.syncedFiles = files;
|
||||
setWatchMappings(watchMappings);
|
||||
}
|
||||
|
||||
export function updateWatchMappingIgnoredFiles(
|
||||
folderPath: string,
|
||||
files: WatchMapping["ignoredFiles"],
|
||||
): void {
|
||||
const watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
|
||||
if (!watchMapping) {
|
||||
throw Error(`Watch mapping not found`);
|
||||
}
|
||||
|
||||
watchMapping.ignoredFiles = files;
|
||||
setWatchMappings(watchMappings);
|
||||
}
|
||||
|
||||
export function registerWatcherFunctions(
|
||||
addFile: (file: ElectronFile) => Promise<void>,
|
||||
removeFile: (path: string) => Promise<void>,
|
||||
removeFolder: (folderPath: string) => Promise<void>,
|
||||
) {
|
||||
ipcRenderer.removeAllListeners("watch-add");
|
||||
ipcRenderer.removeAllListeners("watch-change");
|
||||
ipcRenderer.removeAllListeners("watch-unlink-dir");
|
||||
ipcRenderer.on("watch-add", async (_, filePath: string) => {
|
||||
filePath = filePath.split(path.sep).join(path.posix.sep);
|
||||
|
||||
await addFile(await getElectronFile(filePath));
|
||||
});
|
||||
ipcRenderer.on("watch-unlink", async (_, filePath: string) => {
|
||||
filePath = filePath.split(path.sep).join(path.posix.sep);
|
||||
|
||||
await removeFile(filePath);
|
||||
});
|
||||
ipcRenderer.on("watch-unlink-dir", async (_, folderPath: string) => {
|
||||
folderPath = folderPath.split(path.sep).join(path.posix.sep);
|
||||
await removeFolder(folderPath);
|
||||
});
|
||||
}
|
||||
|
||||
export { getWatchMappings } from "../services/watch";
|
|
@ -1,3 +1,17 @@
|
|||
/**
|
||||
* [Note: Custom errors across Electron/Renderer boundary]
|
||||
*
|
||||
* We need to use the `message` field to disambiguate between errors thrown by
|
||||
* the main process when invoked from the renderer process. This is because:
|
||||
*
|
||||
* > Errors thrown throw `handle` in the main process are not transparent as
|
||||
* > they are serialized and only the `message` property from the original error
|
||||
* > is provided to the renderer process.
|
||||
* >
|
||||
* > - https://www.electronjs.org/docs/latest/tutorial/ipc
|
||||
* >
|
||||
* > Ref: https://github.com/electron/electron/issues/24427
|
||||
*/
|
||||
export const CustomErrors = {
|
||||
WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED:
|
||||
"Windows native image processing is not supported",
|
||||
|
|
|
@ -15,12 +15,11 @@ import { existsSync } from "node:fs";
|
|||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { isDev } from "./main/general";
|
||||
import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc";
|
||||
import { logErrorSentry, setupLogging } from "./main/log";
|
||||
import { initWatcher } from "./services/chokidar";
|
||||
import { addAllowOriginHeader } from "./utils/cors";
|
||||
import { createWindow } from "./utils/createWindow";
|
||||
|
||||
import setupIpcComs from "./utils/ipcComms";
|
||||
import {
|
||||
handleDockIconHideOnAutoLaunch,
|
||||
handleDownloads,
|
||||
|
@ -167,11 +166,12 @@ const main = () => {
|
|||
app.on("ready", async () => {
|
||||
logSystemInfo();
|
||||
mainWindow = await createWindow();
|
||||
const tray = setupTrayItem(mainWindow);
|
||||
const watcher = initWatcher(mainWindow);
|
||||
setupTrayItem(mainWindow);
|
||||
setupMacWindowOnDockIconClick();
|
||||
setupMainMenu(mainWindow);
|
||||
setupIpcComs(tray, mainWindow, watcher);
|
||||
attachIPCHandlers();
|
||||
attachFSWatchIPCHandlers(watcher);
|
||||
await handleUpdates(mainWindow);
|
||||
handleDownloads(mainWindow);
|
||||
handleExternalLinks(mainWindow);
|
||||
|
|
54
desktop/src/main/dialogs.ts
Normal file
54
desktop/src/main/dialogs.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { dialog } from "electron/main";
|
||||
import * as path from "node:path";
|
||||
import { getDirFilePaths, getElectronFile } from "../services/fs";
|
||||
import { getElectronFilesFromGoogleZip } from "../services/upload";
|
||||
import type { ElectronFile } from "../types";
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -6,18 +6,70 @@
|
|||
* context of the main process, and can import other files from `src/`.
|
||||
*/
|
||||
|
||||
import type { FSWatcher } from "chokidar";
|
||||
import { ipcMain } from "electron/main";
|
||||
import { clearElectronStore } from "../api/electronStore";
|
||||
import { getEncryptionKey, setEncryptionKey } from "../api/safeStorage";
|
||||
import {
|
||||
getElectronFilesFromGoogleZip,
|
||||
getPendingUploads,
|
||||
setToUploadCollection,
|
||||
setToUploadFiles,
|
||||
} from "../api/upload";
|
||||
import {
|
||||
appVersion,
|
||||
muteUpdateNotification,
|
||||
skipAppUpdate,
|
||||
updateAndRestart,
|
||||
} from "../services/appUpdater";
|
||||
import { checkExistsAndCreateDir, fsExists } from "./fs";
|
||||
import {
|
||||
computeImageEmbedding,
|
||||
computeTextEmbedding,
|
||||
} from "../services/clipService";
|
||||
import { runFFmpegCmd } from "../services/ffmpeg";
|
||||
import { getDirFiles } from "../services/fs";
|
||||
import {
|
||||
convertToJPEG,
|
||||
generateImageThumbnail,
|
||||
} from "../services/imageProcessor";
|
||||
import {
|
||||
addWatchMapping,
|
||||
getWatchMappings,
|
||||
removeWatchMapping,
|
||||
updateWatchMappingIgnoredFiles,
|
||||
updateWatchMappingSyncedFiles,
|
||||
} from "../services/watch";
|
||||
import type {
|
||||
ElectronFile,
|
||||
FILE_PATH_TYPE,
|
||||
Model,
|
||||
WatchMapping,
|
||||
} from "../types";
|
||||
import {
|
||||
selectDirectory,
|
||||
showUploadDirsDialog,
|
||||
showUploadFilesDialog,
|
||||
showUploadZipDialog,
|
||||
} from "./dialogs";
|
||||
import {
|
||||
checkExistsAndCreateDir,
|
||||
deleteFile,
|
||||
deleteFolder,
|
||||
fsExists,
|
||||
isFolder,
|
||||
moveFile,
|
||||
readTextFile,
|
||||
rename,
|
||||
saveFileToDisk,
|
||||
saveStreamToDisk,
|
||||
} from "./fs";
|
||||
import { openDirectory, openLogDirectory } from "./general";
|
||||
import { logToDisk } from "./log";
|
||||
|
||||
/**
|
||||
* Listen for IPC events sent/invoked by the renderer process, and route them to
|
||||
* their correct handlers.
|
||||
*/
|
||||
export const attachIPCHandlers = () => {
|
||||
// Notes:
|
||||
//
|
||||
|
@ -46,24 +98,169 @@ export const attachIPCHandlers = () => {
|
|||
// See: [Note: Catching exception during .send/.on]
|
||||
ipcMain.on("logToDisk", (_, message) => logToDisk(message));
|
||||
|
||||
ipcMain.on("clear-electron-store", (_) => {
|
||||
clearElectronStore();
|
||||
});
|
||||
|
||||
ipcMain.handle("setEncryptionKey", (_, encryptionKey) =>
|
||||
setEncryptionKey(encryptionKey),
|
||||
);
|
||||
|
||||
ipcMain.handle("getEncryptionKey", (_) => getEncryptionKey());
|
||||
|
||||
// - App update
|
||||
|
||||
ipcMain.on("update-and-restart", (_) => updateAndRestart());
|
||||
|
||||
ipcMain.on("skip-app-update", (_, version) => skipAppUpdate(version));
|
||||
|
||||
ipcMain.on("mute-update-notification", (_, version) =>
|
||||
muteUpdateNotification(version),
|
||||
);
|
||||
|
||||
// - Conversion
|
||||
|
||||
ipcMain.handle("convertToJPEG", (_, fileData, filename) =>
|
||||
convertToJPEG(fileData, filename),
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"generateImageThumbnail",
|
||||
(_, inputFile, maxDimension, maxSize) =>
|
||||
generateImageThumbnail(inputFile, maxDimension, maxSize),
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"runFFmpegCmd",
|
||||
(
|
||||
_,
|
||||
cmd: string[],
|
||||
inputFile: File | ElectronFile,
|
||||
outputFileName: string,
|
||||
dontTimeout?: boolean,
|
||||
) => runFFmpegCmd(cmd, inputFile, outputFileName, dontTimeout),
|
||||
);
|
||||
|
||||
// - ML
|
||||
|
||||
ipcMain.handle(
|
||||
"computeImageEmbedding",
|
||||
(_, model: Model, imageData: Uint8Array) =>
|
||||
computeImageEmbedding(model, imageData),
|
||||
);
|
||||
|
||||
ipcMain.handle("computeTextEmbedding", (_, model: Model, text: string) =>
|
||||
computeTextEmbedding(model, text),
|
||||
);
|
||||
|
||||
// - File selection
|
||||
|
||||
ipcMain.handle("selectDirectory", (_) => selectDirectory());
|
||||
|
||||
ipcMain.handle("showUploadFilesDialog", (_) => showUploadFilesDialog());
|
||||
|
||||
ipcMain.handle("showUploadDirsDialog", (_) => showUploadDirsDialog());
|
||||
|
||||
ipcMain.handle("showUploadZipDialog", (_) => showUploadZipDialog());
|
||||
|
||||
// - FS
|
||||
|
||||
ipcMain.handle("fsExists", (_, path) => fsExists(path));
|
||||
|
||||
// - FS Legacy
|
||||
|
||||
ipcMain.handle("checkExistsAndCreateDir", (_, dirPath) =>
|
||||
checkExistsAndCreateDir(dirPath),
|
||||
);
|
||||
|
||||
ipcMain.on("clear-electron-store", (_) => {
|
||||
clearElectronStore();
|
||||
});
|
||||
ipcMain.handle(
|
||||
"saveStreamToDisk",
|
||||
(_, path: string, fileStream: ReadableStream<any>) =>
|
||||
saveStreamToDisk(path, fileStream),
|
||||
);
|
||||
|
||||
ipcMain.on("update-and-restart", (_) => {
|
||||
updateAndRestart();
|
||||
});
|
||||
ipcMain.on("skip-app-update", (_, version) => {
|
||||
skipAppUpdate(version);
|
||||
});
|
||||
ipcMain.handle("saveFileToDisk", (_, path: string, file: any) =>
|
||||
saveFileToDisk(path, file),
|
||||
);
|
||||
|
||||
ipcMain.on("mute-update-notification", (_, version) => {
|
||||
muteUpdateNotification(version);
|
||||
});
|
||||
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),
|
||||
);
|
||||
|
||||
// - Upload
|
||||
|
||||
ipcMain.handle("getPendingUploads", (_) => getPendingUploads());
|
||||
|
||||
ipcMain.handle(
|
||||
"setToUploadFiles",
|
||||
(_, type: FILE_PATH_TYPE, filePaths: string[]) =>
|
||||
setToUploadFiles(type, filePaths),
|
||||
);
|
||||
|
||||
ipcMain.handle("getElectronFilesFromGoogleZip", (_, filePath: string) =>
|
||||
getElectronFilesFromGoogleZip(filePath),
|
||||
);
|
||||
|
||||
ipcMain.handle("setToUploadCollection", (_, collectionName: string) =>
|
||||
setToUploadCollection(collectionName),
|
||||
);
|
||||
|
||||
ipcMain.handle("getDirFiles", (_, dirPath: string) => getDirFiles(dirPath));
|
||||
};
|
||||
|
||||
/**
|
||||
* Sibling of {@link attachIPCHandlers} that attaches handlers specific to the
|
||||
* watch folder functionality.
|
||||
*
|
||||
* It gets passed a {@link FSWatcher} instance which it can then forward to the
|
||||
* actual handlers.
|
||||
*/
|
||||
export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => {
|
||||
// - Watch
|
||||
|
||||
ipcMain.handle(
|
||||
"addWatchMapping",
|
||||
(
|
||||
_,
|
||||
collectionName: string,
|
||||
folderPath: string,
|
||||
uploadStrategy: number,
|
||||
) =>
|
||||
addWatchMapping(
|
||||
watcher,
|
||||
collectionName,
|
||||
folderPath,
|
||||
uploadStrategy,
|
||||
),
|
||||
);
|
||||
|
||||
ipcMain.handle("removeWatchMapping", (_, folderPath: string) =>
|
||||
removeWatchMapping(watcher, folderPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("getWatchMappings", (_) => getWatchMappings());
|
||||
|
||||
ipcMain.handle(
|
||||
"updateWatchMappingSyncedFiles",
|
||||
(_, folderPath: string, files: WatchMapping["syncedFiles"]) =>
|
||||
updateWatchMappingSyncedFiles(folderPath, files),
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"updateWatchMappingIgnoredFiles",
|
||||
(_, folderPath: string, files: WatchMapping["ignoredFiles"]) =>
|
||||
updateWatchMappingIgnoredFiles(folderPath, files),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -27,34 +27,11 @@
|
|||
*/
|
||||
|
||||
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 { runFFmpegCmd } from "./api/ffmpeg";
|
||||
import { getDirFiles } from "./api/fs";
|
||||
import { convertToJPEG, generateImageThumbnail } from "./api/imageProcessor";
|
||||
import { getEncryptionKey, setEncryptionKey } from "./api/safeStorage";
|
||||
import {
|
||||
getElectronFilesFromGoogleZip,
|
||||
getPendingUploads,
|
||||
setToUploadCollection,
|
||||
setToUploadFiles,
|
||||
showUploadDirsDialog,
|
||||
showUploadFilesDialog,
|
||||
showUploadZipDialog,
|
||||
} from "./api/upload";
|
||||
import {
|
||||
addWatchMapping,
|
||||
getWatchMappings,
|
||||
registerWatcherFunctions,
|
||||
removeWatchMapping,
|
||||
updateWatchMappingIgnoredFiles,
|
||||
updateWatchMappingSyncedFiles,
|
||||
} from "./api/watch";
|
||||
import { logErrorSentry, setupLogging } from "./main/log";
|
||||
import type { ElectronFile } from "./types";
|
||||
|
||||
setupLogging();
|
||||
// TODO (MR): Uncomment and FIXME once preload is getting loaded.
|
||||
// import { setupLogging } from "./main/log";
|
||||
// setupLogging();
|
||||
|
||||
// - General
|
||||
|
||||
|
@ -93,8 +70,24 @@ const fsExists = (path: string): Promise<boolean> =>
|
|||
|
||||
// - AUDIT below this
|
||||
|
||||
const checkExistsAndCreateDir = (dirPath: string): Promise<void> =>
|
||||
ipcRenderer.invoke("checkExistsAndCreateDir", dirPath);
|
||||
const registerForegroundEventListener = (onForeground: () => void) => {
|
||||
ipcRenderer.removeAllListeners("app-in-foreground");
|
||||
ipcRenderer.on("app-in-foreground", () => {
|
||||
onForeground();
|
||||
});
|
||||
};
|
||||
|
||||
const clearElectronStore = () => {
|
||||
ipcRenderer.send("clear-electron-store");
|
||||
};
|
||||
|
||||
const setEncryptionKey = (encryptionKey: string): Promise<void> =>
|
||||
ipcRenderer.invoke("setEncryptionKey", encryptionKey);
|
||||
|
||||
const getEncryptionKey = (): Promise<string> =>
|
||||
ipcRenderer.invoke("getEncryptionKey");
|
||||
|
||||
// - App update
|
||||
|
||||
/* preload: duplicated */
|
||||
interface AppUpdateInfo {
|
||||
|
@ -111,19 +104,6 @@ const registerUpdateEventListener = (
|
|||
});
|
||||
};
|
||||
|
||||
const registerForegroundEventListener = (onForeground: () => void) => {
|
||||
ipcRenderer.removeAllListeners("app-in-foreground");
|
||||
ipcRenderer.on("app-in-foreground", () => {
|
||||
onForeground();
|
||||
});
|
||||
};
|
||||
|
||||
const clearElectronStore = () => {
|
||||
ipcRenderer.send("clear-electron-store");
|
||||
};
|
||||
|
||||
// - App update
|
||||
|
||||
const updateAndRestart = () => {
|
||||
ipcRenderer.send("update-and-restart");
|
||||
};
|
||||
|
@ -136,278 +116,203 @@ const muteUpdateNotification = (version: string) => {
|
|||
ipcRenderer.send("mute-update-notification", version);
|
||||
};
|
||||
|
||||
// - Conversion
|
||||
|
||||
// - FIXME below this
|
||||
const convertToJPEG = (
|
||||
fileData: Uint8Array,
|
||||
filename: string,
|
||||
): Promise<Uint8Array> =>
|
||||
ipcRenderer.invoke("convertToJPEG", fileData, filename);
|
||||
|
||||
/* preload: duplicated logError */
|
||||
const logError = (error: Error, message: string, info?: any) => {
|
||||
logErrorSentry(error, message, info);
|
||||
};
|
||||
const generateImageThumbnail = (
|
||||
inputFile: File | ElectronFile,
|
||||
maxDimension: number,
|
||||
maxSize: number,
|
||||
): Promise<Uint8Array> =>
|
||||
ipcRenderer.invoke(
|
||||
"generateImageThumbnail",
|
||||
inputFile,
|
||||
maxDimension,
|
||||
maxSize,
|
||||
);
|
||||
|
||||
/* 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));
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// - 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 runFFmpegCmd = (
|
||||
cmd: string[],
|
||||
inputFile: File | ElectronFile,
|
||||
outputFileName: string,
|
||||
dontTimeout?: boolean,
|
||||
): Promise<File> =>
|
||||
ipcRenderer.invoke(
|
||||
"runFFmpegCmd",
|
||||
cmd,
|
||||
inputFile,
|
||||
outputFileName,
|
||||
dontTimeout,
|
||||
);
|
||||
|
||||
// - ML
|
||||
|
||||
/* preload: duplicated Model */
|
||||
export enum Model {
|
||||
enum Model {
|
||||
GGML_CLIP = "ggml-clip",
|
||||
ONNX_CLIP = "onnx-clip",
|
||||
}
|
||||
|
||||
const computeImageEmbedding = async (
|
||||
const computeImageEmbedding = (
|
||||
model: Model,
|
||||
imageData: Uint8Array,
|
||||
): Promise<Float32Array> => {
|
||||
let tempInputFilePath = null;
|
||||
try {
|
||||
tempInputFilePath = await ipcRenderer.invoke("get-temp-file-path", "");
|
||||
const imageStream = new Response(imageData.buffer).body;
|
||||
await writeStream(tempInputFilePath, imageStream);
|
||||
const embedding = await ipcRenderer.invoke(
|
||||
"compute-image-embedding",
|
||||
model,
|
||||
tempInputFilePath,
|
||||
);
|
||||
return embedding;
|
||||
} catch (err) {
|
||||
if (isExecError(err)) {
|
||||
const parsedExecError = parseExecError(err);
|
||||
throw Error(parsedExecError);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
if (tempInputFilePath) {
|
||||
await ipcRenderer.invoke("remove-temp-file", tempInputFilePath);
|
||||
}
|
||||
}
|
||||
};
|
||||
): Promise<Float32Array> =>
|
||||
ipcRenderer.invoke("computeImageEmbedding", model, imageData);
|
||||
|
||||
export async function computeTextEmbedding(
|
||||
const computeTextEmbedding = (
|
||||
model: Model,
|
||||
text: string,
|
||||
): Promise<Float32Array> {
|
||||
try {
|
||||
const embedding = await ipcRenderer.invoke(
|
||||
"compute-text-embedding",
|
||||
model,
|
||||
text,
|
||||
);
|
||||
return embedding;
|
||||
} catch (err) {
|
||||
if (isExecError(err)) {
|
||||
const parsedExecError = parseExecError(err);
|
||||
throw Error(parsedExecError);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
): Promise<Float32Array> =>
|
||||
ipcRenderer.invoke("computeTextEmbedding", model, text);
|
||||
|
||||
// - File selection
|
||||
|
||||
// TODO: Deprecated - use dialogs on the renderer process itself
|
||||
|
||||
const selectDirectory = (): Promise<string> =>
|
||||
ipcRenderer.invoke("selectDirectory");
|
||||
|
||||
const showUploadFilesDialog = (): Promise<ElectronFile[]> =>
|
||||
ipcRenderer.invoke("showUploadFilesDialog");
|
||||
|
||||
const showUploadDirsDialog = (): Promise<ElectronFile[]> =>
|
||||
ipcRenderer.invoke("showUploadDirsDialog");
|
||||
|
||||
const showUploadZipDialog = (): Promise<{
|
||||
zipPaths: string[];
|
||||
files: ElectronFile[];
|
||||
}> => ipcRenderer.invoke("showUploadZipDialog");
|
||||
|
||||
// - Watch
|
||||
|
||||
const registerWatcherFunctions = (
|
||||
addFile: (file: ElectronFile) => Promise<void>,
|
||||
removeFile: (path: string) => Promise<void>,
|
||||
removeFolder: (folderPath: string) => Promise<void>,
|
||||
) => {
|
||||
ipcRenderer.removeAllListeners("watch-add");
|
||||
ipcRenderer.removeAllListeners("watch-unlink");
|
||||
ipcRenderer.removeAllListeners("watch-unlink-dir");
|
||||
ipcRenderer.on("watch-add", (_, file: ElectronFile) => addFile(file));
|
||||
ipcRenderer.on("watch-unlink", (_, filePath: string) =>
|
||||
removeFile(filePath),
|
||||
);
|
||||
ipcRenderer.on("watch-unlink-dir", (_, folderPath: string) =>
|
||||
removeFolder(folderPath),
|
||||
);
|
||||
};
|
||||
|
||||
const addWatchMapping = (
|
||||
collectionName: string,
|
||||
folderPath: string,
|
||||
uploadStrategy: number,
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke(
|
||||
"addWatchMapping",
|
||||
collectionName,
|
||||
folderPath,
|
||||
uploadStrategy,
|
||||
);
|
||||
|
||||
const removeWatchMapping = (folderPath: string): Promise<void> =>
|
||||
ipcRenderer.invoke("removeWatchMapping", folderPath);
|
||||
|
||||
/* preload: duplicated WatchMappingSyncedFile */
|
||||
interface WatchMappingSyncedFile {
|
||||
path: string;
|
||||
uploadedFileID: number;
|
||||
collectionID: number;
|
||||
}
|
||||
|
||||
// -
|
||||
/* preload: duplicated WatchMapping */
|
||||
interface WatchMapping {
|
||||
rootFolderName: string;
|
||||
uploadStrategy: number;
|
||||
folderPath: string;
|
||||
syncedFiles: WatchMappingSyncedFile[];
|
||||
ignoredFiles: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* [Note: Custom errors across Electron/Renderer boundary]
|
||||
*
|
||||
* We need to use the `message` field to disambiguate between errors thrown by
|
||||
* the main process when invoked from the renderer process. This is because:
|
||||
*
|
||||
* > Errors thrown throw `handle` in the main process are not transparent as
|
||||
* > they are serialized and only the `message` property from the original error
|
||||
* > is provided to the renderer process.
|
||||
* >
|
||||
* > - https://www.electronjs.org/docs/latest/tutorial/ipc
|
||||
* >
|
||||
* > Ref: https://github.com/electron/electron/issues/24427
|
||||
*/
|
||||
/* preload: duplicated CustomErrors */
|
||||
const CustomErrorsP = {
|
||||
WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED:
|
||||
"Windows native image processing is not supported",
|
||||
INVALID_OS: (os: string) => `Invalid OS - ${os}`,
|
||||
WAIT_TIME_EXCEEDED: "Wait time exceeded",
|
||||
UNSUPPORTED_PLATFORM: (platform: string, arch: string) =>
|
||||
`Unsupported platform - ${platform} ${arch}`,
|
||||
MODEL_DOWNLOAD_PENDING:
|
||||
"Model download pending, skipping clip search request",
|
||||
INVALID_FILE_PATH: "Invalid file path",
|
||||
INVALID_CLIP_MODEL: (model: string) => `Invalid Clip model - ${model}`,
|
||||
};
|
||||
const getWatchMappings = (): Promise<WatchMapping[]> =>
|
||||
ipcRenderer.invoke("getWatchMappings");
|
||||
|
||||
const isExecError = (err: any) => {
|
||||
return err.message.includes("Command failed:");
|
||||
};
|
||||
const updateWatchMappingSyncedFiles = (
|
||||
folderPath: string,
|
||||
files: WatchMapping["syncedFiles"],
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke("updateWatchMappingSyncedFiles", folderPath, files);
|
||||
|
||||
const parseExecError = (err: any) => {
|
||||
const errMessage = err.message;
|
||||
if (errMessage.includes("Bad CPU type in executable")) {
|
||||
return CustomErrorsP.UNSUPPORTED_PLATFORM(
|
||||
process.platform,
|
||||
process.arch,
|
||||
);
|
||||
} else {
|
||||
return errMessage;
|
||||
}
|
||||
};
|
||||
const updateWatchMappingIgnoredFiles = (
|
||||
folderPath: string,
|
||||
files: WatchMapping["ignoredFiles"],
|
||||
): Promise<void> =>
|
||||
ipcRenderer.invoke("updateWatchMappingIgnoredFiles", folderPath, files);
|
||||
|
||||
// - General
|
||||
// - FS Legacy
|
||||
|
||||
const selectDirectory = async (): Promise<string> => {
|
||||
try {
|
||||
return await ipcRenderer.invoke("select-dir");
|
||||
} catch (e) {
|
||||
logError(e, "error while selecting root directory");
|
||||
}
|
||||
};
|
||||
const checkExistsAndCreateDir = (dirPath: string): Promise<void> =>
|
||||
ipcRenderer.invoke("checkExistsAndCreateDir", dirPath);
|
||||
|
||||
// -
|
||||
const saveStreamToDisk = (
|
||||
path: string,
|
||||
fileStream: ReadableStream<any>,
|
||||
): Promise<void> => ipcRenderer.invoke("saveStreamToDisk", path, fileStream);
|
||||
|
||||
const saveFileToDisk = (path: string, file: any): Promise<void> =>
|
||||
ipcRenderer.invoke("saveFileToDisk", path, file);
|
||||
|
||||
const readTextFile = (path: string): Promise<string> =>
|
||||
ipcRenderer.invoke("readTextFile", path);
|
||||
|
||||
const isFolder = (dirPath: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke("isFolder", dirPath);
|
||||
|
||||
const moveFile = (oldPath: string, newPath: string): Promise<void> =>
|
||||
ipcRenderer.invoke("moveFile", oldPath, newPath);
|
||||
|
||||
const deleteFolder = (path: string): Promise<void> =>
|
||||
ipcRenderer.invoke("deleteFolder", path);
|
||||
|
||||
const deleteFile = (path: string): Promise<void> =>
|
||||
ipcRenderer.invoke("deleteFile", path);
|
||||
|
||||
const rename = (oldPath: string, newPath: string): Promise<void> =>
|
||||
ipcRenderer.invoke("rename", oldPath, newPath);
|
||||
|
||||
// - Upload
|
||||
|
||||
const getPendingUploads = (): Promise<{
|
||||
files: ElectronFile[];
|
||||
collectionName: string;
|
||||
type: string;
|
||||
}> => ipcRenderer.invoke("getPendingUploads");
|
||||
|
||||
/* preload: duplicated FILE_PATH_TYPE */
|
||||
enum FILE_PATH_TYPE {
|
||||
FILES = "files",
|
||||
ZIPS = "zips",
|
||||
}
|
||||
|
||||
const setToUploadFiles = (
|
||||
type: FILE_PATH_TYPE,
|
||||
filePaths: string[],
|
||||
): Promise<void> => ipcRenderer.invoke("setToUploadFiles", type, filePaths);
|
||||
|
||||
const getElectronFilesFromGoogleZip = (
|
||||
filePath: string,
|
||||
): Promise<ElectronFile[]> =>
|
||||
ipcRenderer.invoke("getElectronFilesFromGoogleZip", filePath);
|
||||
|
||||
const setToUploadCollection = (collectionName: string): Promise<void> =>
|
||||
ipcRenderer.invoke("setToUploadCollection", collectionName);
|
||||
|
||||
const getDirFiles = (dirPath: string): Promise<ElectronFile[]> =>
|
||||
ipcRenderer.invoke("getDirFiles", dirPath);
|
||||
|
||||
// These objects exposed here will become available to the JS code in our
|
||||
// renderer (the web/ code) as `window.ElectronAPIs.*`
|
||||
|
@ -442,13 +347,15 @@ const selectDirectory = async (): Promise<string> => {
|
|||
// The copy itself is relatively fast, but the problem with transfering large
|
||||
// amounts of data is potentially running out of memory during the copy.
|
||||
contextBridge.exposeInMainWorld("ElectronAPIs", {
|
||||
// General
|
||||
// - General
|
||||
appVersion,
|
||||
openDirectory,
|
||||
registerForegroundEventListener,
|
||||
clearElectronStore,
|
||||
getEncryptionKey,
|
||||
setEncryptionKey,
|
||||
|
||||
// Logging
|
||||
// - Logging
|
||||
openLogDirectory,
|
||||
logToDisk,
|
||||
|
||||
|
@ -458,6 +365,29 @@ contextBridge.exposeInMainWorld("ElectronAPIs", {
|
|||
muteUpdateNotification,
|
||||
registerUpdateEventListener,
|
||||
|
||||
// - Conversion
|
||||
convertToJPEG,
|
||||
generateImageThumbnail,
|
||||
runFFmpegCmd,
|
||||
|
||||
// - ML
|
||||
computeImageEmbedding,
|
||||
computeTextEmbedding,
|
||||
|
||||
// - File selection
|
||||
selectDirectory,
|
||||
showUploadFilesDialog,
|
||||
showUploadDirsDialog,
|
||||
showUploadZipDialog,
|
||||
|
||||
// - Watch
|
||||
registerWatcherFunctions,
|
||||
addWatchMapping,
|
||||
removeWatchMapping,
|
||||
getWatchMappings,
|
||||
updateWatchMappingSyncedFiles,
|
||||
updateWatchMappingIgnoredFiles,
|
||||
|
||||
// - FS
|
||||
fs: {
|
||||
exists: fsExists,
|
||||
|
@ -466,40 +396,20 @@ contextBridge.exposeInMainWorld("ElectronAPIs", {
|
|||
// - FS legacy
|
||||
// TODO: Move these into fs + document + rename if needed
|
||||
checkExistsAndCreateDir,
|
||||
|
||||
// - Export
|
||||
saveStreamToDisk,
|
||||
saveFileToDisk,
|
||||
|
||||
selectDirectory,
|
||||
readTextFile,
|
||||
showUploadFilesDialog,
|
||||
showUploadDirsDialog,
|
||||
getPendingUploads,
|
||||
setToUploadFiles,
|
||||
showUploadZipDialog,
|
||||
getElectronFilesFromGoogleZip,
|
||||
setToUploadCollection,
|
||||
getEncryptionKey,
|
||||
setEncryptionKey,
|
||||
getDirFiles,
|
||||
getWatchMappings,
|
||||
addWatchMapping,
|
||||
removeWatchMapping,
|
||||
registerWatcherFunctions,
|
||||
isFolder,
|
||||
updateWatchMappingSyncedFiles,
|
||||
updateWatchMappingIgnoredFiles,
|
||||
convertToJPEG,
|
||||
|
||||
runFFmpegCmd,
|
||||
generateImageThumbnail,
|
||||
moveFile,
|
||||
deleteFolder,
|
||||
rename,
|
||||
deleteFile,
|
||||
rename,
|
||||
|
||||
// - ML
|
||||
computeImageEmbedding,
|
||||
computeTextEmbedding,
|
||||
// - Upload
|
||||
|
||||
getPendingUploads,
|
||||
setToUploadFiles,
|
||||
getElectronFilesFromGoogleZip,
|
||||
setToUploadCollection,
|
||||
getDirFiles,
|
||||
});
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
import chokidar from "chokidar";
|
||||
import { BrowserWindow } from "electron";
|
||||
import { getWatchMappings } from "../api/watch";
|
||||
import * as path from "path";
|
||||
import { logError } from "../main/log";
|
||||
import { getWatchMappings } from "../services/watch";
|
||||
import { getElectronFile } from "./fs";
|
||||
|
||||
/**
|
||||
* Convert a file system {@link filePath} that uses the local system specific
|
||||
* path separators into a path that uses POSIX file separators.
|
||||
*/
|
||||
const normalizeToPOSIX = (filePath: string) =>
|
||||
filePath.split(path.sep).join(path.posix.sep);
|
||||
|
||||
export function initWatcher(mainWindow: BrowserWindow) {
|
||||
const mappings = getWatchMappings();
|
||||
|
@ -13,17 +22,20 @@ export function initWatcher(mainWindow: BrowserWindow) {
|
|||
awaitWriteFinish: true,
|
||||
});
|
||||
watcher
|
||||
.on("add", (path) => {
|
||||
mainWindow.webContents.send("watch-add", path);
|
||||
})
|
||||
.on("change", (path) => {
|
||||
mainWindow.webContents.send("watch-change", path);
|
||||
.on("add", async (path) => {
|
||||
mainWindow.webContents.send(
|
||||
"watch-add",
|
||||
await getElectronFile(normalizeToPOSIX(path)),
|
||||
);
|
||||
})
|
||||
.on("unlink", (path) => {
|
||||
mainWindow.webContents.send("watch-unlink", path);
|
||||
mainWindow.webContents.send("watch-unlink", normalizeToPOSIX(path));
|
||||
})
|
||||
.on("unlinkDir", (path) => {
|
||||
mainWindow.webContents.send("watch-unlink-dir", path);
|
||||
mainWindow.webContents.send(
|
||||
"watch-unlink-dir",
|
||||
normalizeToPOSIX(path),
|
||||
);
|
||||
})
|
||||
.on("error", (error) => {
|
||||
logError(error, "error while watching files");
|
||||
|
|
|
@ -5,12 +5,14 @@ import * as fs from "node:fs/promises";
|
|||
import * as path from "node:path";
|
||||
import util from "util";
|
||||
import { CustomErrors } from "../constants/errors";
|
||||
import { writeStream } from "../main/fs";
|
||||
import { isDev } from "../main/general";
|
||||
import { logErrorSentry } from "../main/log";
|
||||
import { Model } from "../types";
|
||||
import Tokenizer from "../utils/clip-bpe-ts/mod";
|
||||
import { isDev } from "../main/general";
|
||||
import { getPlatform } from "../utils/common/platform";
|
||||
import { writeStream } from "./fs";
|
||||
import { logErrorSentry } from "../main/log";
|
||||
import { generateTempFilePath } from "../utils/temp";
|
||||
import { deleteTempFile } from "./ffmpeg";
|
||||
const shellescape = require("any-shell-escape");
|
||||
const execAsync = util.promisify(require("child_process").exec);
|
||||
const jpeg = require("jpeg-js");
|
||||
|
@ -198,7 +200,51 @@ function getTokenizer() {
|
|||
return tokenizer;
|
||||
}
|
||||
|
||||
export async function computeImageEmbedding(
|
||||
export const computeImageEmbedding = async (
|
||||
model: Model,
|
||||
imageData: Uint8Array,
|
||||
): Promise<Float32Array> => {
|
||||
let tempInputFilePath = null;
|
||||
try {
|
||||
tempInputFilePath = await generateTempFilePath("");
|
||||
const imageStream = new Response(imageData.buffer).body;
|
||||
await writeStream(tempInputFilePath, imageStream);
|
||||
const embedding = await computeImageEmbedding_(
|
||||
model,
|
||||
tempInputFilePath,
|
||||
);
|
||||
return embedding;
|
||||
} catch (err) {
|
||||
if (isExecError(err)) {
|
||||
const parsedExecError = parseExecError(err);
|
||||
throw Error(parsedExecError);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
if (tempInputFilePath) {
|
||||
await deleteTempFile(tempInputFilePath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isExecError = (err: any) => {
|
||||
return err.message.includes("Command failed:");
|
||||
};
|
||||
|
||||
const parseExecError = (err: any) => {
|
||||
const errMessage = err.message;
|
||||
if (errMessage.includes("Bad CPU type in executable")) {
|
||||
return CustomErrors.UNSUPPORTED_PLATFORM(
|
||||
process.platform,
|
||||
process.arch,
|
||||
);
|
||||
} else {
|
||||
return errMessage;
|
||||
}
|
||||
};
|
||||
|
||||
async function computeImageEmbedding_(
|
||||
model: Model,
|
||||
inputFilePath: string,
|
||||
): Promise<Float32Array> {
|
||||
|
@ -278,6 +324,23 @@ export async function computeONNXImageEmbedding(
|
|||
export async function computeTextEmbedding(
|
||||
model: Model,
|
||||
text: string,
|
||||
): Promise<Float32Array> {
|
||||
try {
|
||||
const embedding = computeTextEmbedding_(model, text);
|
||||
return embedding;
|
||||
} catch (err) {
|
||||
if (isExecError(err)) {
|
||||
const parsedExecError = parseExecError(err);
|
||||
throw Error(parsedExecError);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function computeTextEmbedding_(
|
||||
model: Model,
|
||||
text: string,
|
||||
): Promise<Float32Array> {
|
||||
if (model === Model.GGML_CLIP) {
|
||||
return await computeGGMLTextEmbedding(text);
|
||||
|
|
|
@ -4,8 +4,11 @@ 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 { ElectronFile } from "../types";
|
||||
import { generateTempFilePath, getTempDirPath } from "../utils/temp";
|
||||
import { logErrorSentry } from "../main/log";
|
||||
|
||||
const shellescape = require("any-shell-escape");
|
||||
|
||||
const execAsync = util.promisify(require("child_process").exec);
|
||||
|
@ -42,6 +45,41 @@ const OUTPUT_PATH_PLACEHOLDER = "OUTPUT";
|
|||
* I'm not sure if our code is supposed to be able to use it, and how.
|
||||
*/
|
||||
export async function runFFmpegCmd(
|
||||
cmd: string[],
|
||||
inputFile: File | ElectronFile,
|
||||
outputFileName: string,
|
||||
dontTimeout?: boolean,
|
||||
) {
|
||||
let inputFilePath = null;
|
||||
let createdTempInputFile = null;
|
||||
try {
|
||||
if (!existsSync(inputFile.path)) {
|
||||
const tempFilePath = await generateTempFilePath(inputFile.name);
|
||||
await writeStream(tempFilePath, await inputFile.stream());
|
||||
inputFilePath = tempFilePath;
|
||||
createdTempInputFile = true;
|
||||
} else {
|
||||
inputFilePath = inputFile.path;
|
||||
}
|
||||
const outputFileData = await runFFmpegCmd_(
|
||||
cmd,
|
||||
inputFilePath,
|
||||
outputFileName,
|
||||
dontTimeout,
|
||||
);
|
||||
return new File([outputFileData], outputFileName);
|
||||
} finally {
|
||||
if (createdTempInputFile) {
|
||||
try {
|
||||
await deleteTempFile(inputFilePath);
|
||||
} catch (e) {
|
||||
logError(e, "failed to deleteTempFile");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function runFFmpegCmd_(
|
||||
cmd: string[],
|
||||
inputFilePath: string,
|
||||
outputFileName: string,
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
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;
|
||||
|
||||
export async function getDirFiles(dirPath: string) {
|
||||
const files = await getDirFilePaths(dirPath);
|
||||
const electronFiles = await Promise.all(files.map(getElectronFile));
|
||||
return electronFiles;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/63111390
|
||||
export const getDirFilePaths = async (dirPath: string) => {
|
||||
if (!(await fs.stat(dirPath)).isDirectory()) {
|
||||
|
@ -181,58 +186,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);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import { exec } from "child_process";
|
||||
import util from "util";
|
||||
|
||||
import log from "electron-log";
|
||||
import { existsSync } from "fs";
|
||||
import * as fs from "node:fs/promises";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import { CustomErrors } from "../constants/errors";
|
||||
import { writeStream } from "../main/fs";
|
||||
import { isDev } from "../main/general";
|
||||
import { logError, logErrorSentry } from "../main/log";
|
||||
import { ElectronFile } from "../types";
|
||||
import { isPlatform } from "../utils/common/platform";
|
||||
import { generateTempFilePath } from "../utils/temp";
|
||||
import { logErrorSentry } from "../main/log";
|
||||
import { deleteTempFile } from "./ffmpeg";
|
||||
|
||||
const shellescape = require("any-shell-escape");
|
||||
|
||||
const asyncExec = util.promisify(exec);
|
||||
|
@ -80,6 +84,17 @@ function getImageMagickStaticPath() {
|
|||
export async function convertToJPEG(
|
||||
fileData: Uint8Array,
|
||||
filename: string,
|
||||
): Promise<Uint8Array> {
|
||||
if (isPlatform("windows")) {
|
||||
throw Error(CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED);
|
||||
}
|
||||
const convertedFileData = await convertToJPEG_(fileData, filename);
|
||||
return convertedFileData;
|
||||
}
|
||||
|
||||
async function convertToJPEG_(
|
||||
fileData: Uint8Array,
|
||||
filename: string,
|
||||
): Promise<Uint8Array> {
|
||||
let tempInputFilePath: string;
|
||||
let tempOutputFilePath: string;
|
||||
|
@ -159,6 +174,44 @@ function constructConvertCommand(
|
|||
}
|
||||
|
||||
export async function generateImageThumbnail(
|
||||
inputFile: File | ElectronFile,
|
||||
maxDimension: number,
|
||||
maxSize: number,
|
||||
): Promise<Uint8Array> {
|
||||
let inputFilePath = null;
|
||||
let createdTempInputFile = null;
|
||||
try {
|
||||
if (isPlatform("windows")) {
|
||||
throw Error(
|
||||
CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED,
|
||||
);
|
||||
}
|
||||
if (!existsSync(inputFile.path)) {
|
||||
const tempFilePath = await generateTempFilePath(inputFile.name);
|
||||
await writeStream(tempFilePath, await inputFile.stream());
|
||||
inputFilePath = tempFilePath;
|
||||
createdTempInputFile = true;
|
||||
} else {
|
||||
inputFilePath = inputFile.path;
|
||||
}
|
||||
const thumbnail = await generateImageThumbnail_(
|
||||
inputFilePath,
|
||||
maxDimension,
|
||||
maxSize,
|
||||
);
|
||||
return thumbnail;
|
||||
} finally {
|
||||
if (createdTempInputFile) {
|
||||
try {
|
||||
await deleteTempFile(inputFilePath);
|
||||
} catch (e) {
|
||||
logError(e, "failed to deleteTempFile");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function generateImageThumbnail_(
|
||||
inputFilePath: string,
|
||||
width: number,
|
||||
maxSize: number,
|
||||
|
|
|
@ -1,11 +1,95 @@
|
|||
import type { FSWatcher } from "chokidar";
|
||||
import ElectronLog from "electron-log";
|
||||
import { watchStore } from "../stores/watch.store";
|
||||
import { WatchStoreType } from "../types";
|
||||
import { WatchMapping, WatchStoreType } from "../types";
|
||||
import { isMappingPresent } from "../utils/watch";
|
||||
|
||||
export const addWatchMapping = async (
|
||||
watcher: FSWatcher,
|
||||
rootFolderName: string,
|
||||
folderPath: string,
|
||||
uploadStrategy: number,
|
||||
) => {
|
||||
ElectronLog.log(`Adding watch mapping: ${folderPath}`);
|
||||
const watchMappings = getWatchMappings();
|
||||
if (isMappingPresent(watchMappings, folderPath)) {
|
||||
throw new Error(`Watch mapping already exists`);
|
||||
}
|
||||
|
||||
watcher.add(folderPath);
|
||||
|
||||
watchMappings.push({
|
||||
rootFolderName,
|
||||
uploadStrategy,
|
||||
folderPath,
|
||||
syncedFiles: [],
|
||||
ignoredFiles: [],
|
||||
});
|
||||
|
||||
setWatchMappings(watchMappings);
|
||||
};
|
||||
|
||||
export const removeWatchMapping = async (
|
||||
watcher: FSWatcher,
|
||||
folderPath: string,
|
||||
) => {
|
||||
let watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
|
||||
if (!watchMapping) {
|
||||
throw new Error(`Watch mapping does not exist`);
|
||||
}
|
||||
|
||||
watcher.unwatch(watchMapping.folderPath);
|
||||
|
||||
watchMappings = watchMappings.filter(
|
||||
(mapping) => mapping.folderPath !== watchMapping.folderPath,
|
||||
);
|
||||
|
||||
setWatchMappings(watchMappings);
|
||||
};
|
||||
|
||||
export function updateWatchMappingSyncedFiles(
|
||||
folderPath: string,
|
||||
files: WatchMapping["syncedFiles"],
|
||||
): void {
|
||||
const watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
|
||||
if (!watchMapping) {
|
||||
throw Error(`Watch mapping not found`);
|
||||
}
|
||||
|
||||
watchMapping.syncedFiles = files;
|
||||
setWatchMappings(watchMappings);
|
||||
}
|
||||
|
||||
export function updateWatchMappingIgnoredFiles(
|
||||
folderPath: string,
|
||||
files: WatchMapping["ignoredFiles"],
|
||||
): void {
|
||||
const watchMappings = getWatchMappings();
|
||||
const watchMapping = watchMappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
|
||||
if (!watchMapping) {
|
||||
throw Error(`Watch mapping not found`);
|
||||
}
|
||||
|
||||
watchMapping.ignoredFiles = files;
|
||||
setWatchMappings(watchMappings);
|
||||
}
|
||||
|
||||
export function getWatchMappings() {
|
||||
const mappings = watchStore.get("mappings") ?? [];
|
||||
return mappings;
|
||||
}
|
||||
|
||||
export function setWatchMappings(watchMappings: WatchStoreType["mappings"]) {
|
||||
function setWatchMappings(watchMappings: WatchStoreType["mappings"]) {
|
||||
watchStore.set("mappings", watchMappings);
|
||||
}
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
import chokidar from "chokidar";
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
dialog,
|
||||
ipcMain,
|
||||
safeStorage,
|
||||
shell,
|
||||
Tray,
|
||||
} from "electron";
|
||||
import path from "path";
|
||||
import { attachIPCHandlers } from "../main/ipc";
|
||||
import {
|
||||
muteUpdateNotification,
|
||||
skipAppUpdate,
|
||||
updateAndRestart,
|
||||
} from "../services/appUpdater";
|
||||
import {
|
||||
computeImageEmbedding,
|
||||
computeTextEmbedding,
|
||||
} from "../services/clipService";
|
||||
import { deleteTempFile, runFFmpegCmd } from "../services/ffmpeg";
|
||||
import { getDirFilePaths } from "../services/fs";
|
||||
import {
|
||||
convertToJPEG,
|
||||
generateImageThumbnail,
|
||||
} from "../services/imageProcessor";
|
||||
import { generateTempFilePath } from "./temp";
|
||||
|
||||
export default function setupIpcComs(
|
||||
tray: Tray,
|
||||
mainWindow: BrowserWindow,
|
||||
watcher: chokidar.FSWatcher,
|
||||
): void {
|
||||
attachIPCHandlers();
|
||||
|
||||
ipcMain.handle("select-dir", 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);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("show-upload-files-dialog", async () => {
|
||||
const files = await dialog.showOpenDialog({
|
||||
properties: ["openFile", "multiSelections"],
|
||||
});
|
||||
return files.filePaths;
|
||||
});
|
||||
|
||||
ipcMain.handle("show-upload-zip-dialog", async () => {
|
||||
const files = await dialog.showOpenDialog({
|
||||
properties: ["openFile", "multiSelections"],
|
||||
filters: [{ name: "Zip File", extensions: ["zip"] }],
|
||||
});
|
||||
return files.filePaths;
|
||||
});
|
||||
|
||||
ipcMain.handle("show-upload-dirs-dialog", async () => {
|
||||
const dir = await dialog.showOpenDialog({
|
||||
properties: ["openDirectory", "multiSelections"],
|
||||
});
|
||||
|
||||
let files: string[] = [];
|
||||
for (const dirPath of dir.filePaths) {
|
||||
files = [...files, ...(await getDirFilePaths(dirPath))];
|
||||
}
|
||||
|
||||
return files;
|
||||
});
|
||||
|
||||
ipcMain.handle("add-watcher", async (_, args: { dir: string }) => {
|
||||
watcher.add(args.dir);
|
||||
});
|
||||
|
||||
ipcMain.handle("remove-watcher", async (_, args: { dir: string }) => {
|
||||
watcher.unwatch(args.dir);
|
||||
});
|
||||
|
||||
ipcMain.handle("safeStorage-encrypt", (_, message) => {
|
||||
return safeStorage.encryptString(message);
|
||||
});
|
||||
|
||||
ipcMain.handle("safeStorage-decrypt", (_, message) => {
|
||||
return safeStorage.decryptString(message);
|
||||
});
|
||||
|
||||
ipcMain.handle("convert-to-jpeg", (_, fileData, filename) => {
|
||||
return convertToJPEG(fileData, filename);
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
"run-ffmpeg-cmd",
|
||||
(_, cmd, inputFilePath, outputFileName, dontTimeout) => {
|
||||
return runFFmpegCmd(
|
||||
cmd,
|
||||
inputFilePath,
|
||||
outputFileName,
|
||||
dontTimeout,
|
||||
);
|
||||
},
|
||||
);
|
||||
ipcMain.handle("get-temp-file-path", (_, formatSuffix) => {
|
||||
return generateTempFilePath(formatSuffix);
|
||||
});
|
||||
ipcMain.handle("remove-temp-file", (_, tempFilePath: string) => {
|
||||
return deleteTempFile(tempFilePath);
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
"generate-image-thumbnail",
|
||||
(_, fileData, maxDimension, maxSize) => {
|
||||
return generateImageThumbnail(fileData, maxDimension, maxSize);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle("compute-image-embedding", (_, model, inputFilePath) => {
|
||||
return computeImageEmbedding(model, inputFilePath);
|
||||
});
|
||||
ipcMain.handle("compute-text-embedding", (_, model, text) => {
|
||||
return computeTextEmbedding(model, text);
|
||||
});
|
||||
}
|
|
@ -5,10 +5,10 @@ import os from "os";
|
|||
import path from "path";
|
||||
import util from "util";
|
||||
import { rendererURL } from "../main";
|
||||
import { isDev } from "../main/general";
|
||||
import { setupAutoUpdater } from "../services/appUpdater";
|
||||
import autoLauncher from "../services/autoLauncher";
|
||||
import { getHideDockIconPreference } from "../services/userPreference";
|
||||
import { isDev } from "../main/general";
|
||||
import { isPlatform } from "./common/platform";
|
||||
import { buildContextMenu, buildMenuBar } from "./menu";
|
||||
const execAsync = util.promisify(require("child_process").exec);
|
||||
|
@ -19,7 +19,8 @@ export async function handleUpdates(mainWindow: BrowserWindow) {
|
|||
setupAutoUpdater(mainWindow);
|
||||
}
|
||||
}
|
||||
export function setupTrayItem(mainWindow: BrowserWindow) {
|
||||
|
||||
export const setupTrayItem = (mainWindow: BrowserWindow) => {
|
||||
const iconName = isPlatform("mac")
|
||||
? "taskbar-icon-Template.png"
|
||||
: "taskbar-icon.png";
|
||||
|
@ -31,8 +32,7 @@ export function setupTrayItem(mainWindow: BrowserWindow) {
|
|||
const tray = new Tray(trayIcon);
|
||||
tray.setToolTip("ente");
|
||||
tray.setContextMenu(buildContextMenu(mainWindow));
|
||||
return tray;
|
||||
}
|
||||
};
|
||||
|
||||
export function handleDownloads(mainWindow: BrowserWindow) {
|
||||
mainWindow.webContents.session.on("will-download", (_, item) => {
|
||||
|
|
|
@ -530,13 +530,13 @@ export default function Uploader(props: Props) {
|
|||
) {
|
||||
await ImportService.setToUploadCollection(collections);
|
||||
if (zipPaths.current) {
|
||||
ElectronAPIs.setToUploadFiles(
|
||||
await ElectronAPIs.setToUploadFiles(
|
||||
PICKED_UPLOAD_TYPE.ZIPS,
|
||||
zipPaths.current,
|
||||
);
|
||||
zipPaths.current = null;
|
||||
}
|
||||
ElectronAPIs.setToUploadFiles(
|
||||
await ElectronAPIs.setToUploadFiles(
|
||||
PICKED_UPLOAD_TYPE.FILES,
|
||||
filesWithCollectionToUploadIn.map(
|
||||
({ file }) => (file as ElectronFile).path,
|
||||
|
|
|
@ -28,7 +28,7 @@ export default function WatchFolder({ open, onClose }: Iprops) {
|
|||
if (!isElectron()) {
|
||||
return;
|
||||
}
|
||||
setMappings(watchFolderService.getWatchMappings());
|
||||
watchFolderService.getWatchMappings().then((m) => setMappings(m));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -87,12 +87,12 @@ export default function WatchFolder({ open, onClose }: Iprops) {
|
|||
uploadStrategy,
|
||||
);
|
||||
setInputFolderPath("");
|
||||
setMappings(watchFolderService.getWatchMappings());
|
||||
setMappings(await watchFolderService.getWatchMappings());
|
||||
};
|
||||
|
||||
const handleRemoveWatchMapping = async (mapping: WatchMapping) => {
|
||||
await watchFolderService.removeWatchMapping(mapping.folderPath);
|
||||
setMappings(watchFolderService.getWatchMappings());
|
||||
setMappings(await watchFolderService.getWatchMappings());
|
||||
};
|
||||
|
||||
const closeChoiceModal = () => setChoiceModalOpen(false);
|
||||
|
|
|
@ -30,20 +30,20 @@ class ImportService {
|
|||
let collectionName: string = null;
|
||||
/* collection being one suggest one of two things
|
||||
1. Either the user has upload to a single existing collection
|
||||
2. Created a new single collection to upload to
|
||||
2. Created a new single collection to upload to
|
||||
may have had multiple folder, but chose to upload
|
||||
to one album
|
||||
hence saving the collection name when upload collection count is 1
|
||||
helps the info of user choosing this options
|
||||
and on next upload we can directly start uploading to this collection
|
||||
and on next upload we can directly start uploading to this collection
|
||||
*/
|
||||
if (collections.length === 1) {
|
||||
collectionName = collections[0].name;
|
||||
}
|
||||
ElectronAPIs.setToUploadCollection(collectionName);
|
||||
await ElectronAPIs.setToUploadCollection(collectionName);
|
||||
}
|
||||
|
||||
updatePendingUploads(files: FileWithCollection[]) {
|
||||
async updatePendingUploads(files: FileWithCollection[]) {
|
||||
const filePaths = [];
|
||||
for (const fileWithCollection of files) {
|
||||
if (fileWithCollection.isLivePhoto) {
|
||||
|
@ -57,13 +57,16 @@ class ImportService {
|
|||
filePaths.push((fileWithCollection.file as ElectronFile).path);
|
||||
}
|
||||
}
|
||||
ElectronAPIs.setToUploadFiles(PICKED_UPLOAD_TYPE.FILES, filePaths);
|
||||
await ElectronAPIs.setToUploadFiles(
|
||||
PICKED_UPLOAD_TYPE.FILES,
|
||||
filePaths,
|
||||
);
|
||||
}
|
||||
|
||||
cancelRemainingUploads() {
|
||||
ElectronAPIs.setToUploadCollection(null);
|
||||
ElectronAPIs.setToUploadFiles(PICKED_UPLOAD_TYPE.ZIPS, []);
|
||||
ElectronAPIs.setToUploadFiles(PICKED_UPLOAD_TYPE.FILES, []);
|
||||
async cancelRemainingUploads() {
|
||||
await ElectronAPIs.setToUploadCollection(null);
|
||||
await ElectronAPIs.setToUploadFiles(PICKED_UPLOAD_TYPE.ZIPS, []);
|
||||
await ElectronAPIs.setToUploadFiles(PICKED_UPLOAD_TYPE.FILES, []);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -180,7 +180,7 @@ class UploadManager {
|
|||
if (e.message === CustomError.UPLOAD_CANCELLED) {
|
||||
if (isElectron()) {
|
||||
this.remainingFiles = [];
|
||||
ImportService.cancelRemainingUploads();
|
||||
await ImportService.cancelRemainingUploads();
|
||||
}
|
||||
} else {
|
||||
logError(e, "uploading failed with error");
|
||||
|
@ -326,7 +326,7 @@ class UploadManager {
|
|||
addLogLine(
|
||||
`post upload action -> fileUploadResult: ${fileUploadResult} uploadedFile present ${!!uploadedFile}`,
|
||||
);
|
||||
this.updateElectronRemainingFiles(fileWithCollection);
|
||||
await this.updateElectronRemainingFiles(fileWithCollection);
|
||||
switch (fileUploadResult) {
|
||||
case UPLOAD_RESULT.FAILED:
|
||||
case UPLOAD_RESULT.BLOCKED:
|
||||
|
@ -427,14 +427,14 @@ class UploadManager {
|
|||
this.setFiles((files) => sortFiles([...files, decryptedFile]));
|
||||
}
|
||||
|
||||
private updateElectronRemainingFiles(
|
||||
private async updateElectronRemainingFiles(
|
||||
fileWithCollection: FileWithCollection,
|
||||
) {
|
||||
if (isElectron()) {
|
||||
this.remainingFiles = this.remainingFiles.filter(
|
||||
(file) => !areFileWithCollectionsSame(file, fileWithCollection),
|
||||
);
|
||||
ImportService.updatePendingUploads(this.remainingFiles);
|
||||
await ImportService.updatePendingUploads(this.remainingFiles);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ export async function diskFileRemovedCallback(filePath: string) {
|
|||
|
||||
export async function diskFolderRemovedCallback(folderPath: string) {
|
||||
try {
|
||||
const mappings = watchFolderService.getWatchMappings();
|
||||
const mappings = await watchFolderService.getWatchMappings();
|
||||
const mapping = mappings.find(
|
||||
(mapping) => mapping.folderPath === folderPath,
|
||||
);
|
||||
|
|
|
@ -72,7 +72,7 @@ class watchFolderService {
|
|||
|
||||
async getAndSyncDiffOfFiles() {
|
||||
try {
|
||||
let mappings = this.getWatchMappings();
|
||||
let mappings = await this.getWatchMappings();
|
||||
|
||||
if (!mappings?.length) {
|
||||
return;
|
||||
|
@ -205,14 +205,13 @@ class watchFolderService {
|
|||
}
|
||||
}
|
||||
|
||||
getWatchMappings(): WatchMapping[] {
|
||||
async getWatchMappings(): Promise<WatchMapping[]> {
|
||||
try {
|
||||
return ElectronAPIs.getWatchMappings() ?? [];
|
||||
return (await ElectronAPIs.getWatchMappings()) ?? [];
|
||||
} catch (e) {
|
||||
logError(e, "error while getting watch mappings");
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private setIsEventRunning(isEventRunning: boolean) {
|
||||
|
@ -234,7 +233,7 @@ class watchFolderService {
|
|||
addLogLine(
|
||||
`running event type:${event.type} collectionName:${event.collectionName} folderPath:${event.folderPath} , fileCount:${event.files?.length} pathsCount: ${event.paths?.length}`,
|
||||
);
|
||||
const mappings = this.getWatchMappings();
|
||||
const mappings = await this.getWatchMappings();
|
||||
const mapping = mappings.find(
|
||||
(mapping) => mapping.folderPath === event.folderPath,
|
||||
);
|
||||
|
@ -380,7 +379,7 @@ class watchFolderService {
|
|||
...this.currentlySyncedMapping.syncedFiles,
|
||||
...syncedFiles,
|
||||
];
|
||||
ElectronAPIs.updateWatchMappingSyncedFiles(
|
||||
await ElectronAPIs.updateWatchMappingSyncedFiles(
|
||||
this.currentlySyncedMapping.folderPath,
|
||||
this.currentlySyncedMapping.syncedFiles,
|
||||
);
|
||||
|
@ -390,7 +389,7 @@ class watchFolderService {
|
|||
...this.currentlySyncedMapping.ignoredFiles,
|
||||
...ignoredFiles,
|
||||
];
|
||||
ElectronAPIs.updateWatchMappingIgnoredFiles(
|
||||
await ElectronAPIs.updateWatchMappingIgnoredFiles(
|
||||
this.currentlySyncedMapping.folderPath,
|
||||
this.currentlySyncedMapping.ignoredFiles,
|
||||
);
|
||||
|
@ -505,7 +504,7 @@ class watchFolderService {
|
|||
this.currentlySyncedMapping.syncedFiles.filter(
|
||||
(file) => !filePathsToRemove.has(file.path),
|
||||
);
|
||||
ElectronAPIs.updateWatchMappingSyncedFiles(
|
||||
await ElectronAPIs.updateWatchMappingSyncedFiles(
|
||||
this.currentlySyncedMapping.folderPath,
|
||||
this.currentlySyncedMapping.syncedFiles,
|
||||
);
|
||||
|
@ -561,7 +560,7 @@ class watchFolderService {
|
|||
|
||||
async getCollectionNameAndFolderPath(filePath: string) {
|
||||
try {
|
||||
const mappings = this.getWatchMappings();
|
||||
const mappings = await this.getWatchMappings();
|
||||
|
||||
const mapping = mappings.find(
|
||||
(mapping) =>
|
||||
|
|
|
@ -11,6 +11,17 @@ export enum Model {
|
|||
ONNX_CLIP = "onnx-clip",
|
||||
}
|
||||
|
||||
export enum FILE_PATH_TYPE {
|
||||
FILES = "files",
|
||||
ZIPS = "zips",
|
||||
}
|
||||
|
||||
export enum PICKED_UPLOAD_TYPE {
|
||||
FILES = "files",
|
||||
FOLDERS = "folders",
|
||||
ZIPS = "zips",
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra APIs provided by the Node.js layer when our code is running in Electron
|
||||
*
|
||||
|
@ -78,75 +89,48 @@ 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
|
||||
|
||||
registerForegroundEventListener: (onForeground: () => void) => void;
|
||||
|
||||
clearElectronStore: () => void;
|
||||
|
||||
// - FS legacy
|
||||
checkExistsAndCreateDir: (dirPath: string) => Promise<void>;
|
||||
setEncryptionKey: (encryptionKey: string) => Promise<void>;
|
||||
|
||||
getEncryptionKey: () => Promise<string>;
|
||||
|
||||
// - App update
|
||||
|
||||
updateAndRestart: () => void;
|
||||
|
||||
skipAppUpdate: (version: string) => void;
|
||||
|
||||
muteUpdateNotification: (version: string) => void;
|
||||
|
||||
registerUpdateEventListener: (
|
||||
showUpdateDialog: (updateInfo: AppUpdateInfo) => void,
|
||||
) => void;
|
||||
|
||||
/** TODO: FIXME or migrate below this */
|
||||
saveStreamToDisk: (
|
||||
path: string,
|
||||
fileStream: ReadableStream<any>,
|
||||
) => Promise<void>;
|
||||
saveFileToDisk: (path: string, file: any) => Promise<void>;
|
||||
selectDirectory: () => Promise<string>;
|
||||
readTextFile: (path: string) => Promise<string>;
|
||||
showUploadFilesDialog: () => Promise<ElectronFile[]>;
|
||||
showUploadDirsDialog: () => Promise<ElectronFile[]>;
|
||||
getPendingUploads: () => Promise<{
|
||||
files: ElectronFile[];
|
||||
collectionName: string;
|
||||
type: string;
|
||||
}>;
|
||||
setToUploadFiles: (type: string, filePaths: string[]) => void;
|
||||
showUploadZipDialog: () => Promise<{
|
||||
zipPaths: string[];
|
||||
files: ElectronFile[];
|
||||
}>;
|
||||
getElectronFilesFromGoogleZip: (
|
||||
filePath: string,
|
||||
) => Promise<ElectronFile[]>;
|
||||
setToUploadCollection: (collectionName: string) => void;
|
||||
getDirFiles: (dirPath: string) => Promise<ElectronFile[]>;
|
||||
getWatchMappings: () => WatchMapping[];
|
||||
updateWatchMappingSyncedFiles: (
|
||||
folderPath: string,
|
||||
files: WatchMapping["syncedFiles"],
|
||||
) => void;
|
||||
updateWatchMappingIgnoredFiles: (
|
||||
folderPath: string,
|
||||
files: WatchMapping["ignoredFiles"],
|
||||
) => void;
|
||||
addWatchMapping: (
|
||||
collectionName: string,
|
||||
folderPath: string,
|
||||
uploadStrategy: number,
|
||||
) => Promise<void>;
|
||||
removeWatchMapping: (folderPath: string) => Promise<void>;
|
||||
registerWatcherFunctions: (
|
||||
addFile: (file: ElectronFile) => Promise<void>,
|
||||
removeFile: (path: string) => Promise<void>,
|
||||
removeFolder: (folderPath: string) => Promise<void>,
|
||||
) => void;
|
||||
isFolder: (dirPath: string) => Promise<boolean>;
|
||||
setEncryptionKey: (encryptionKey: string) => Promise<void>;
|
||||
getEncryptionKey: () => Promise<string>;
|
||||
// - Conversion
|
||||
|
||||
convertToJPEG: (
|
||||
fileData: Uint8Array,
|
||||
filename: string,
|
||||
) => Promise<Uint8Array>;
|
||||
|
||||
generateImageThumbnail: (
|
||||
inputFile: File | ElectronFile,
|
||||
maxDimension: number,
|
||||
maxSize: number,
|
||||
) => Promise<Uint8Array>;
|
||||
|
||||
runFFmpegCmd: (
|
||||
cmd: string[],
|
||||
inputFile: File | ElectronFile,
|
||||
|
@ -154,18 +138,87 @@ export interface ElectronAPIsType {
|
|||
dontTimeout?: boolean,
|
||||
) => Promise<File>;
|
||||
|
||||
generateImageThumbnail: (
|
||||
inputFile: File | ElectronFile,
|
||||
maxDimension: number,
|
||||
maxSize: number,
|
||||
) => Promise<Uint8Array>;
|
||||
moveFile: (oldPath: string, newPath: string) => Promise<void>;
|
||||
deleteFolder: (path: string) => Promise<void>;
|
||||
deleteFile: (path: string) => Promise<void>;
|
||||
rename: (oldPath: string, newPath: string) => Promise<void>;
|
||||
// - ML
|
||||
|
||||
computeImageEmbedding: (
|
||||
model: Model,
|
||||
imageData: Uint8Array,
|
||||
) => Promise<Float32Array>;
|
||||
|
||||
computeTextEmbedding: (model: Model, text: string) => 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
|
||||
|
||||
registerWatcherFunctions: (
|
||||
addFile: (file: ElectronFile) => Promise<void>,
|
||||
removeFile: (path: string) => Promise<void>,
|
||||
removeFolder: (folderPath: string) => Promise<void>,
|
||||
) => void;
|
||||
|
||||
addWatchMapping: (
|
||||
collectionName: string,
|
||||
folderPath: string,
|
||||
uploadStrategy: number,
|
||||
) => Promise<void>;
|
||||
|
||||
removeWatchMapping: (folderPath: string) => Promise<void>;
|
||||
|
||||
getWatchMappings: () => Promise<WatchMapping[]>;
|
||||
|
||||
updateWatchMappingSyncedFiles: (
|
||||
folderPath: string,
|
||||
files: WatchMapping["syncedFiles"],
|
||||
) => Promise<void>;
|
||||
|
||||
updateWatchMappingIgnoredFiles: (
|
||||
folderPath: string,
|
||||
files: WatchMapping["ignoredFiles"],
|
||||
) => Promise<void>;
|
||||
|
||||
// - FS legacy
|
||||
checkExistsAndCreateDir: (dirPath: string) => Promise<void>;
|
||||
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>;
|
||||
|
||||
// - Upload
|
||||
|
||||
getPendingUploads: () => Promise<{
|
||||
files: ElectronFile[];
|
||||
collectionName: string;
|
||||
type: string;
|
||||
}>;
|
||||
setToUploadFiles: (
|
||||
/** TODO(MR): This is the actual type */
|
||||
// type: FILE_PATH_TYPE,
|
||||
type: PICKED_UPLOAD_TYPE,
|
||||
filePaths: string[],
|
||||
) => Promise<void>;
|
||||
getElectronFilesFromGoogleZip: (
|
||||
filePath: string,
|
||||
) => Promise<ElectronFile[]>;
|
||||
setToUploadCollection: (collectionName: string) => Promise<void>;
|
||||
getDirFiles: (dirPath: string) => Promise<ElectronFile[]>;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue