[desktop] Fix watch related IPC - Part 1/x (#1463)

This commit is contained in:
Manav Rathi 2024-04-16 18:45:38 +05:30 committed by GitHub
commit bb9c384a52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 537 additions and 542 deletions

View file

@ -10,7 +10,7 @@
import type { FSWatcher } from "chokidar";
import { ipcMain } from "electron/main";
import type { ElectronFile, FILE_PATH_TYPE, WatchMapping } from "../types/ipc";
import type { ElectronFile, FILE_PATH_TYPE, FolderWatch } from "../types/ipc";
import {
selectDirectory,
showUploadDirsDialog,
@ -242,13 +242,13 @@ export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => {
ipcMain.handle(
"updateWatchMappingSyncedFiles",
(_, folderPath: string, files: WatchMapping["syncedFiles"]) =>
(_, folderPath: string, files: FolderWatch["syncedFiles"]) =>
updateWatchMappingSyncedFiles(folderPath, files),
);
ipcMain.handle(
"updateWatchMappingIgnoredFiles",
(_, folderPath: string, files: WatchMapping["ignoredFiles"]) =>
(_, folderPath: string, files: FolderWatch["ignoredFiles"]) =>
updateWatchMappingIgnoredFiles(folderPath, files),
);
};

View file

@ -1,6 +1,6 @@
import type { FSWatcher } from "chokidar";
import ElectronLog from "electron-log";
import { WatchMapping, WatchStoreType } from "../../types/ipc";
import { FolderWatch, WatchStoreType } from "../../types/ipc";
import { watchStore } from "../stores/watch.store";
export const addWatchMapping = async (
@ -28,7 +28,7 @@ export const addWatchMapping = async (
setWatchMappings(watchMappings);
};
function isMappingPresent(watchMappings: WatchMapping[], folderPath: string) {
function isMappingPresent(watchMappings: FolderWatch[], folderPath: string) {
const watchMapping = watchMappings?.find(
(mapping) => mapping.folderPath === folderPath,
);
@ -59,7 +59,7 @@ export const removeWatchMapping = async (
export function updateWatchMappingSyncedFiles(
folderPath: string,
files: WatchMapping["syncedFiles"],
files: FolderWatch["syncedFiles"],
): void {
const watchMappings = getWatchMappings();
const watchMapping = watchMappings.find(
@ -76,7 +76,7 @@ export function updateWatchMappingSyncedFiles(
export function updateWatchMappingIgnoredFiles(
folderPath: string,
files: WatchMapping["ignoredFiles"],
files: FolderWatch["ignoredFiles"],
): void {
const watchMappings = getWatchMappings();
const watchMapping = watchMappings.find(

View file

@ -45,7 +45,7 @@ import type {
AppUpdateInfo,
ElectronFile,
FILE_PATH_TYPE,
WatchMapping,
FolderWatch,
} from "./types/ipc";
// - General
@ -220,18 +220,18 @@ const addWatchMapping = (
const removeWatchMapping = (folderPath: string): Promise<void> =>
ipcRenderer.invoke("removeWatchMapping", folderPath);
const getWatchMappings = (): Promise<WatchMapping[]> =>
const getWatchMappings = (): Promise<FolderWatch[]> =>
ipcRenderer.invoke("getWatchMappings");
const updateWatchMappingSyncedFiles = (
folderPath: string,
files: WatchMapping["syncedFiles"],
files: FolderWatch["syncedFiles"],
): Promise<void> =>
ipcRenderer.invoke("updateWatchMappingSyncedFiles", folderPath, files);
const updateWatchMappingIgnoredFiles = (
folderPath: string,
files: WatchMapping["ignoredFiles"],
files: FolderWatch["ignoredFiles"],
): Promise<void> =>
ipcRenderer.invoke("updateWatchMappingIgnoredFiles", folderPath, files);

View file

@ -5,6 +5,20 @@
* See [Note: types.ts <-> preload.ts <-> ipc.ts]
*/
export interface FolderWatch {
rootFolderName: string;
uploadStrategy: number;
folderPath: string;
syncedFiles: FolderWatchSyncedFile[];
ignoredFiles: string[];
}
export interface FolderWatchSyncedFile {
path: string;
uploadedFileID: number;
collectionID: number;
}
/**
* Errors that have special semantics on the web side.
*
@ -52,22 +66,8 @@ export interface ElectronFile {
arrayBuffer: () => Promise<Uint8Array>;
}
interface WatchMappingSyncedFile {
path: string;
uploadedFileID: number;
collectionID: number;
}
export interface WatchMapping {
rootFolderName: string;
uploadStrategy: number;
folderPath: string;
syncedFiles: WatchMappingSyncedFile[];
ignoredFiles: string[];
}
export interface WatchStoreType {
mappings: WatchMapping[];
mappings: FolderWatch[];
}
export enum FILE_PATH_TYPE {

View file

@ -24,7 +24,7 @@ import {
import { getAccountsURL } from "@ente/shared/network/api";
import { THEME_COLOR } from "@ente/shared/themes/constants";
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
import WatchFolder from "components/WatchFolder";
import { WatchFolder } from "components/WatchFolder";
import isElectron from "is-electron";
import { getAccountsToken } from "services/userService";
import { getDownloadAppMessage } from "utils/ui";

View file

@ -24,7 +24,7 @@ import {
savePublicCollectionUploaderName,
} from "services/publicCollectionService";
import uploadManager from "services/upload/uploadManager";
import watchFolderService from "services/watchFolder/watchFolderService";
import watchFolderService from "services/watch";
import { NotificationAttributes } from "types/Notification";
import { Collection } from "types/collection";
import {

View file

@ -0,0 +1,364 @@
import {
FlexWrapper,
HorizontalFlex,
SpaceBetweenFlex,
VerticallyCentered,
} from "@ente/shared/components/Container";
import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton";
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
import CheckIcon from "@mui/icons-material/Check";
import DoNotDisturbOutlinedIcon from "@mui/icons-material/DoNotDisturbOutlined";
import FolderCopyOutlinedIcon from "@mui/icons-material/FolderCopyOutlined";
import FolderOpenIcon from "@mui/icons-material/FolderOpen";
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
import {
Box,
Button,
CircularProgress,
Dialog,
DialogContent,
Stack,
Tooltip,
Typography,
} from "@mui/material";
import { styled } from "@mui/material/styles";
import UploadStrategyChoiceModal from "components/Upload/UploadStrategyChoiceModal";
import { PICKED_UPLOAD_TYPE, UPLOAD_STRATEGY } from "constants/upload";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import React, { useContext, useEffect, useState } from "react";
import watchFolderService from "services/watch";
import { WatchMapping } from "types/watchFolder";
import { getImportSuggestion } from "utils/upload";
interface WatchFolderProps {
open: boolean;
onClose: () => void;
}
export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
const [mappings, setMappings] = useState<WatchMapping[]>([]);
const [inputFolderPath, setInputFolderPath] = useState("");
const [choiceModalOpen, setChoiceModalOpen] = useState(false);
const appContext = useContext(AppContext);
const electron = globalThis.electron;
useEffect(() => {
if (!electron) return;
watchFolderService.getWatchMappings().then((m) => setMappings(m));
}, []);
useEffect(() => {
if (
appContext.watchFolderFiles &&
appContext.watchFolderFiles.length > 0
) {
handleFolderDrop(appContext.watchFolderFiles);
appContext.setWatchFolderFiles(null);
}
}, [appContext.watchFolderFiles]);
const handleFolderDrop = async (folders: FileList) => {
for (let i = 0; i < folders.length; i++) {
const folder: any = folders[i];
const path = (folder.path as string).replace(/\\/g, "/");
if (await watchFolderService.isFolder(path)) {
await addFolderForWatching(path);
}
}
};
const addFolderForWatching = async (path: string) => {
if (!electron) return;
setInputFolderPath(path);
const files = await electron.getDirFiles(path);
const analysisResult = getImportSuggestion(
PICKED_UPLOAD_TYPE.FOLDERS,
files,
);
if (analysisResult.hasNestedFolders) {
setChoiceModalOpen(true);
} else {
handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION, path);
}
};
const handleAddFolderClick = async () => {
await handleFolderSelection();
};
const handleFolderSelection = async () => {
const folderPath = await watchFolderService.selectFolder();
if (folderPath) {
await addFolderForWatching(folderPath);
}
};
const handleAddWatchMapping = async (
uploadStrategy: UPLOAD_STRATEGY,
folderPath?: string,
) => {
folderPath = folderPath || inputFolderPath;
await watchFolderService.addWatchMapping(
folderPath.substring(folderPath.lastIndexOf("/") + 1),
folderPath,
uploadStrategy,
);
setInputFolderPath("");
setMappings(await watchFolderService.getWatchMappings());
};
const handleRemoveWatchMapping = (mapping: WatchMapping) => {
watchFolderService
.mappingsAfterRemovingFolder(mapping.folderPath)
.then((ms) => setMappings(ms));
};
const closeChoiceModal = () => setChoiceModalOpen(false);
const uploadToSingleCollection = () => {
closeChoiceModal();
handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION);
};
const uploadToMultipleCollection = () => {
closeChoiceModal();
handleAddWatchMapping(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
};
return (
<>
<Dialog
open={open}
onClose={onClose}
PaperProps={{ sx: { height: "448px", maxWidth: "414px" } }}
>
<DialogTitleWithCloseButton
onClose={onClose}
sx={{ "&&&": { padding: "32px 16px 16px 24px" } }}
>
{t("WATCHED_FOLDERS")}
</DialogTitleWithCloseButton>
<DialogContent sx={{ flex: 1 }}>
<Stack spacing={1} p={1.5} height={"100%"}>
<MappingList
mappings={mappings}
handleRemoveWatchMapping={handleRemoveWatchMapping}
/>
<Button
fullWidth
color="accent"
onClick={handleAddFolderClick}
>
<span>+</span>
<span
style={{
marginLeft: "8px",
}}
></span>
{t("ADD_FOLDER")}
</Button>
</Stack>
</DialogContent>
</Dialog>
<UploadStrategyChoiceModal
open={choiceModalOpen}
onClose={closeChoiceModal}
uploadToSingleCollection={uploadToSingleCollection}
uploadToMultipleCollection={uploadToMultipleCollection}
/>
</>
);
};
const MappingsContainer = styled(Box)(() => ({
height: "278px",
overflow: "auto",
"&::-webkit-scrollbar": {
width: "4px",
},
}));
const NoMappingsContainer = styled(VerticallyCentered)({
textAlign: "left",
alignItems: "flex-start",
marginBottom: "32px",
});
const EntryContainer = styled(Box)({
marginLeft: "12px",
marginRight: "6px",
marginBottom: "12px",
});
interface MappingListProps {
mappings: WatchMapping[];
handleRemoveWatchMapping: (value: WatchMapping) => void;
}
const MappingList: React.FC<MappingListProps> = ({
mappings,
handleRemoveWatchMapping,
}) => {
return mappings.length === 0 ? (
<NoMappingsContent />
) : (
<MappingsContainer>
{mappings.map((mapping) => {
return (
<MappingEntry
key={mapping.rootFolderName}
mapping={mapping}
handleRemoveMapping={handleRemoveWatchMapping}
/>
);
})}
</MappingsContainer>
);
};
const NoMappingsContent: React.FC = () => {
return (
<NoMappingsContainer>
<Stack spacing={1}>
<Typography variant="large" fontWeight={"bold"}>
{t("NO_FOLDERS_ADDED")}
</Typography>
<Typography py={0.5} variant={"small"} color="text.muted">
{t("FOLDERS_AUTOMATICALLY_MONITORED")}
</Typography>
<Typography variant={"small"} color="text.muted">
<FlexWrapper gap={1}>
<CheckmarkIcon />
{t("UPLOAD_NEW_FILES_TO_ENTE")}
</FlexWrapper>
</Typography>
<Typography variant={"small"} color="text.muted">
<FlexWrapper gap={1}>
<CheckmarkIcon />
{t("REMOVE_DELETED_FILES_FROM_ENTE")}
</FlexWrapper>
</Typography>
</Stack>
</NoMappingsContainer>
);
};
const CheckmarkIcon: React.FC = () => {
return (
<CheckIcon
fontSize="small"
sx={{
display: "inline",
fontSize: "15px",
color: (theme) => theme.palette.secondary.main,
}}
/>
);
};
interface MappingEntryProps {
mapping: WatchMapping;
handleRemoveMapping: (mapping: WatchMapping) => void;
}
const MappingEntry: React.FC<MappingEntryProps> = ({
mapping,
handleRemoveMapping,
}) => {
const appContext = React.useContext(AppContext);
const stopWatching = () => {
handleRemoveMapping(mapping);
};
const confirmStopWatching = () => {
appContext.setDialogMessage({
title: t("STOP_WATCHING_FOLDER"),
content: t("STOP_WATCHING_DIALOG_MESSAGE"),
close: {
text: t("CANCEL"),
variant: "secondary",
},
proceed: {
action: stopWatching,
text: t("YES_STOP"),
variant: "critical",
},
});
};
return (
<SpaceBetweenFlex>
<HorizontalFlex>
{mapping &&
mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? (
<Tooltip title={t("UPLOADED_TO_SINGLE_COLLECTION")}>
<FolderOpenIcon />
</Tooltip>
) : (
<Tooltip title={t("UPLOADED_TO_SEPARATE_COLLECTIONS")}>
<FolderCopyOutlinedIcon />
</Tooltip>
)}
<EntryContainer>
<EntryHeading mapping={mapping} />
<Typography color="text.muted" variant="small">
{mapping.folderPath}
</Typography>
</EntryContainer>
</HorizontalFlex>
<MappingEntryOptions confirmStopWatching={confirmStopWatching} />
</SpaceBetweenFlex>
);
};
interface EntryHeadingProps {
mapping: WatchMapping;
}
const EntryHeading: React.FC<EntryHeadingProps> = ({ mapping }) => {
const appContext = useContext(AppContext);
return (
<FlexWrapper gap={1}>
<Typography>{mapping.rootFolderName}</Typography>
{appContext.isFolderSyncRunning &&
watchFolderService.isMappingSyncInProgress(mapping) && (
<CircularProgress size={12} />
)}
</FlexWrapper>
);
};
interface MappingEntryOptionsProps {
confirmStopWatching: () => void;
}
const MappingEntryOptions: React.FC<MappingEntryOptionsProps> = ({
confirmStopWatching,
}) => {
return (
<OverflowMenu
menuPaperProps={{
sx: {
backgroundColor: (theme) =>
theme.colors.background.elevated2,
},
}}
ariaControls={"watch-mapping-option"}
triggerButtonIcon={<MoreHorizIcon />}
>
<OverflowMenuOption
color="critical"
onClick={confirmStopWatching}
startIcon={<DoNotDisturbOutlinedIcon />}
>
{t("STOP_WATCHING")}
</OverflowMenuOption>
</OverflowMenu>
);
};

View file

@ -1,152 +0,0 @@
import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton";
import { Button, Dialog, DialogContent, Stack } from "@mui/material";
import UploadStrategyChoiceModal from "components/Upload/UploadStrategyChoiceModal";
import { PICKED_UPLOAD_TYPE, UPLOAD_STRATEGY } from "constants/upload";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { useContext, useEffect, useState } from "react";
import watchFolderService from "services/watchFolder/watchFolderService";
import { WatchMapping } from "types/watchFolder";
import { getImportSuggestion } from "utils/upload";
import { MappingList } from "./mappingList";
interface Iprops {
open: boolean;
onClose: () => void;
}
export default function WatchFolder({ open, onClose }: Iprops) {
const [mappings, setMappings] = useState<WatchMapping[]>([]);
const [inputFolderPath, setInputFolderPath] = useState("");
const [choiceModalOpen, setChoiceModalOpen] = useState(false);
const appContext = useContext(AppContext);
const electron = globalThis.electron;
useEffect(() => {
if (!electron) return;
watchFolderService.getWatchMappings().then((m) => setMappings(m));
}, []);
useEffect(() => {
if (
appContext.watchFolderFiles &&
appContext.watchFolderFiles.length > 0
) {
handleFolderDrop(appContext.watchFolderFiles);
appContext.setWatchFolderFiles(null);
}
}, [appContext.watchFolderFiles]);
const handleFolderDrop = async (folders: FileList) => {
for (let i = 0; i < folders.length; i++) {
const folder: any = folders[i];
const path = (folder.path as string).replace(/\\/g, "/");
if (await watchFolderService.isFolder(path)) {
await addFolderForWatching(path);
}
}
};
const addFolderForWatching = async (path: string) => {
if (!electron) return;
setInputFolderPath(path);
const files = await electron.getDirFiles(path);
const analysisResult = getImportSuggestion(
PICKED_UPLOAD_TYPE.FOLDERS,
files,
);
if (analysisResult.hasNestedFolders) {
setChoiceModalOpen(true);
} else {
handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION, path);
}
};
const handleAddFolderClick = async () => {
await handleFolderSelection();
};
const handleFolderSelection = async () => {
const folderPath = await watchFolderService.selectFolder();
if (folderPath) {
await addFolderForWatching(folderPath);
}
};
const handleAddWatchMapping = async (
uploadStrategy: UPLOAD_STRATEGY,
folderPath?: string,
) => {
folderPath = folderPath || inputFolderPath;
await watchFolderService.addWatchMapping(
folderPath.substring(folderPath.lastIndexOf("/") + 1),
folderPath,
uploadStrategy,
);
setInputFolderPath("");
setMappings(await watchFolderService.getWatchMappings());
};
const handleRemoveWatchMapping = async (mapping: WatchMapping) => {
await watchFolderService.removeWatchMapping(mapping.folderPath);
setMappings(await watchFolderService.getWatchMappings());
};
const closeChoiceModal = () => setChoiceModalOpen(false);
const uploadToSingleCollection = () => {
closeChoiceModal();
handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION);
};
const uploadToMultipleCollection = () => {
closeChoiceModal();
handleAddWatchMapping(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
};
return (
<>
<Dialog
open={open}
onClose={onClose}
PaperProps={{ sx: { height: "448px", maxWidth: "414px" } }}
>
<DialogTitleWithCloseButton
onClose={onClose}
sx={{ "&&&": { padding: "32px 16px 16px 24px" } }}
>
{t("WATCHED_FOLDERS")}
</DialogTitleWithCloseButton>
<DialogContent sx={{ flex: 1 }}>
<Stack spacing={1} p={1.5} height={"100%"}>
<MappingList
mappings={mappings}
handleRemoveWatchMapping={handleRemoveWatchMapping}
/>
<Button
fullWidth
color="accent"
onClick={handleAddFolderClick}
>
<span>+</span>
<span
style={{
marginLeft: "8px",
}}
></span>
{t("ADD_FOLDER")}
</Button>
</Stack>
</DialogContent>
</Dialog>
<UploadStrategyChoiceModal
open={choiceModalOpen}
onClose={closeChoiceModal}
uploadToSingleCollection={uploadToSingleCollection}
uploadToMultipleCollection={uploadToMultipleCollection}
/>
</>
);
}

View file

@ -1,23 +0,0 @@
import { FlexWrapper } from "@ente/shared/components/Container";
import { CircularProgress, Typography } from "@mui/material";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import watchFolderService from "services/watchFolder/watchFolderService";
import { WatchMapping } from "types/watchFolder";
interface Iprops {
mapping: WatchMapping;
}
export function EntryHeading({ mapping }: Iprops) {
const appContext = useContext(AppContext);
return (
<FlexWrapper gap={1}>
<Typography>{mapping.rootFolderName}</Typography>
{appContext.isFolderSyncRunning &&
watchFolderService.isMappingSyncInProgress(mapping) && (
<CircularProgress size={12} />
)}
</FlexWrapper>
);
}

View file

@ -1,69 +0,0 @@
import {
HorizontalFlex,
SpaceBetweenFlex,
} from "@ente/shared/components/Container";
import FolderCopyOutlinedIcon from "@mui/icons-material/FolderCopyOutlined";
import FolderOpenIcon from "@mui/icons-material/FolderOpen";
import { Tooltip, Typography } from "@mui/material";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import React from "react";
import { WatchMapping } from "types/watchFolder";
import { EntryContainer } from "../styledComponents";
import { UPLOAD_STRATEGY } from "constants/upload";
import { EntryHeading } from "./entryHeading";
import MappingEntryOptions from "./mappingEntryOptions";
interface Iprops {
mapping: WatchMapping;
handleRemoveMapping: (mapping: WatchMapping) => void;
}
export function MappingEntry({ mapping, handleRemoveMapping }: Iprops) {
const appContext = React.useContext(AppContext);
const stopWatching = () => {
handleRemoveMapping(mapping);
};
const confirmStopWatching = () => {
appContext.setDialogMessage({
title: t("STOP_WATCHING_FOLDER"),
content: t("STOP_WATCHING_DIALOG_MESSAGE"),
close: {
text: t("CANCEL"),
variant: "secondary",
},
proceed: {
action: stopWatching,
text: t("YES_STOP"),
variant: "critical",
},
});
};
return (
<SpaceBetweenFlex>
<HorizontalFlex>
{mapping &&
mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? (
<Tooltip title={t("UPLOADED_TO_SINGLE_COLLECTION")}>
<FolderOpenIcon />
</Tooltip>
) : (
<Tooltip title={t("UPLOADED_TO_SEPARATE_COLLECTIONS")}>
<FolderCopyOutlinedIcon />
</Tooltip>
)}
<EntryContainer>
<EntryHeading mapping={mapping} />
<Typography color="text.muted" variant="small">
{mapping.folderPath}
</Typography>
</EntryContainer>
</HorizontalFlex>
<MappingEntryOptions confirmStopWatching={confirmStopWatching} />
</SpaceBetweenFlex>
);
}

View file

@ -1,33 +0,0 @@
import { t } from "i18next";
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
import DoNotDisturbOutlinedIcon from "@mui/icons-material/DoNotDisturbOutlined";
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
interface Iprops {
confirmStopWatching: () => void;
}
export default function MappingEntryOptions({ confirmStopWatching }: Iprops) {
return (
<OverflowMenu
menuPaperProps={{
sx: {
backgroundColor: (theme) =>
theme.colors.background.elevated2,
},
}}
ariaControls={"watch-mapping-option"}
triggerButtonIcon={<MoreHorizIcon />}
>
<OverflowMenuOption
color="critical"
onClick={confirmStopWatching}
startIcon={<DoNotDisturbOutlinedIcon />}
>
{t("STOP_WATCHING")}
</OverflowMenuOption>
</OverflowMenu>
);
}

View file

@ -1,26 +0,0 @@
import { WatchMapping } from "types/watchFolder";
import { MappingEntry } from "../mappingEntry";
import { MappingsContainer } from "../styledComponents";
import { NoMappingsContent } from "./noMappingsContent/noMappingsContent";
interface Iprops {
mappings: WatchMapping[];
handleRemoveWatchMapping: (value: WatchMapping) => void;
}
export function MappingList({ mappings, handleRemoveWatchMapping }: Iprops) {
return mappings.length === 0 ? (
<NoMappingsContent />
) : (
<MappingsContainer>
{mappings.map((mapping) => {
return (
<MappingEntry
key={mapping.rootFolderName}
mapping={mapping}
handleRemoveMapping={handleRemoveWatchMapping}
/>
);
})}
</MappingsContainer>
);
}

View file

@ -1,15 +0,0 @@
import CheckIcon from "@mui/icons-material/Check";
export function CheckmarkIcon() {
return (
<CheckIcon
fontSize="small"
sx={{
display: "inline",
fontSize: "15px",
color: (theme) => theme.palette.secondary.main,
}}
/>
);
}

View file

@ -1,33 +0,0 @@
import { Stack, Typography } from "@mui/material";
import { t } from "i18next";
import { FlexWrapper } from "@ente/shared/components/Container";
import { NoMappingsContainer } from "../../styledComponents";
import { CheckmarkIcon } from "./checkmarkIcon";
export function NoMappingsContent() {
return (
<NoMappingsContainer>
<Stack spacing={1}>
<Typography variant="large" fontWeight={"bold"}>
{t("NO_FOLDERS_ADDED")}
</Typography>
<Typography py={0.5} variant={"small"} color="text.muted">
{t("FOLDERS_AUTOMATICALLY_MONITORED")}
</Typography>
<Typography variant={"small"} color="text.muted">
<FlexWrapper gap={1}>
<CheckmarkIcon />
{t("UPLOAD_NEW_FILES_TO_ENTE")}
</FlexWrapper>
</Typography>
<Typography variant={"small"} color="text.muted">
<FlexWrapper gap={1}>
<CheckmarkIcon />
{t("REMOVE_DELETED_FILES_FROM_ENTE")}
</FlexWrapper>
</Typography>
</Stack>
</NoMappingsContainer>
);
}

View file

@ -1,23 +0,0 @@
import { VerticallyCentered } from "@ente/shared/components/Container";
import { Box } from "@mui/material";
import { styled } from "@mui/material/styles";
export const MappingsContainer = styled(Box)(() => ({
height: "278px",
overflow: "auto",
"&::-webkit-scrollbar": {
width: "4px",
},
}));
export const NoMappingsContainer = styled(VerticallyCentered)({
textAlign: "left",
alignItems: "flex-start",
marginBottom: "32px",
});
export const EntryContainer = styled(Box)({
marginLeft: "12px",
marginRight: "6px",
marginBottom: "12px",
});

View file

@ -14,7 +14,7 @@ import {
getPublicCollectionUID,
} from "services/publicCollectionService";
import { getDisableCFUploadProxyFlag } from "services/userService";
import watchFolderService from "services/watchFolder/watchFolderService";
import watchFolderService from "services/watch";
import { Collection } from "types/collection";
import { EncryptedEnteFile, EnteFile } from "types/file";
import { SetFiles } from "types/gallery";

View file

@ -1,3 +1,8 @@
/**
* @file Interface with the Node.js layer of our desktop app to provide the
* watch folders functionality.
*/
import { ensureElectron } from "@/next/electron";
import log from "@/next/log";
import { UPLOAD_RESULT, UPLOAD_STRATEGY } from "constants/upload";
@ -12,17 +17,11 @@ import {
WatchMappingSyncedFile,
} from "types/watchFolder";
import { groupFilesBasedOnCollectionID } from "utils/file";
import { getValidFilesToUpload } from "utils/watch";
import { removeFromCollection } from "../collectionService";
import { getLocalFiles } from "../fileService";
import { getParentFolderName } from "./utils";
import {
diskFileAddedCallback,
diskFileRemovedCallback,
diskFolderRemovedCallback,
} from "./watchFolderEventHandlers";
import { isSystemFile } from "utils/upload";
import { removeFromCollection } from "./collectionService";
import { getLocalFiles } from "./fileService";
class watchFolderService {
class WatchFolderService {
private eventQueue: EventQueueItem[] = [];
private currentEvent: EventQueueItem;
private currentlySyncedMapping: WatchMapping;
@ -196,12 +195,9 @@ class watchFolderService {
}
}
async removeWatchMapping(folderPath: string) {
try {
await ensureElectron().removeWatchMapping(folderPath);
} catch (e) {
log.error("error while removing watch mapping", e);
}
async mappingsAfterRemovingFolder(folderPath: string) {
await ensureElectron().removeWatchMapping(folderPath);
return await this.getWatchMappings();
}
async getWatchMappings(): Promise<WatchMapping[]> {
@ -641,4 +637,104 @@ class watchFolderService {
}
}
export default new watchFolderService();
const watchFolderService = new WatchFolderService();
export default watchFolderService;
const getParentFolderName = (filePath: string) => {
const folderPath = filePath.substring(0, filePath.lastIndexOf("/"));
const folderName = folderPath.substring(folderPath.lastIndexOf("/") + 1);
return folderName;
};
async function diskFileAddedCallback(file: ElectronFile) {
try {
const collectionNameAndFolderPath =
await watchFolderService.getCollectionNameAndFolderPath(file.path);
if (!collectionNameAndFolderPath) {
return;
}
const { collectionName, folderPath } = collectionNameAndFolderPath;
const event: EventQueueItem = {
type: "upload",
collectionName,
folderPath,
files: [file],
};
watchFolderService.pushEvent(event);
log.info(
`added (upload) to event queue, collectionName:${event.collectionName} folderPath:${event.folderPath}, filesCount: ${event.files.length}`,
);
} catch (e) {
log.error("error while calling diskFileAddedCallback", e);
}
}
async function diskFileRemovedCallback(filePath: string) {
try {
const collectionNameAndFolderPath =
await watchFolderService.getCollectionNameAndFolderPath(filePath);
if (!collectionNameAndFolderPath) {
return;
}
const { collectionName, folderPath } = collectionNameAndFolderPath;
const event: EventQueueItem = {
type: "trash",
collectionName,
folderPath,
paths: [filePath],
};
watchFolderService.pushEvent(event);
log.info(
`added (trash) to event queue collectionName:${event.collectionName} folderPath:${event.folderPath} , pathsCount: ${event.paths.length}`,
);
} catch (e) {
log.error("error while calling diskFileRemovedCallback", e);
}
}
async function diskFolderRemovedCallback(folderPath: string) {
try {
const mappings = await watchFolderService.getWatchMappings();
const mapping = mappings.find(
(mapping) => mapping.folderPath === folderPath,
);
if (!mapping) {
log.info(`folder not found in mappings, ${folderPath}`);
throw Error(`Watch mapping not found`);
}
watchFolderService.pushTrashedDir(folderPath);
log.info(`added trashedDir, ${folderPath}`);
} catch (e) {
log.error("error while calling diskFolderRemovedCallback", e);
}
}
export function getValidFilesToUpload(
files: ElectronFile[],
mapping: WatchMapping,
) {
const uniqueFilePaths = new Set<string>();
return files.filter((file) => {
if (!isSystemFile(file) && !isSyncedOrIgnoredFile(file, mapping)) {
if (!uniqueFilePaths.has(file.path)) {
uniqueFilePaths.add(file.path);
return true;
}
}
return false;
});
}
function isSyncedOrIgnoredFile(file: ElectronFile, mapping: WatchMapping) {
return (
mapping.ignoredFiles.includes(file.path) ||
mapping.syncedFiles.find((f) => f.path === file.path)
);
}

View file

@ -1,5 +0,0 @@
export const getParentFolderName = (filePath: string) => {
const folderPath = filePath.substring(0, filePath.lastIndexOf("/"));
const folderName = folderPath.substring(folderPath.lastIndexOf("/") + 1);
return folderName;
};

View file

@ -1,73 +0,0 @@
import log from "@/next/log";
import { ElectronFile } from "types/upload";
import { EventQueueItem } from "types/watchFolder";
import watchFolderService from "./watchFolderService";
export async function diskFileAddedCallback(file: ElectronFile) {
try {
const collectionNameAndFolderPath =
await watchFolderService.getCollectionNameAndFolderPath(file.path);
if (!collectionNameAndFolderPath) {
return;
}
const { collectionName, folderPath } = collectionNameAndFolderPath;
const event: EventQueueItem = {
type: "upload",
collectionName,
folderPath,
files: [file],
};
watchFolderService.pushEvent(event);
log.info(
`added (upload) to event queue, collectionName:${event.collectionName} folderPath:${event.folderPath}, filesCount: ${event.files.length}`,
);
} catch (e) {
log.error("error while calling diskFileAddedCallback", e);
}
}
export async function diskFileRemovedCallback(filePath: string) {
try {
const collectionNameAndFolderPath =
await watchFolderService.getCollectionNameAndFolderPath(filePath);
if (!collectionNameAndFolderPath) {
return;
}
const { collectionName, folderPath } = collectionNameAndFolderPath;
const event: EventQueueItem = {
type: "trash",
collectionName,
folderPath,
paths: [filePath],
};
watchFolderService.pushEvent(event);
log.info(
`added (trash) to event queue collectionName:${event.collectionName} folderPath:${event.folderPath} , pathsCount: ${event.paths.length}`,
);
} catch (e) {
log.error("error while calling diskFileRemovedCallback", e);
}
}
export async function diskFolderRemovedCallback(folderPath: string) {
try {
const mappings = await watchFolderService.getWatchMappings();
const mapping = mappings.find(
(mapping) => mapping.folderPath === folderPath,
);
if (!mapping) {
log.info(`folder not found in mappings, ${folderPath}`);
throw Error(`Watch mapping not found`);
}
watchFolderService.pushTrashedDir(folderPath);
log.info(`added trashedDir, ${folderPath}`);
} catch (e) {
log.error("error while calling diskFolderRemovedCallback", e);
}
}

View file

@ -1,26 +0,0 @@
import { ElectronFile } from "types/upload";
import { WatchMapping } from "types/watchFolder";
import { isSystemFile } from "utils/upload";
function isSyncedOrIgnoredFile(file: ElectronFile, mapping: WatchMapping) {
return (
mapping.ignoredFiles.includes(file.path) ||
mapping.syncedFiles.find((f) => f.path === file.path)
);
}
export function getValidFilesToUpload(
files: ElectronFile[],
mapping: WatchMapping,
) {
const uniqueFilePaths = new Set<string>();
return files.filter((file) => {
if (!isSystemFile(file) && !isSyncedOrIgnoredFile(file, mapping)) {
if (!uniqueFilePaths.has(file.path)) {
uniqueFilePaths.add(file.path);
return true;
}
}
return false;
});
}

View file

@ -26,20 +26,6 @@ export interface DataStream {
chunkCount: number;
}
export interface WatchMappingSyncedFile {
path: string;
uploadedFileID: number;
collectionID: number;
}
export interface WatchMapping {
rootFolderName: string;
folderPath: string;
uploadStrategy: UPLOAD_STRATEGY;
syncedFiles: WatchMappingSyncedFile[];
ignoredFiles: string[];
}
export interface EventQueueItem {
type: "upload" | "trash";
folderPath: string;

View file

@ -3,7 +3,7 @@
//
// See [Note: types.ts <-> preload.ts <-> ipc.ts]
import type { ElectronFile, WatchMapping } from "./file";
import type { ElectronFile } from "./file";
export interface AppUpdateInfo {
autoUpdatable: boolean;
@ -298,16 +298,16 @@ export interface Electron {
removeWatchMapping: (folderPath: string) => Promise<void>;
getWatchMappings: () => Promise<WatchMapping[]>;
getWatchMappings: () => Promise<FolderWatch[]>;
updateWatchMappingSyncedFiles: (
folderPath: string,
files: WatchMapping["syncedFiles"],
files: FolderWatch["syncedFiles"],
) => Promise<void>;
updateWatchMappingIgnoredFiles: (
folderPath: string,
files: WatchMapping["ignoredFiles"],
files: FolderWatch["ignoredFiles"],
) => Promise<void>;
// - FS legacy
@ -332,3 +332,30 @@ export interface Electron {
setToUploadCollection: (collectionName: string) => Promise<void>;
getDirFiles: (dirPath: string) => Promise<ElectronFile[]>;
}
/**
* A top level folder that was selected by the user for watching.
*
* The user can set up multiple such watches. Each of these can in turn be
* syncing multiple on disk folders to one or more (dependening on the
* {@link uploadStrategy}) Ente albums.
*
* This type is passed across the IPC boundary. It is persisted on the Node.js
* side.
*/
export interface FolderWatch {
rootFolderName: string;
uploadStrategy: number;
folderPath: string;
syncedFiles: FolderWatchSyncedFile[];
ignoredFiles: string[];
}
/**
* An on-disk file that was synced as part of a folder watch.
*/
export interface FolderWatchSyncedFile {
path: string;
uploadedFileID: number;
collectionID: number;
}