diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index bd29057da..a5de4514f 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -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), ); }; diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index 8a3414c58..1d466d415 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -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( diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index ff2cf505a..2749fa50d 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -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 => ipcRenderer.invoke("removeWatchMapping", folderPath); -const getWatchMappings = (): Promise => +const getWatchMappings = (): Promise => ipcRenderer.invoke("getWatchMappings"); const updateWatchMappingSyncedFiles = ( folderPath: string, - files: WatchMapping["syncedFiles"], + files: FolderWatch["syncedFiles"], ): Promise => ipcRenderer.invoke("updateWatchMappingSyncedFiles", folderPath, files); const updateWatchMappingIgnoredFiles = ( folderPath: string, - files: WatchMapping["ignoredFiles"], + files: FolderWatch["ignoredFiles"], ): Promise => ipcRenderer.invoke("updateWatchMappingIgnoredFiles", folderPath, files); diff --git a/desktop/src/types/ipc.ts b/desktop/src/types/ipc.ts index 3dba231f2..3dae605a8 100644 --- a/desktop/src/types/ipc.ts +++ b/desktop/src/types/ipc.ts @@ -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; } -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 { diff --git a/web/apps/photos/src/components/Sidebar/UtilitySection.tsx b/web/apps/photos/src/components/Sidebar/UtilitySection.tsx index 904eab747..c9c734cd9 100644 --- a/web/apps/photos/src/components/Sidebar/UtilitySection.tsx +++ b/web/apps/photos/src/components/Sidebar/UtilitySection.tsx @@ -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"; diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 4d81b1612..bb3d4fd9d 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -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 { diff --git a/web/apps/photos/src/components/WatchFolder.tsx b/web/apps/photos/src/components/WatchFolder.tsx new file mode 100644 index 000000000..b5ff00b29 --- /dev/null +++ b/web/apps/photos/src/components/WatchFolder.tsx @@ -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 = ({ open, onClose }) => { + const [mappings, setMappings] = useState([]); + 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 ( + <> + + + {t("WATCHED_FOLDERS")} + + + + + + + + + + + ); +}; + +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 = ({ + mappings, + handleRemoveWatchMapping, +}) => { + return mappings.length === 0 ? ( + + ) : ( + + {mappings.map((mapping) => { + return ( + + ); + })} + + ); +}; + +const NoMappingsContent: React.FC = () => { + return ( + + + + {t("NO_FOLDERS_ADDED")} + + + {t("FOLDERS_AUTOMATICALLY_MONITORED")} + + + + + {t("UPLOAD_NEW_FILES_TO_ENTE")} + + + + + + {t("REMOVE_DELETED_FILES_FROM_ENTE")} + + + + + ); +}; + +const CheckmarkIcon: React.FC = () => { + return ( + theme.palette.secondary.main, + }} + /> + ); +}; + +interface MappingEntryProps { + mapping: WatchMapping; + handleRemoveMapping: (mapping: WatchMapping) => void; +} + +const MappingEntry: React.FC = ({ + 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 ( + + + {mapping && + mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? ( + + + + ) : ( + + + + )} + + + + {mapping.folderPath} + + + + + + ); +}; + +interface EntryHeadingProps { + mapping: WatchMapping; +} + +const EntryHeading: React.FC = ({ mapping }) => { + const appContext = useContext(AppContext); + return ( + + {mapping.rootFolderName} + {appContext.isFolderSyncRunning && + watchFolderService.isMappingSyncInProgress(mapping) && ( + + )} + + ); +}; + +interface MappingEntryOptionsProps { + confirmStopWatching: () => void; +} + +const MappingEntryOptions: React.FC = ({ + confirmStopWatching, +}) => { + return ( + + theme.colors.background.elevated2, + }, + }} + ariaControls={"watch-mapping-option"} + triggerButtonIcon={} + > + } + > + {t("STOP_WATCHING")} + + + ); +}; diff --git a/web/apps/photos/src/components/WatchFolder/index.tsx b/web/apps/photos/src/components/WatchFolder/index.tsx deleted file mode 100644 index 4ccfd4138..000000000 --- a/web/apps/photos/src/components/WatchFolder/index.tsx +++ /dev/null @@ -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([]); - 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 ( - <> - - - {t("WATCHED_FOLDERS")} - - - - - - - - - - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingEntry/entryHeading.tsx b/web/apps/photos/src/components/WatchFolder/mappingEntry/entryHeading.tsx deleted file mode 100644 index b34e4277f..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingEntry/entryHeading.tsx +++ /dev/null @@ -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 ( - - {mapping.rootFolderName} - {appContext.isFolderSyncRunning && - watchFolderService.isMappingSyncInProgress(mapping) && ( - - )} - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingEntry/index.tsx b/web/apps/photos/src/components/WatchFolder/mappingEntry/index.tsx deleted file mode 100644 index 819394699..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingEntry/index.tsx +++ /dev/null @@ -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 ( - - - {mapping && - mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? ( - - - - ) : ( - - - - )} - - - - {mapping.folderPath} - - - - - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingEntry/mappingEntryOptions.tsx b/web/apps/photos/src/components/WatchFolder/mappingEntry/mappingEntryOptions.tsx deleted file mode 100644 index 4f3cdc56d..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingEntry/mappingEntryOptions.tsx +++ /dev/null @@ -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 ( - - theme.colors.background.elevated2, - }, - }} - ariaControls={"watch-mapping-option"} - triggerButtonIcon={} - > - } - > - {t("STOP_WATCHING")} - - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingList/index.tsx b/web/apps/photos/src/components/WatchFolder/mappingList/index.tsx deleted file mode 100644 index f2c7b781c..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingList/index.tsx +++ /dev/null @@ -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 ? ( - - ) : ( - - {mappings.map((mapping) => { - return ( - - ); - })} - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/checkmarkIcon.tsx b/web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/checkmarkIcon.tsx deleted file mode 100644 index aedd79404..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/checkmarkIcon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import CheckIcon from "@mui/icons-material/Check"; - -export function CheckmarkIcon() { - return ( - theme.palette.secondary.main, - }} - /> - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/noMappingsContent.tsx b/web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/noMappingsContent.tsx deleted file mode 100644 index a5af6aff9..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingList/noMappingsContent/noMappingsContent.tsx +++ /dev/null @@ -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 ( - - - - {t("NO_FOLDERS_ADDED")} - - - {t("FOLDERS_AUTOMATICALLY_MONITORED")} - - - - - {t("UPLOAD_NEW_FILES_TO_ENTE")} - - - - - - {t("REMOVE_DELETED_FILES_FROM_ENTE")} - - - - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/styledComponents.tsx b/web/apps/photos/src/components/WatchFolder/styledComponents.tsx deleted file mode 100644 index d507bbaa8..000000000 --- a/web/apps/photos/src/components/WatchFolder/styledComponents.tsx +++ /dev/null @@ -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", -}); diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 82b761091..d222999d8 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -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"; diff --git a/web/apps/photos/src/services/watchFolder/watchFolderService.ts b/web/apps/photos/src/services/watch.ts similarity index 85% rename from web/apps/photos/src/services/watchFolder/watchFolderService.ts rename to web/apps/photos/src/services/watch.ts index 791aed445..2d5ef0228 100644 --- a/web/apps/photos/src/services/watchFolder/watchFolderService.ts +++ b/web/apps/photos/src/services/watch.ts @@ -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 { @@ -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(); + 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) + ); +} diff --git a/web/apps/photos/src/services/watchFolder/utils.ts b/web/apps/photos/src/services/watchFolder/utils.ts deleted file mode 100644 index bd6ceb853..000000000 --- a/web/apps/photos/src/services/watchFolder/utils.ts +++ /dev/null @@ -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; -}; diff --git a/web/apps/photos/src/services/watchFolder/watchFolderEventHandlers.ts b/web/apps/photos/src/services/watchFolder/watchFolderEventHandlers.ts deleted file mode 100644 index ba4ad62ee..000000000 --- a/web/apps/photos/src/services/watchFolder/watchFolderEventHandlers.ts +++ /dev/null @@ -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); - } -} diff --git a/web/apps/photos/src/utils/watch/index.ts b/web/apps/photos/src/utils/watch/index.ts deleted file mode 100644 index eb16780dd..000000000 --- a/web/apps/photos/src/utils/watch/index.ts +++ /dev/null @@ -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(); - return files.filter((file) => { - if (!isSystemFile(file) && !isSyncedOrIgnoredFile(file, mapping)) { - if (!uniqueFilePaths.has(file.path)) { - uniqueFilePaths.add(file.path); - return true; - } - } - return false; - }); -} diff --git a/web/packages/next/types/file.ts b/web/packages/next/types/file.ts index e7d3ced5a..dc8a148e9 100644 --- a/web/packages/next/types/file.ts +++ b/web/packages/next/types/file.ts @@ -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; diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 3477d745e..85986b639 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -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; - getWatchMappings: () => Promise; + getWatchMappings: () => Promise; updateWatchMappingSyncedFiles: ( folderPath: string, - files: WatchMapping["syncedFiles"], + files: FolderWatch["syncedFiles"], ) => Promise; updateWatchMappingIgnoredFiles: ( folderPath: string, - files: WatchMapping["ignoredFiles"], + files: FolderWatch["ignoredFiles"], ) => Promise; // - FS legacy @@ -332,3 +332,30 @@ export interface Electron { setToUploadCollection: (collectionName: string) => Promise; getDirFiles: (dirPath: string) => Promise; } + +/** + * 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; +}