Manav Rathi 1 년 전
부모
커밋
170ea0c997
2개의 변경된 파일111개의 추가작업 그리고 125개의 파일을 삭제
  1. 62 79
      web/apps/photos/src/components/WatchFolder.tsx
  2. 49 46
      web/apps/photos/src/services/watch.ts

+ 62 - 79
web/apps/photos/src/components/WatchFolder.tsx

@@ -28,8 +28,8 @@ 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 watcher from "services/watch";
+import { WatchMapping as FolderWatch } from "types/watchFolder";
 import { getImportSuggestion } from "utils/upload";
 
 interface WatchFolderProps {
@@ -38,7 +38,7 @@ interface WatchFolderProps {
 }
 
 export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
-    const [mappings, setMappings] = useState<WatchMapping[]>([]);
+    const [mappings, setMappings] = useState<FolderWatch[]>([]);
     const [inputFolderPath, setInputFolderPath] = useState("");
     const [choiceModalOpen, setChoiceModalOpen] = useState(false);
     const appContext = useContext(AppContext);
@@ -47,7 +47,7 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
 
     useEffect(() => {
         if (!electron) return;
-        watchFolderService.getWatchMappings().then((m) => setMappings(m));
+        watcher.getWatchMappings().then((m) => setMappings(m));
     }, []);
 
     useEffect(() => {
@@ -64,7 +64,7 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
         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)) {
+            if (await watcher.isFolder(path)) {
                 await addFolderForWatching(path);
             }
         }
@@ -91,7 +91,7 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
     };
 
     const handleFolderSelection = async () => {
-        const folderPath = await watchFolderService.selectFolder();
+        const folderPath = await watcher.selectFolder();
         if (folderPath) {
             await addFolderForWatching(folderPath);
         }
@@ -102,19 +102,18 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
         folderPath?: string,
     ) => {
         folderPath = folderPath || inputFolderPath;
-        await watchFolderService.addWatchMapping(
+        await watcher.addWatchMapping(
             folderPath.substring(folderPath.lastIndexOf("/") + 1),
             folderPath,
             uploadStrategy,
         );
         setInputFolderPath("");
-        setMappings(await watchFolderService.getWatchMappings());
+        setMappings(await watcher.getWatchMappings());
     };
 
-    const handleRemoveWatchMapping = (mapping: WatchMapping) => {
-        watchFolderService
-            .mappingsAfterRemovingFolder(mapping.folderPath)
-            .then((ms) => setMappings(ms));
+    const stopWatching = async (watch: FolderWatch) => {
+        await watcher.removeWatchForFolderPath(watch.folderPath);
+        setMappings(await watcher.getWatchMappings());
     };
 
     const closeChoiceModal = () => setChoiceModalOpen(false);
@@ -144,9 +143,9 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
                 </DialogTitleWithCloseButton>
                 <DialogContent sx={{ flex: 1 }}>
                     <Stack spacing={1} p={1.5} height={"100%"}>
-                        <MappingList
-                            mappings={mappings}
-                            handleRemoveWatchMapping={handleRemoveWatchMapping}
+                        <WatchList
+                            watches={mappings}
+                            stopWatching={stopWatching}
                         />
                         <Button
                             fullWidth
@@ -174,53 +173,38 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
     );
 };
 
-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;
+interface WatchList {
+    watches: FolderWatch[];
+    stopWatching: (watch: FolderWatch) => void;
 }
 
-const MappingList: React.FC<MappingListProps> = ({
-    mappings,
-    handleRemoveWatchMapping,
-}) => {
-    return mappings.length === 0 ? (
-        <NoMappingsContent />
+const WatchList: React.FC<WatchList> = ({ watches, stopWatching }) => {
+    return watches.length === 0 ? (
+        <NoWatches />
     ) : (
-        <MappingsContainer>
-            {mappings.map((mapping) => {
+        <WatchesContainer>
+            {watches.map((mapping) => {
                 return (
-                    <MappingEntry
+                    <WatchEntry
                         key={mapping.rootFolderName}
-                        mapping={mapping}
-                        handleRemoveMapping={handleRemoveWatchMapping}
+                        watch={mapping}
+                        stopWatching={stopWatching}
                     />
                 );
             })}
-        </MappingsContainer>
+        </WatchesContainer>
     );
 };
 
-const NoMappingsContent: React.FC = () => {
+const WatchesContainer = styled(Box)(() => ({
+    height: "278px",
+    overflow: "auto",
+    "&::-webkit-scrollbar": {
+        width: "4px",
+    },
+}));
+
+const NoWatches: React.FC = () => {
     return (
         <NoMappingsContainer>
             <Stack spacing={1}>
@@ -247,6 +231,12 @@ const NoMappingsContent: React.FC = () => {
     );
 };
 
+const NoMappingsContainer = styled(VerticallyCentered)({
+    textAlign: "left",
+    alignItems: "flex-start",
+    marginBottom: "32px",
+});
+
 const CheckmarkIcon: React.FC = () => {
     return (
         <CheckIcon
@@ -254,28 +244,20 @@ const CheckmarkIcon: React.FC = () => {
             sx={{
                 display: "inline",
                 fontSize: "15px",
-
                 color: (theme) => theme.palette.secondary.main,
             }}
         />
     );
 };
 
-interface MappingEntryProps {
-    mapping: WatchMapping;
-    handleRemoveMapping: (mapping: WatchMapping) => void;
+interface WatchEntryProps {
+    watch: FolderWatch;
+    stopWatching: (watch: FolderWatch) => void;
 }
 
-const MappingEntry: React.FC<MappingEntryProps> = ({
-    mapping,
-    handleRemoveMapping,
-}) => {
+const WatchEntry: React.FC<WatchEntryProps> = ({ watch, stopWatching }) => {
     const appContext = React.useContext(AppContext);
 
-    const stopWatching = () => {
-        handleRemoveMapping(mapping);
-    };
-
     const confirmStopWatching = () => {
         appContext.setDialogMessage({
             title: t("STOP_WATCHING_FOLDER"),
@@ -285,7 +267,7 @@ const MappingEntry: React.FC<MappingEntryProps> = ({
                 variant: "secondary",
             },
             proceed: {
-                action: stopWatching,
+                action: () => stopWatching(watch),
                 text: t("YES_STOP"),
                 variant: "critical",
             },
@@ -295,8 +277,7 @@ const MappingEntry: React.FC<MappingEntryProps> = ({
     return (
         <SpaceBetweenFlex>
             <HorizontalFlex>
-                {mapping &&
-                mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? (
+                {watch.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? (
                     <Tooltip title={t("UPLOADED_TO_SINGLE_COLLECTION")}>
                         <FolderOpenIcon />
                     </Tooltip>
@@ -306,41 +287,43 @@ const MappingEntry: React.FC<MappingEntryProps> = ({
                     </Tooltip>
                 )}
                 <EntryContainer>
-                    <EntryHeading mapping={mapping} />
+                    <EntryHeading watch={watch} />
                     <Typography color="text.muted" variant="small">
-                        {mapping.folderPath}
+                        {watch.folderPath}
                     </Typography>
                 </EntryContainer>
             </HorizontalFlex>
-            <MappingEntryOptions confirmStopWatching={confirmStopWatching} />
+            <EntryOptions {...{ confirmStopWatching }} />
         </SpaceBetweenFlex>
     );
 };
 
+const EntryContainer = styled(Box)({
+    marginLeft: "12px",
+    marginRight: "6px",
+    marginBottom: "12px",
+});
+
 interface EntryHeadingProps {
-    mapping: WatchMapping;
+    watch: FolderWatch;
 }
 
-const EntryHeading: React.FC<EntryHeadingProps> = ({ mapping }) => {
+const EntryHeading: React.FC<EntryHeadingProps> = ({ watch }) => {
     const appContext = useContext(AppContext);
     return (
         <FlexWrapper gap={1}>
-            <Typography>{mapping.rootFolderName}</Typography>
+            <Typography>{watch.rootFolderName}</Typography>
             {appContext.isFolderSyncRunning &&
-                watchFolderService.isMappingSyncInProgress(mapping) && (
-                    <CircularProgress size={12} />
-                )}
+                watcher.isSyncingWatch(watch) && <CircularProgress size={12} />}
         </FlexWrapper>
     );
 };
 
-interface MappingEntryOptionsProps {
+interface EntryOptionsProps {
     confirmStopWatching: () => void;
 }
 
-const MappingEntryOptions: React.FC<MappingEntryOptionsProps> = ({
-    confirmStopWatching,
-}) => {
+const EntryOptions: React.FC<EntryOptionsProps> = ({ confirmStopWatching }) => {
     return (
         <OverflowMenu
             menuPaperProps={{

+ 49 - 46
web/apps/photos/src/services/watch.ts

@@ -13,7 +13,7 @@ import uploadManager from "services/upload/uploadManager";
 import { Collection } from "types/collection";
 import { EncryptedEnteFile } from "types/file";
 import { ElectronFile, FileWithCollection } from "types/upload";
-import { WatchMapping, WatchMappingSyncedFile } from "types/watchFolder";
+import { WatchMappingSyncedFile } from "types/watchFolder";
 import { groupFilesBasedOnCollectionID } from "utils/file";
 import { isSystemFile } from "utils/upload";
 import { removeFromCollection } from "./collectionService";
@@ -50,7 +50,7 @@ interface WatchEvent {
 class WatchFolderService {
     private eventQueue: WatchEvent[] = [];
     private currentEvent: WatchEvent;
-    private currentlySyncedMapping: WatchMapping;
+    private currentlySyncedMapping: FolderWatch;
     private trashingDirQueue: string[] = [];
     private isEventRunning: boolean = false;
     private uploadRunning: boolean = false;
@@ -94,6 +94,14 @@ class WatchFolderService {
         }
     }
 
+    /**
+     * Return true if we are currently processing an event for the given
+     * {@link watch}
+     */
+    isSyncingWatch(watch: FolderWatch) {
+        return this.currentEvent?.folderPath === watch.folderPath;
+    }
+
     private async syncWithDisk() {
         try {
             const electron = ensureElectron();
@@ -102,7 +110,8 @@ class WatchFolderService {
 
             this.eventQueue = [];
             const { events, deletedFolderPaths } = await deduceEvents(mappings);
-            this.eventQueue = [...this.eventQueue, ...events];
+            log.info(`Folder watch deduced ${events.length} events`);
+            this.eventQueue = this.eventQueue.concat(events);
 
             for (const path of deletedFolderPaths)
                 electron.removeWatchMapping(path);
@@ -113,13 +122,9 @@ class WatchFolderService {
         }
     }
 
-    isMappingSyncInProgress(mapping: WatchMapping) {
-        return this.currentEvent?.folderPath === mapping.folderPath;
-    }
-
     private pushEvent(event: WatchEvent) {
         this.eventQueue.push(event);
-        log.info("Watch event", event);
+        log.info("Folder watch event", event);
         this.debouncedRunNextEvent();
     }
 
@@ -152,12 +157,15 @@ class WatchFolderService {
         }
     }
 
-    async mappingsAfterRemovingFolder(folderPath: string) {
+    /**
+     * Remove the folder watch corresponding to the given root
+     * {@link folderPath}.
+     */
+    async removeWatchForFolderPath(folderPath: string) {
         await ensureElectron().removeWatchMapping(folderPath);
-        return await this.getWatchMappings();
     }
 
-    async getWatchMappings(): Promise<WatchMapping[]> {
+    async getWatchMappings(): Promise<FolderWatch[]> {
         try {
             return (await ensureElectron().getWatchMappings()) ?? [];
         } catch (e) {
@@ -312,8 +320,8 @@ class WatchFolderService {
                 return;
             }
 
-            const syncedFiles: WatchMapping["syncedFiles"] = [];
-            const ignoredFiles: WatchMapping["ignoredFiles"] = [];
+            const syncedFiles: FolderWatch["syncedFiles"] = [];
+            const ignoredFiles: FolderWatch["ignoredFiles"] = [];
 
             for (const fileWithCollection of filesWithCollection) {
                 this.handleUploadedFile(
@@ -361,8 +369,8 @@ class WatchFolderService {
 
     private handleUploadedFile(
         fileWithCollection: FileWithCollection,
-        syncedFiles: WatchMapping["syncedFiles"],
-        ignoredFiles: WatchMapping["ignoredFiles"],
+        syncedFiles: FolderWatch["syncedFiles"],
+        ignoredFiles: FolderWatch["ignoredFiles"],
     ) {
         if (fileWithCollection.isLivePhoto) {
             const imagePath = (
@@ -465,7 +473,7 @@ class WatchFolderService {
         }
     }
 
-    private async trashByIDs(toTrashFiles: WatchMapping["syncedFiles"]) {
+    private async trashByIDs(toTrashFiles: FolderWatch["syncedFiles"]) {
         try {
             const files = await getLocalFiles();
             const toTrashFilesMap = new Map<number, WatchMappingSyncedFile>();
@@ -526,7 +534,7 @@ class WatchFolderService {
             }
 
             return {
-                collectionName: getCollectionNameForMapping(mapping, filePath),
+                collectionName: collectionNameForPath(filePath, mapping),
                 folderPath: mapping.folderPath,
             };
         } catch (e) {
@@ -642,7 +650,7 @@ async function diskFolderRemovedCallback(folderPath: string) {
 
 export function getValidFilesToUpload(
     files: ElectronFile[],
-    mapping: WatchMapping,
+    mapping: FolderWatch,
 ) {
     const uniqueFilePaths = new Set<string>();
     return files.filter((file) => {
@@ -656,7 +664,7 @@ export function getValidFilesToUpload(
     });
 }
 
-function isSyncedOrIgnoredFile(file: ElectronFile, mapping: WatchMapping) {
+function isSyncedOrIgnoredFile(file: ElectronFile, mapping: FolderWatch) {
     return (
         mapping.ignoredFiles.includes(file.path) ||
         mapping.syncedFiles.find((f) => f.path === file.path)
@@ -671,24 +679,26 @@ function isSyncedOrIgnoredFile(file: ElectronFile, mapping: WatchMapping) {
  * longer any no corresponding directory on disk.
  */
 const deduceEvents = async (
-    mappings: FolderWatch[],
+    watches: FolderWatch[],
 ): Promise<{
     events: WatchEvent[];
     deletedFolderPaths: string[];
 }> => {
-    const activeMappings = [];
+    const electron = ensureElectron();
+
+    const activeWatches = [];
     const deletedFolderPaths: string[] = [];
 
-    for (const mapping of mappings) {
-        const valid = await electron.fs.isDir(mapping.folderPath);
-        if (!valid) deletedFolderPaths.push(mapping.folderPath);
-        else activeMappings.push(mapping);
+    for (const watch of watches) {
+        const valid = await electron.fs.isDir(watch.folderPath);
+        if (!valid) deletedFolderPaths.push(watch.folderPath);
+        else activeWatches.push(watch);
     }
 
     const events: WatchEvent[] = [];
 
-    for (const mapping of activeMappings) {
-        const folderPath = mapping.folderPath;
+    for (const watch of activeWatches) {
+        const folderPath = watch.folderPath;
 
         const paths = (await electron.watch.findFiles(folderPath))
             // Filter out hidden files (files whose names begins with a dot)
@@ -696,27 +706,27 @@ const deduceEvents = async (
 
         // Files that are on disk but not yet synced.
         const pathsToUpload = paths.filter(
-            (path) => !isSyncedOrIgnoredPath(path, mapping),
+            (path) => !isSyncedOrIgnoredPath(path, watch),
         );
 
         for (const path of pathsToUpload)
             events.push({
                 action: "upload",
-                collectionName: getCollectionNameForMapping(mapping, path),
                 folderPath,
+                collectionName: collectionNameForPath(path, watch),
                 filePath: path,
             });
 
         // Synced files that are no longer on disk
-        const pathsToRemove = mapping.syncedFiles.filter(
+        const pathsToRemove = watch.syncedFiles.filter(
             (file) => !paths.includes(file.path),
         );
 
         for (const path of pathsToRemove)
             events.push({
-                type: "trash",
-                collectionName: getCollectionNameForMapping(mapping, path),
-                folderPath: mapping.folderPath,
+                action: "trash",
+                folderPath,
+                collectionName: collectionNameForPath(path, watch),
                 filePath: path,
             });
     }
@@ -724,21 +734,14 @@ const deduceEvents = async (
     return { events, deletedFolderPaths };
 };
 
-function isSyncedOrIgnoredPath(path: string, mapping: WatchMapping) {
-    return (
-        mapping.ignoredFiles.includes(path) ||
-        mapping.syncedFiles.find((f) => f.path === path)
-    );
-}
+const isSyncedOrIgnoredPath = (path: string, watch: FolderWatch) =>
+    watch.ignoredFiles.includes(path) ||
+    watch.syncedFiles.find((f) => f.path === path);
 
-const getCollectionNameForMapping = (
-    mapping: WatchMapping,
-    filePath: string,
-) => {
-    return mapping.uploadStrategy === UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
+const collectionNameForPath = (filePath: string, watch: FolderWatch) =>
+    watch.uploadStrategy === UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
         ? parentDirectoryName(filePath)
-        : mapping.rootFolderName;
-};
+        : watch.rootFolderName;
 
 const parentDirectoryName = (filePath: string) => {
     const components = filePath.split("/");