This commit is contained in:
Manav Rathi 2024-04-17 18:54:45 +05:30
parent a22423d039
commit 170ea0c997
No known key found for this signature in database
2 changed files with 115 additions and 129 deletions

View file

@ -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,7 +173,30 @@ export const WatchFolder: React.FC<WatchFolderProps> = ({ open, onClose }) => {
);
};
const MappingsContainer = styled(Box)(() => ({
interface WatchList {
watches: FolderWatch[];
stopWatching: (watch: FolderWatch) => void;
}
const WatchList: React.FC<WatchList> = ({ watches, stopWatching }) => {
return watches.length === 0 ? (
<NoWatches />
) : (
<WatchesContainer>
{watches.map((mapping) => {
return (
<WatchEntry
key={mapping.rootFolderName}
watch={mapping}
stopWatching={stopWatching}
/>
);
})}
</WatchesContainer>
);
};
const WatchesContainer = styled(Box)(() => ({
height: "278px",
overflow: "auto",
"&::-webkit-scrollbar": {
@ -182,45 +204,7 @@ const MappingsContainer = styled(Box)(() => ({
},
}));
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 = () => {
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={{

View file

@ -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("/");