|
@@ -4,283 +4,318 @@
|
|
|
*/
|
|
|
|
|
|
import { ensureElectron } from "@/next/electron";
|
|
|
+import { basename, dirname } from "@/next/file";
|
|
|
import log from "@/next/log";
|
|
|
-import { UPLOAD_RESULT, UPLOAD_STRATEGY } from "constants/upload";
|
|
|
+import type {
|
|
|
+ CollectionMapping,
|
|
|
+ FolderWatch,
|
|
|
+ FolderWatchSyncedFile,
|
|
|
+} from "@/next/types/ipc";
|
|
|
+import { UPLOAD_RESULT } from "constants/upload";
|
|
|
import debounce from "debounce";
|
|
|
import uploadManager from "services/upload/uploadManager";
|
|
|
import { Collection } from "types/collection";
|
|
|
import { EncryptedEnteFile } from "types/file";
|
|
|
import { ElectronFile, FileWithCollection } from "types/upload";
|
|
|
-import {
|
|
|
- EventQueueItem,
|
|
|
- WatchMapping,
|
|
|
- WatchMappingSyncedFile,
|
|
|
-} from "types/watchFolder";
|
|
|
import { groupFilesBasedOnCollectionID } from "utils/file";
|
|
|
-import { isSystemFile } from "utils/upload";
|
|
|
+import { isHiddenFile } from "utils/upload";
|
|
|
import { removeFromCollection } from "./collectionService";
|
|
|
import { getLocalFiles } from "./fileService";
|
|
|
|
|
|
-class WatchFolderService {
|
|
|
- private eventQueue: EventQueueItem[] = [];
|
|
|
- private currentEvent: EventQueueItem;
|
|
|
- private currentlySyncedMapping: WatchMapping;
|
|
|
- private trashingDirQueue: string[] = [];
|
|
|
- private isEventRunning: boolean = false;
|
|
|
- private uploadRunning: boolean = false;
|
|
|
+/**
|
|
|
+ * Watch for file system folders and automatically update the corresponding Ente
|
|
|
+ * collections.
|
|
|
+ *
|
|
|
+ * This class relies on APIs exposed over the Electron IPC layer, and thus only
|
|
|
+ * works when we're running inside our desktop app.
|
|
|
+ */
|
|
|
+class FolderWatcher {
|
|
|
+ /** Pending file system events that we need to process. */
|
|
|
+ private eventQueue: WatchEvent[] = [];
|
|
|
+ /** The folder watch whose event we're currently processing */
|
|
|
+ private activeWatch: FolderWatch | undefined;
|
|
|
+ /**
|
|
|
+ * If the file system directory corresponding to the (root) folder path of a
|
|
|
+ * folder watch is deleted on disk, we note down that in this queue so that
|
|
|
+ * we can ignore any file system events that come for it next.
|
|
|
+ *
|
|
|
+ * TODO: is this really needed? the mappings are pre-checked first.
|
|
|
+ */
|
|
|
+ private deletedFolderPaths: string[] = [];
|
|
|
+ /** `true` if we are using the uploader. */
|
|
|
+ private uploadRunning = false;
|
|
|
+ /** `true` if we are temporarily paused to let a user upload go through. */
|
|
|
+ private isPaused = false;
|
|
|
private filePathToUploadedFileIDMap = new Map<string, EncryptedEnteFile>();
|
|
|
private unUploadableFilePaths = new Set<string>();
|
|
|
- private isPaused = false;
|
|
|
- private setElectronFiles: (files: ElectronFile[]) => void;
|
|
|
- private setCollectionName: (collectionName: string) => void;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A function to call when we want to enqueue a new upload of the given list
|
|
|
+ * of file paths to the given Ente collection.
|
|
|
+ *
|
|
|
+ * This is passed as a param to {@link init}.
|
|
|
+ */
|
|
|
+ private upload: (collectionName: string, filePaths: string[]) => void;
|
|
|
+ /**
|
|
|
+ * A function to call when we want to sync with the backend.
|
|
|
+ *
|
|
|
+ * This is passed as a param to {@link init}.
|
|
|
+ */
|
|
|
private syncWithRemote: () => void;
|
|
|
- private setWatchFolderServiceIsRunning: (isRunning: boolean) => void;
|
|
|
+
|
|
|
+ /** A helper function that debounces invocations of {@link runNextEvent}. */
|
|
|
private debouncedRunNextEvent: () => void;
|
|
|
|
|
|
constructor() {
|
|
|
this.debouncedRunNextEvent = debounce(() => this.runNextEvent(), 1000);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Initialize the watcher and start processing file system events.
|
|
|
+ *
|
|
|
+ * This is only called when we're running in the context of our desktop app.
|
|
|
+ *
|
|
|
+ * The caller provides us with the hooks we can use to actually upload the
|
|
|
+ * files, and to sync with remote (say after deletion).
|
|
|
+ */
|
|
|
+ init(
|
|
|
+ upload: (collectionName: string, filePaths: string[]) => void,
|
|
|
+ syncWithRemote: () => void,
|
|
|
+ ) {
|
|
|
+ this.upload = upload;
|
|
|
+ this.syncWithRemote = syncWithRemote;
|
|
|
+ this.registerListeners();
|
|
|
+ this.syncWithDisk();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** `true` if we are currently using the uploader */
|
|
|
isUploadRunning() {
|
|
|
return this.uploadRunning;
|
|
|
}
|
|
|
|
|
|
+ /** `true` if syncing has been temporarily paused */
|
|
|
isSyncPaused() {
|
|
|
return this.isPaused;
|
|
|
}
|
|
|
|
|
|
- async init(
|
|
|
- setElectronFiles: (files: ElectronFile[]) => void,
|
|
|
- setCollectionName: (collectionName: string) => void,
|
|
|
- syncWithRemote: () => void,
|
|
|
- setWatchFolderServiceIsRunning: (isRunning: boolean) => void,
|
|
|
- ) {
|
|
|
- try {
|
|
|
- this.setElectronFiles = setElectronFiles;
|
|
|
- this.setCollectionName = setCollectionName;
|
|
|
- this.syncWithRemote = syncWithRemote;
|
|
|
- this.setWatchFolderServiceIsRunning =
|
|
|
- setWatchFolderServiceIsRunning;
|
|
|
- this.setupWatcherFunctions();
|
|
|
- await this.getAndSyncDiffOfFiles();
|
|
|
- } catch (e) {
|
|
|
- log.error("error while initializing watch service", e);
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * Temporarily pause syncing and cancel any running uploads.
|
|
|
+ *
|
|
|
+ * This frees up the uploader for handling user initated uploads.
|
|
|
+ */
|
|
|
+ pauseRunningSync() {
|
|
|
+ this.isPaused = true;
|
|
|
+ uploadManager.cancelRunningUpload();
|
|
|
}
|
|
|
|
|
|
- async getAndSyncDiffOfFiles() {
|
|
|
- try {
|
|
|
- let mappings = await this.getWatchMappings();
|
|
|
+ /**
|
|
|
+ * Resume from a temporary pause, resyncing from disk.
|
|
|
+ *
|
|
|
+ * Sibling of {@link pauseRunningSync}.
|
|
|
+ */
|
|
|
+ resumePausedSync() {
|
|
|
+ this.isPaused = false;
|
|
|
+ this.syncWithDisk();
|
|
|
+ }
|
|
|
|
|
|
- if (!mappings?.length) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ /** Return the list of folders we are watching for changes. */
|
|
|
+ async getWatches(): Promise<FolderWatch[]> {
|
|
|
+ return await ensureElectron().watch.get();
|
|
|
+ }
|
|
|
|
|
|
- mappings = await this.filterOutDeletedMappings(mappings);
|
|
|
+ /**
|
|
|
+ * Return true if we are currently syncing files that belong to the given
|
|
|
+ * {@link folderPath}.
|
|
|
+ */
|
|
|
+ isSyncingFolder(folderPath: string) {
|
|
|
+ return this.activeWatch?.folderPath == folderPath;
|
|
|
+ }
|
|
|
|
|
|
- this.eventQueue = [];
|
|
|
+ /**
|
|
|
+ * Add a new folder watch for the given root {@link folderPath}
|
|
|
+ *
|
|
|
+ * @param mapping The {@link CollectionMapping} to use to decide which
|
|
|
+ * collection do files belonging to nested directories go to.
|
|
|
+ *
|
|
|
+ * @returns The updated list of watches.
|
|
|
+ */
|
|
|
+ async addWatch(folderPath: string, mapping: CollectionMapping) {
|
|
|
+ const watches = await ensureElectron().watch.add(folderPath, mapping);
|
|
|
+ this.syncWithDisk();
|
|
|
+ return watches;
|
|
|
+ }
|
|
|
|
|
|
- for (const mapping of mappings) {
|
|
|
- const filesOnDisk: ElectronFile[] =
|
|
|
- await ensureElectron().getDirFiles(mapping.folderPath);
|
|
|
+ /**
|
|
|
+ * Remove the folder watch for the given root {@link folderPath}.
|
|
|
+ *
|
|
|
+ * @returns The updated list of watches.
|
|
|
+ */
|
|
|
+ async removeWatch(folderPath: string) {
|
|
|
+ return await ensureElectron().watch.remove(folderPath);
|
|
|
+ }
|
|
|
|
|
|
- this.uploadDiffOfFiles(mapping, filesOnDisk);
|
|
|
- this.trashDiffOfFiles(mapping, filesOnDisk);
|
|
|
- }
|
|
|
+ private async syncWithDisk() {
|
|
|
+ try {
|
|
|
+ const watches = await this.getWatches();
|
|
|
+ if (!watches) return;
|
|
|
+
|
|
|
+ this.eventQueue = [];
|
|
|
+ const events = await deduceEvents(watches);
|
|
|
+ log.info(`Folder watch deduced ${events.length} events`);
|
|
|
+ this.eventQueue = this.eventQueue.concat(events);
|
|
|
+
|
|
|
+ this.debouncedRunNextEvent();
|
|
|
} catch (e) {
|
|
|
- log.error("error while getting and syncing diff of files", e);
|
|
|
+ log.error("Ignoring error while syncing watched folders", e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- isMappingSyncInProgress(mapping: WatchMapping) {
|
|
|
- return this.currentEvent?.folderPath === mapping.folderPath;
|
|
|
+ pushEvent(event: WatchEvent) {
|
|
|
+ this.eventQueue.push(event);
|
|
|
+ log.info("Folder watch event", event);
|
|
|
+ this.debouncedRunNextEvent();
|
|
|
}
|
|
|
|
|
|
- private uploadDiffOfFiles(
|
|
|
- mapping: WatchMapping,
|
|
|
- filesOnDisk: ElectronFile[],
|
|
|
- ) {
|
|
|
- const filesToUpload = getValidFilesToUpload(filesOnDisk, mapping);
|
|
|
-
|
|
|
- if (filesToUpload.length > 0) {
|
|
|
- for (const file of filesToUpload) {
|
|
|
- const event: EventQueueItem = {
|
|
|
- type: "upload",
|
|
|
- collectionName: this.getCollectionNameForMapping(
|
|
|
- mapping,
|
|
|
- file.path,
|
|
|
- ),
|
|
|
- folderPath: mapping.folderPath,
|
|
|
- files: [file],
|
|
|
- };
|
|
|
- this.pushEvent(event);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ private registerListeners() {
|
|
|
+ const watch = ensureElectron().watch;
|
|
|
|
|
|
- private trashDiffOfFiles(
|
|
|
- mapping: WatchMapping,
|
|
|
- filesOnDisk: ElectronFile[],
|
|
|
- ) {
|
|
|
- const filesToRemove = mapping.syncedFiles.filter((file) => {
|
|
|
- return !filesOnDisk.find(
|
|
|
- (electronFile) => electronFile.path === file.path,
|
|
|
- );
|
|
|
+ // [Note: File renames during folder watch]
|
|
|
+ //
|
|
|
+ // Renames come as two file system events - an `onAddFile` + an
|
|
|
+ // `onRemoveFile` - in an arbitrary order.
|
|
|
+
|
|
|
+ watch.onAddFile((path: string, watch: FolderWatch) => {
|
|
|
+ this.pushEvent({
|
|
|
+ action: "upload",
|
|
|
+ collectionName: collectionNameForPath(path, watch),
|
|
|
+ folderPath: watch.folderPath,
|
|
|
+ filePath: path,
|
|
|
+ });
|
|
|
});
|
|
|
|
|
|
- if (filesToRemove.length > 0) {
|
|
|
- for (const file of filesToRemove) {
|
|
|
- const event: EventQueueItem = {
|
|
|
- type: "trash",
|
|
|
- collectionName: this.getCollectionNameForMapping(
|
|
|
- mapping,
|
|
|
- file.path,
|
|
|
- ),
|
|
|
- folderPath: mapping.folderPath,
|
|
|
- paths: [file.path],
|
|
|
- };
|
|
|
- this.pushEvent(event);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ watch.onRemoveFile((path: string, watch: FolderWatch) => {
|
|
|
+ this.pushEvent({
|
|
|
+ action: "trash",
|
|
|
+ collectionName: collectionNameForPath(path, watch),
|
|
|
+ folderPath: watch.folderPath,
|
|
|
+ filePath: path,
|
|
|
+ });
|
|
|
+ });
|
|
|
|
|
|
- private async filterOutDeletedMappings(
|
|
|
- mappings: WatchMapping[],
|
|
|
- ): Promise<WatchMapping[]> {
|
|
|
- const notDeletedMappings = [];
|
|
|
- for (const mapping of mappings) {
|
|
|
- const mappingExists = await ensureElectron().isFolder(
|
|
|
- mapping.folderPath,
|
|
|
- );
|
|
|
- if (!mappingExists) {
|
|
|
- ensureElectron().removeWatchMapping(mapping.folderPath);
|
|
|
- } else {
|
|
|
- notDeletedMappings.push(mapping);
|
|
|
+ watch.onRemoveDir((path: string, watch: FolderWatch) => {
|
|
|
+ if (path == watch.folderPath) {
|
|
|
+ log.info(
|
|
|
+ `Received file system delete event for a watched folder at ${path}`,
|
|
|
+ );
|
|
|
+ this.deletedFolderPaths.push(path);
|
|
|
}
|
|
|
- }
|
|
|
- return notDeletedMappings;
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- pushEvent(event: EventQueueItem) {
|
|
|
- this.eventQueue.push(event);
|
|
|
- this.debouncedRunNextEvent();
|
|
|
- }
|
|
|
+ private async runNextEvent() {
|
|
|
+ if (this.eventQueue.length == 0 || this.activeWatch || this.isPaused)
|
|
|
+ return;
|
|
|
|
|
|
- async pushTrashedDir(path: string) {
|
|
|
- this.trashingDirQueue.push(path);
|
|
|
- }
|
|
|
+ const skip = (reason: string) => {
|
|
|
+ log.info(`Ignoring event since ${reason}`);
|
|
|
+ this.debouncedRunNextEvent();
|
|
|
+ };
|
|
|
|
|
|
- private setupWatcherFunctions() {
|
|
|
- ensureElectron().registerWatcherFunctions(
|
|
|
- diskFileAddedCallback,
|
|
|
- diskFileRemovedCallback,
|
|
|
- diskFolderRemovedCallback,
|
|
|
+ const event = this.dequeueClubbedEvent();
|
|
|
+ log.info(
|
|
|
+ `Processing ${event.action} event for folder watch ${event.folderPath} (collectionName ${event.collectionName}, ${event.filePaths.length} files)`,
|
|
|
);
|
|
|
- }
|
|
|
|
|
|
- async addWatchMapping(
|
|
|
- rootFolderName: string,
|
|
|
- folderPath: string,
|
|
|
- uploadStrategy: UPLOAD_STRATEGY,
|
|
|
- ) {
|
|
|
- try {
|
|
|
- await ensureElectron().addWatchMapping(
|
|
|
- rootFolderName,
|
|
|
- folderPath,
|
|
|
- uploadStrategy,
|
|
|
- );
|
|
|
- this.getAndSyncDiffOfFiles();
|
|
|
- } catch (e) {
|
|
|
- log.error("error while adding watch mapping", e);
|
|
|
+ const watch = (await this.getWatches()).find(
|
|
|
+ (watch) => watch.folderPath == event.folderPath,
|
|
|
+ );
|
|
|
+ if (!watch) {
|
|
|
+ // Possibly stale
|
|
|
+ skip(`no folder watch for found for ${event.folderPath}`);
|
|
|
+ return;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- async mappingsAfterRemovingFolder(folderPath: string) {
|
|
|
- await ensureElectron().removeWatchMapping(folderPath);
|
|
|
- return await this.getWatchMappings();
|
|
|
- }
|
|
|
+ if (event.action === "upload") {
|
|
|
+ const paths = pathsToUpload(event.filePaths, watch);
|
|
|
+ if (paths.length == 0) {
|
|
|
+ skip("none of the files need uploading");
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- async getWatchMappings(): Promise<WatchMapping[]> {
|
|
|
- try {
|
|
|
- return (await ensureElectron().getWatchMappings()) ?? [];
|
|
|
- } catch (e) {
|
|
|
- log.error("error while getting watch mappings", e);
|
|
|
- return [];
|
|
|
- }
|
|
|
- }
|
|
|
+ // Here we pass control to the uploader. When the upload is done,
|
|
|
+ // the uploader will notify us by calling allFileUploadsDone.
|
|
|
|
|
|
- private setIsEventRunning(isEventRunning: boolean) {
|
|
|
- this.isEventRunning = isEventRunning;
|
|
|
- this.setWatchFolderServiceIsRunning(isEventRunning);
|
|
|
- }
|
|
|
+ this.activeWatch = watch;
|
|
|
+ this.uploadRunning = true;
|
|
|
|
|
|
- private async runNextEvent() {
|
|
|
- try {
|
|
|
- if (
|
|
|
- this.eventQueue.length === 0 ||
|
|
|
- this.isEventRunning ||
|
|
|
- this.isPaused
|
|
|
- ) {
|
|
|
+ const collectionName = event.collectionName;
|
|
|
+ log.info(
|
|
|
+ `Folder watch requested upload of ${paths.length} files to collection ${collectionName}`,
|
|
|
+ );
|
|
|
+
|
|
|
+ this.upload(collectionName, paths);
|
|
|
+ } else {
|
|
|
+ if (this.pruneFileEventsFromDeletedFolderPaths()) {
|
|
|
+ skip("event was from a deleted folder path");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const event = this.clubSameCollectionEvents();
|
|
|
- log.info(
|
|
|
- `running event type:${event.type} collectionName:${event.collectionName} folderPath:${event.folderPath} , fileCount:${event.files?.length} pathsCount: ${event.paths?.length}`,
|
|
|
+ const [removed, rest] = watch.syncedFiles.reduce(
|
|
|
+ ([removed, rest], { path }) => {
|
|
|
+ (event.filePaths.includes(path) ? rest : removed).push(
|
|
|
+ watch,
|
|
|
+ );
|
|
|
+ return [removed, rest];
|
|
|
+ },
|
|
|
+ [[], []],
|
|
|
);
|
|
|
- const mappings = await this.getWatchMappings();
|
|
|
- const mapping = mappings.find(
|
|
|
- (mapping) => mapping.folderPath === event.folderPath,
|
|
|
- );
|
|
|
- if (!mapping) {
|
|
|
- throw Error("no Mapping found for event");
|
|
|
- }
|
|
|
- log.info(
|
|
|
- `mapping for event rootFolder: ${mapping.rootFolderName} folderPath: ${mapping.folderPath} uploadStrategy: ${mapping.uploadStrategy} syncedFilesCount: ${mapping.syncedFiles.length} ignoredFilesCount ${mapping.ignoredFiles.length}`,
|
|
|
+
|
|
|
+ this.activeWatch = watch;
|
|
|
+
|
|
|
+ await this.moveToTrash(removed);
|
|
|
+
|
|
|
+ await ensureElectron().watch.updateSyncedFiles(
|
|
|
+ rest,
|
|
|
+ watch.folderPath,
|
|
|
);
|
|
|
- if (event.type === "upload") {
|
|
|
- event.files = getValidFilesToUpload(event.files, mapping);
|
|
|
- log.info(`valid files count: ${event.files?.length}`);
|
|
|
- if (event.files.length === 0) {
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
- this.currentEvent = event;
|
|
|
- this.currentlySyncedMapping = mapping;
|
|
|
|
|
|
- this.setIsEventRunning(true);
|
|
|
- if (event.type === "upload") {
|
|
|
- this.processUploadEvent();
|
|
|
- } else {
|
|
|
- await this.processTrashEvent();
|
|
|
- this.setIsEventRunning(false);
|
|
|
- setTimeout(() => this.runNextEvent(), 0);
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- log.error("runNextEvent failed", e);
|
|
|
+ this.activeWatch = undefined;
|
|
|
+
|
|
|
+ this.debouncedRunNextEvent();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private async processUploadEvent() {
|
|
|
- try {
|
|
|
- this.uploadRunning = true;
|
|
|
+ /**
|
|
|
+ * Batch the next run of events with the same action, collection and folder
|
|
|
+ * path into a single clubbed event that contains the list of all effected
|
|
|
+ * file paths from the individual events.
|
|
|
+ */
|
|
|
+ private dequeueClubbedEvent(): ClubbedWatchEvent | undefined {
|
|
|
+ const event = this.eventQueue.shift();
|
|
|
+ if (!event) return undefined;
|
|
|
|
|
|
- this.setCollectionName(this.currentEvent.collectionName);
|
|
|
- this.setElectronFiles(this.currentEvent.files);
|
|
|
- } catch (e) {
|
|
|
- log.error("error while running next upload", e);
|
|
|
+ const filePaths = [event.filePath];
|
|
|
+ while (
|
|
|
+ this.eventQueue.length > 0 &&
|
|
|
+ event.action === this.eventQueue[0].action &&
|
|
|
+ event.folderPath === this.eventQueue[0].folderPath &&
|
|
|
+ event.collectionName === this.eventQueue[0].collectionName
|
|
|
+ ) {
|
|
|
+ filePaths.push(this.eventQueue[0].filePath);
|
|
|
+ this.eventQueue.shift();
|
|
|
}
|
|
|
+ return { ...event, filePaths };
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Callback invoked by the uploader whenever a file we requested to
|
|
|
+ * {@link upload} gets uploaded.
|
|
|
+ */
|
|
|
async onFileUpload(
|
|
|
fileUploadResult: UPLOAD_RESULT,
|
|
|
fileWithCollection: FileWithCollection,
|
|
|
file: EncryptedEnteFile,
|
|
|
) {
|
|
|
- log.debug(() => `onFileUpload called`);
|
|
|
- if (!this.isUploadRunning()) {
|
|
|
- return;
|
|
|
- }
|
|
|
if (
|
|
|
[
|
|
|
UPLOAD_RESULT.ADDED_SYMLINK,
|
|
@@ -328,191 +363,151 @@ class WatchFolderService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Callback invoked by the uploader whenever all the files we requested to
|
|
|
+ * {@link upload} get uploaded.
|
|
|
+ */
|
|
|
async allFileUploadsDone(
|
|
|
filesWithCollection: FileWithCollection[],
|
|
|
collections: Collection[],
|
|
|
) {
|
|
|
- try {
|
|
|
- log.debug(
|
|
|
- () =>
|
|
|
- `allFileUploadsDone,${JSON.stringify(
|
|
|
- filesWithCollection,
|
|
|
- )} ${JSON.stringify(collections)}`,
|
|
|
- );
|
|
|
- const collection = collections.find(
|
|
|
- (collection) =>
|
|
|
- collection.id === filesWithCollection[0].collectionID,
|
|
|
- );
|
|
|
- log.debug(() => `got collection ${!!collection}`);
|
|
|
- log.debug(
|
|
|
- () =>
|
|
|
- `${this.isEventRunning} ${this.currentEvent.collectionName} ${collection?.name}`,
|
|
|
- );
|
|
|
- if (
|
|
|
- !this.isEventRunning ||
|
|
|
- this.currentEvent.collectionName !== collection?.name
|
|
|
- ) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ const electron = ensureElectron();
|
|
|
+ const watch = this.activeWatch;
|
|
|
+
|
|
|
+ log.debug(() =>
|
|
|
+ JSON.stringify({
|
|
|
+ f: "watch/allFileUploadsDone",
|
|
|
+ filesWithCollection,
|
|
|
+ collections,
|
|
|
+ watch,
|
|
|
+ }),
|
|
|
+ );
|
|
|
|
|
|
- const syncedFiles: WatchMapping["syncedFiles"] = [];
|
|
|
- const ignoredFiles: WatchMapping["ignoredFiles"] = [];
|
|
|
+ const { syncedFiles, ignoredFiles } =
|
|
|
+ this.parseAllFileUploadsDone(filesWithCollection);
|
|
|
|
|
|
- for (const fileWithCollection of filesWithCollection) {
|
|
|
- this.handleUploadedFile(
|
|
|
- fileWithCollection,
|
|
|
- syncedFiles,
|
|
|
- ignoredFiles,
|
|
|
- );
|
|
|
- }
|
|
|
+ log.debug(() =>
|
|
|
+ JSON.stringify({
|
|
|
+ f: "watch/allFileUploadsDone",
|
|
|
+ syncedFiles,
|
|
|
+ ignoredFiles,
|
|
|
+ }),
|
|
|
+ );
|
|
|
|
|
|
- log.debug(() => `syncedFiles ${JSON.stringify(syncedFiles)}`);
|
|
|
- log.debug(() => `ignoredFiles ${JSON.stringify(ignoredFiles)}`);
|
|
|
-
|
|
|
- if (syncedFiles.length > 0) {
|
|
|
- this.currentlySyncedMapping.syncedFiles = [
|
|
|
- ...this.currentlySyncedMapping.syncedFiles,
|
|
|
- ...syncedFiles,
|
|
|
- ];
|
|
|
- await ensureElectron().updateWatchMappingSyncedFiles(
|
|
|
- this.currentlySyncedMapping.folderPath,
|
|
|
- this.currentlySyncedMapping.syncedFiles,
|
|
|
- );
|
|
|
- }
|
|
|
- if (ignoredFiles.length > 0) {
|
|
|
- this.currentlySyncedMapping.ignoredFiles = [
|
|
|
- ...this.currentlySyncedMapping.ignoredFiles,
|
|
|
- ...ignoredFiles,
|
|
|
- ];
|
|
|
- await ensureElectron().updateWatchMappingIgnoredFiles(
|
|
|
- this.currentlySyncedMapping.folderPath,
|
|
|
- this.currentlySyncedMapping.ignoredFiles,
|
|
|
- );
|
|
|
- }
|
|
|
+ if (syncedFiles.length > 0)
|
|
|
+ await electron.watch.updateSyncedFiles(
|
|
|
+ watch.syncedFiles.concat(syncedFiles),
|
|
|
+ watch.folderPath,
|
|
|
+ );
|
|
|
|
|
|
- this.runPostUploadsAction();
|
|
|
- } catch (e) {
|
|
|
- log.error("error while running all file uploads done", e);
|
|
|
- }
|
|
|
- }
|
|
|
+ if (ignoredFiles.length > 0)
|
|
|
+ await electron.watch.updateIgnoredFiles(
|
|
|
+ watch.ignoredFiles.concat(ignoredFiles),
|
|
|
+ watch.folderPath,
|
|
|
+ );
|
|
|
|
|
|
- private runPostUploadsAction() {
|
|
|
- this.setIsEventRunning(false);
|
|
|
+ this.activeWatch = undefined;
|
|
|
this.uploadRunning = false;
|
|
|
- this.runNextEvent();
|
|
|
- }
|
|
|
|
|
|
- private handleUploadedFile(
|
|
|
- fileWithCollection: FileWithCollection,
|
|
|
- syncedFiles: WatchMapping["syncedFiles"],
|
|
|
- ignoredFiles: WatchMapping["ignoredFiles"],
|
|
|
- ) {
|
|
|
- if (fileWithCollection.isLivePhoto) {
|
|
|
- const imagePath = (
|
|
|
- fileWithCollection.livePhotoAssets.image as ElectronFile
|
|
|
- ).path;
|
|
|
- const videoPath = (
|
|
|
- fileWithCollection.livePhotoAssets.video as ElectronFile
|
|
|
- ).path;
|
|
|
-
|
|
|
- if (
|
|
|
- this.filePathToUploadedFileIDMap.has(imagePath) &&
|
|
|
- this.filePathToUploadedFileIDMap.has(videoPath)
|
|
|
- ) {
|
|
|
- const imageFile = {
|
|
|
- path: imagePath,
|
|
|
- uploadedFileID:
|
|
|
- this.filePathToUploadedFileIDMap.get(imagePath).id,
|
|
|
- collectionID:
|
|
|
- this.filePathToUploadedFileIDMap.get(imagePath)
|
|
|
- .collectionID,
|
|
|
- };
|
|
|
- const videoFile = {
|
|
|
- path: videoPath,
|
|
|
- uploadedFileID:
|
|
|
- this.filePathToUploadedFileIDMap.get(videoPath).id,
|
|
|
- collectionID:
|
|
|
- this.filePathToUploadedFileIDMap.get(videoPath)
|
|
|
- .collectionID,
|
|
|
- };
|
|
|
- syncedFiles.push(imageFile);
|
|
|
- syncedFiles.push(videoFile);
|
|
|
- log.debug(
|
|
|
- () =>
|
|
|
- `added image ${JSON.stringify(
|
|
|
- imageFile,
|
|
|
- )} and video file ${JSON.stringify(
|
|
|
- videoFile,
|
|
|
- )} to uploadedFiles`,
|
|
|
- );
|
|
|
- } else if (
|
|
|
- this.unUploadableFilePaths.has(imagePath) &&
|
|
|
- this.unUploadableFilePaths.has(videoPath)
|
|
|
- ) {
|
|
|
- ignoredFiles.push(imagePath);
|
|
|
- ignoredFiles.push(videoPath);
|
|
|
- log.debug(
|
|
|
- () =>
|
|
|
- `added image ${imagePath} and video file ${videoPath} to rejectedFiles`,
|
|
|
- );
|
|
|
- }
|
|
|
- this.filePathToUploadedFileIDMap.delete(imagePath);
|
|
|
- this.filePathToUploadedFileIDMap.delete(videoPath);
|
|
|
- } else {
|
|
|
- const filePath = (fileWithCollection.file as ElectronFile).path;
|
|
|
-
|
|
|
- if (this.filePathToUploadedFileIDMap.has(filePath)) {
|
|
|
- const file = {
|
|
|
- path: filePath,
|
|
|
- uploadedFileID:
|
|
|
- this.filePathToUploadedFileIDMap.get(filePath).id,
|
|
|
- collectionID:
|
|
|
- this.filePathToUploadedFileIDMap.get(filePath)
|
|
|
- .collectionID,
|
|
|
- };
|
|
|
- syncedFiles.push(file);
|
|
|
- log.debug(() => `added file ${JSON.stringify(file)}`);
|
|
|
- } else if (this.unUploadableFilePaths.has(filePath)) {
|
|
|
- ignoredFiles.push(filePath);
|
|
|
- log.debug(() => `added file ${filePath} to rejectedFiles`);
|
|
|
- }
|
|
|
- this.filePathToUploadedFileIDMap.delete(filePath);
|
|
|
- }
|
|
|
+ this.debouncedRunNextEvent();
|
|
|
}
|
|
|
|
|
|
- private async processTrashEvent() {
|
|
|
- try {
|
|
|
- if (this.checkAndIgnoreIfFileEventsFromTrashedDir()) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ private parseAllFileUploadsDone(filesWithCollection: FileWithCollection[]) {
|
|
|
+ const syncedFiles: FolderWatch["syncedFiles"] = [];
|
|
|
+ const ignoredFiles: FolderWatch["ignoredFiles"] = [];
|
|
|
|
|
|
- const { paths } = this.currentEvent;
|
|
|
- const filePathsToRemove = new Set(paths);
|
|
|
+ for (const fileWithCollection of filesWithCollection) {
|
|
|
+ if (fileWithCollection.isLivePhoto) {
|
|
|
+ const imagePath = (
|
|
|
+ fileWithCollection.livePhotoAssets.image as ElectronFile
|
|
|
+ ).path;
|
|
|
+ const videoPath = (
|
|
|
+ fileWithCollection.livePhotoAssets.video as ElectronFile
|
|
|
+ ).path;
|
|
|
+
|
|
|
+ if (
|
|
|
+ this.filePathToUploadedFileIDMap.has(imagePath) &&
|
|
|
+ this.filePathToUploadedFileIDMap.has(videoPath)
|
|
|
+ ) {
|
|
|
+ const imageFile = {
|
|
|
+ path: imagePath,
|
|
|
+ uploadedFileID:
|
|
|
+ this.filePathToUploadedFileIDMap.get(imagePath).id,
|
|
|
+ collectionID:
|
|
|
+ this.filePathToUploadedFileIDMap.get(imagePath)
|
|
|
+ .collectionID,
|
|
|
+ };
|
|
|
+ const videoFile = {
|
|
|
+ path: videoPath,
|
|
|
+ uploadedFileID:
|
|
|
+ this.filePathToUploadedFileIDMap.get(videoPath).id,
|
|
|
+ collectionID:
|
|
|
+ this.filePathToUploadedFileIDMap.get(videoPath)
|
|
|
+ .collectionID,
|
|
|
+ };
|
|
|
+ syncedFiles.push(imageFile);
|
|
|
+ syncedFiles.push(videoFile);
|
|
|
+ log.debug(
|
|
|
+ () =>
|
|
|
+ `added image ${JSON.stringify(
|
|
|
+ imageFile,
|
|
|
+ )} and video file ${JSON.stringify(
|
|
|
+ videoFile,
|
|
|
+ )} to uploadedFiles`,
|
|
|
+ );
|
|
|
+ } else if (
|
|
|
+ this.unUploadableFilePaths.has(imagePath) &&
|
|
|
+ this.unUploadableFilePaths.has(videoPath)
|
|
|
+ ) {
|
|
|
+ ignoredFiles.push(imagePath);
|
|
|
+ ignoredFiles.push(videoPath);
|
|
|
+ log.debug(
|
|
|
+ () =>
|
|
|
+ `added image ${imagePath} and video file ${videoPath} to rejectedFiles`,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ this.filePathToUploadedFileIDMap.delete(imagePath);
|
|
|
+ this.filePathToUploadedFileIDMap.delete(videoPath);
|
|
|
+ } else {
|
|
|
+ const filePath = (fileWithCollection.file as ElectronFile).path;
|
|
|
+
|
|
|
+ if (this.filePathToUploadedFileIDMap.has(filePath)) {
|
|
|
+ const file = {
|
|
|
+ path: filePath,
|
|
|
+ uploadedFileID:
|
|
|
+ this.filePathToUploadedFileIDMap.get(filePath).id,
|
|
|
+ collectionID:
|
|
|
+ this.filePathToUploadedFileIDMap.get(filePath)
|
|
|
+ .collectionID,
|
|
|
+ };
|
|
|
+ syncedFiles.push(file);
|
|
|
+ log.debug(() => `added file ${JSON.stringify(file)}`);
|
|
|
+ } else if (this.unUploadableFilePaths.has(filePath)) {
|
|
|
+ ignoredFiles.push(filePath);
|
|
|
+ log.debug(() => `added file ${filePath} to rejectedFiles`);
|
|
|
+ }
|
|
|
+ this.filePathToUploadedFileIDMap.delete(filePath);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- const files = this.currentlySyncedMapping.syncedFiles.filter(
|
|
|
- (file) => filePathsToRemove.has(file.path),
|
|
|
- );
|
|
|
+ return { syncedFiles, ignoredFiles };
|
|
|
+ }
|
|
|
|
|
|
- await this.trashByIDs(files);
|
|
|
+ private pruneFileEventsFromDeletedFolderPaths() {
|
|
|
+ const deletedFolderPath = this.deletedFolderPaths.shift();
|
|
|
+ if (!deletedFolderPath) return false;
|
|
|
|
|
|
- this.currentlySyncedMapping.syncedFiles =
|
|
|
- this.currentlySyncedMapping.syncedFiles.filter(
|
|
|
- (file) => !filePathsToRemove.has(file.path),
|
|
|
- );
|
|
|
- await ensureElectron().updateWatchMappingSyncedFiles(
|
|
|
- this.currentlySyncedMapping.folderPath,
|
|
|
- this.currentlySyncedMapping.syncedFiles,
|
|
|
- );
|
|
|
- } catch (e) {
|
|
|
- log.error("error while running next trash", e);
|
|
|
- }
|
|
|
+ this.eventQueue = this.eventQueue.filter(
|
|
|
+ (event) => !event.filePath.startsWith(deletedFolderPath),
|
|
|
+ );
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
- private async trashByIDs(toTrashFiles: WatchMapping["syncedFiles"]) {
|
|
|
+ private async moveToTrash(syncedFiles: FolderWatch["syncedFiles"]) {
|
|
|
try {
|
|
|
const files = await getLocalFiles();
|
|
|
- const toTrashFilesMap = new Map<number, WatchMappingSyncedFile>();
|
|
|
- for (const file of toTrashFiles) {
|
|
|
+ const toTrashFilesMap = new Map<number, FolderWatchSyncedFile>();
|
|
|
+ for (const file of syncedFiles) {
|
|
|
toTrashFilesMap.set(file.uploadedFileID, file);
|
|
|
}
|
|
|
const filesToTrash = files.filter((file) => {
|
|
@@ -537,204 +532,116 @@ class WatchFolderService {
|
|
|
log.error("error while trashing by IDs", e);
|
|
|
}
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- private checkAndIgnoreIfFileEventsFromTrashedDir() {
|
|
|
- if (this.trashingDirQueue.length !== 0) {
|
|
|
- this.ignoreFileEventsFromTrashedDir(this.trashingDirQueue[0]);
|
|
|
- this.trashingDirQueue.shift();
|
|
|
- return true;
|
|
|
- }
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- private ignoreFileEventsFromTrashedDir(trashingDir: string) {
|
|
|
- this.eventQueue = this.eventQueue.filter((event) =>
|
|
|
- event.paths.every((path) => !path.startsWith(trashingDir)),
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- async getCollectionNameAndFolderPath(filePath: string) {
|
|
|
- try {
|
|
|
- const mappings = await this.getWatchMappings();
|
|
|
-
|
|
|
- const mapping = mappings.find(
|
|
|
- (mapping) =>
|
|
|
- filePath.length > mapping.folderPath.length &&
|
|
|
- filePath.startsWith(mapping.folderPath) &&
|
|
|
- filePath[mapping.folderPath.length] === "/",
|
|
|
- );
|
|
|
-
|
|
|
- if (!mapping) {
|
|
|
- throw Error(`no mapping found`);
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- collectionName: this.getCollectionNameForMapping(
|
|
|
- mapping,
|
|
|
- filePath,
|
|
|
- ),
|
|
|
- folderPath: mapping.folderPath,
|
|
|
- };
|
|
|
- } catch (e) {
|
|
|
- log.error("error while getting collection name", e);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private getCollectionNameForMapping(
|
|
|
- mapping: WatchMapping,
|
|
|
- filePath: string,
|
|
|
- ) {
|
|
|
- return mapping.uploadStrategy === UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
|
|
|
- ? getParentFolderName(filePath)
|
|
|
- : mapping.rootFolderName;
|
|
|
- }
|
|
|
-
|
|
|
- async selectFolder(): Promise<string> {
|
|
|
- try {
|
|
|
- const folderPath = await ensureElectron().selectDirectory();
|
|
|
- return folderPath;
|
|
|
- } catch (e) {
|
|
|
- log.error("error while selecting folder", e);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Batches all the files to be uploaded (or trashed) from the
|
|
|
- // event queue of same collection as the next event
|
|
|
- private clubSameCollectionEvents(): EventQueueItem {
|
|
|
- const event = this.eventQueue.shift();
|
|
|
- while (
|
|
|
- this.eventQueue.length > 0 &&
|
|
|
- event.collectionName === this.eventQueue[0].collectionName &&
|
|
|
- event.type === this.eventQueue[0].type
|
|
|
- ) {
|
|
|
- if (event.type === "trash") {
|
|
|
- event.paths = [...event.paths, ...this.eventQueue[0].paths];
|
|
|
- } else {
|
|
|
- event.files = [...event.files, ...this.eventQueue[0].files];
|
|
|
- }
|
|
|
- this.eventQueue.shift();
|
|
|
- }
|
|
|
- return event;
|
|
|
- }
|
|
|
-
|
|
|
- async isFolder(folderPath: string) {
|
|
|
- try {
|
|
|
- const isFolder = await ensureElectron().isFolder(folderPath);
|
|
|
- return isFolder;
|
|
|
- } catch (e) {
|
|
|
- log.error("error while checking if folder exists", e);
|
|
|
- }
|
|
|
- }
|
|
|
+/** The singleton instance of the {@link FolderWatcher}. */
|
|
|
+const watcher = new FolderWatcher();
|
|
|
|
|
|
- pauseRunningSync() {
|
|
|
- this.isPaused = true;
|
|
|
- uploadManager.cancelRunningUpload();
|
|
|
- }
|
|
|
+export default watcher;
|
|
|
|
|
|
- resumePausedSync() {
|
|
|
- this.isPaused = false;
|
|
|
- this.getAndSyncDiffOfFiles();
|
|
|
- }
|
|
|
+/**
|
|
|
+ * A file system watch event encapsulates a change that has occurred on disk
|
|
|
+ * that needs us to take some action within Ente to synchronize with the user's
|
|
|
+ * Ente collections.
|
|
|
+ *
|
|
|
+ * Events get added in two ways:
|
|
|
+ *
|
|
|
+ * - When the app starts, it reads the current state of files on disk and
|
|
|
+ * compares that with its last known state to determine what all events it
|
|
|
+ * missed. This is easier than it sounds as we have only two events: add and
|
|
|
+ * remove.
|
|
|
+ *
|
|
|
+ * - When the app is running, it gets live notifications from our file system
|
|
|
+ * watcher (from the Node.js layer) about changes that have happened on disk,
|
|
|
+ * which the app then enqueues onto the event queue if they pertain to the
|
|
|
+ * files we're interested in.
|
|
|
+ */
|
|
|
+interface WatchEvent {
|
|
|
+ /** The action to take */
|
|
|
+ action: "upload" | "trash";
|
|
|
+ /** The path of the root folder corresponding to the {@link FolderWatch}. */
|
|
|
+ folderPath: string;
|
|
|
+ /** The name of the Ente collection the file belongs to. */
|
|
|
+ collectionName: string;
|
|
|
+ /** The absolute path to the file under consideration. */
|
|
|
+ filePath: string;
|
|
|
}
|
|
|
|
|
|
-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;
|
|
|
+/**
|
|
|
+ * A composite of multiple {@link WatchEvent}s that only differ in their
|
|
|
+ * {@link filePath}.
|
|
|
+ *
|
|
|
+ * When processing events, we combine a run of events with the same
|
|
|
+ * {@link action}, {@link folderPath} and {@link collectionName}. This allows us
|
|
|
+ * to process all the affected {@link filePaths} in one shot.
|
|
|
+ */
|
|
|
+type ClubbedWatchEvent = Omit<WatchEvent, "filePath"> & {
|
|
|
+ filePaths: string[];
|
|
|
};
|
|
|
|
|
|
-async function diskFileAddedCallback(file: ElectronFile) {
|
|
|
- try {
|
|
|
- const collectionNameAndFolderPath =
|
|
|
- await watchFolderService.getCollectionNameAndFolderPath(file.path);
|
|
|
+/**
|
|
|
+ * Determine which events we need to process to synchronize the watched on-disk
|
|
|
+ * folders to their corresponding collections.
|
|
|
+ */
|
|
|
+const deduceEvents = async (watches: FolderWatch[]): Promise<WatchEvent[]> => {
|
|
|
+ const electron = ensureElectron();
|
|
|
+ const events: WatchEvent[] = [];
|
|
|
|
|
|
- if (!collectionNameAndFolderPath) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ for (const watch of watches) {
|
|
|
+ const folderPath = watch.folderPath;
|
|
|
|
|
|
- const { collectionName, folderPath } = collectionNameAndFolderPath;
|
|
|
+ const filePaths = await electron.watch.findFiles(folderPath);
|
|
|
|
|
|
- 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);
|
|
|
- }
|
|
|
-}
|
|
|
+ // Files that are on disk but not yet synced.
|
|
|
+ for (const filePath of pathsToUpload(filePaths, watch))
|
|
|
+ events.push({
|
|
|
+ action: "upload",
|
|
|
+ folderPath,
|
|
|
+ collectionName: collectionNameForPath(filePath, watch),
|
|
|
+ filePath,
|
|
|
+ });
|
|
|
|
|
|
-async function diskFileRemovedCallback(filePath: string) {
|
|
|
- try {
|
|
|
- const collectionNameAndFolderPath =
|
|
|
- await watchFolderService.getCollectionNameAndFolderPath(filePath);
|
|
|
+ // Previously synced files that are no longer on disk.
|
|
|
+ for (const filePath of pathsToRemove(filePaths, watch))
|
|
|
+ events.push({
|
|
|
+ action: "trash",
|
|
|
+ folderPath,
|
|
|
+ collectionName: collectionNameForPath(filePath, watch),
|
|
|
+ filePath,
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- if (!collectionNameAndFolderPath) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ return events;
|
|
|
+};
|
|
|
|
|
|
- const { collectionName, folderPath } = collectionNameAndFolderPath;
|
|
|
+/**
|
|
|
+ * Filter out hidden files and previously synced or ignored paths from
|
|
|
+ * {@link paths} to get the list of paths that need to be uploaded to the Ente
|
|
|
+ * collection.
|
|
|
+ */
|
|
|
+const pathsToUpload = (paths: string[], watch: FolderWatch) =>
|
|
|
+ paths
|
|
|
+ // Filter out hidden files (files whose names begins with a dot)
|
|
|
+ .filter((path) => !isHiddenFile(path))
|
|
|
+ // Files that are on disk but not yet synced or ignored.
|
|
|
+ .filter((path) => !isSyncedOrIgnoredPath(path, watch));
|
|
|
|
|
|
- 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);
|
|
|
- }
|
|
|
-}
|
|
|
+/**
|
|
|
+ * Return the paths to previously synced files that are no longer on disk and so
|
|
|
+ * must be removed from the Ente collection.
|
|
|
+ */
|
|
|
+const pathsToRemove = (paths: string[], watch: FolderWatch) =>
|
|
|
+ watch.syncedFiles
|
|
|
+ .map((f) => f.path)
|
|
|
+ .filter((path) => !paths.includes(path));
|
|
|
|
|
|
-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);
|
|
|
- }
|
|
|
-}
|
|
|
+const isSyncedOrIgnoredPath = (path: string, watch: FolderWatch) =>
|
|
|
+ watch.ignoredFiles.includes(path) ||
|
|
|
+ watch.syncedFiles.find((f) => f.path === 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;
|
|
|
- });
|
|
|
-}
|
|
|
+const collectionNameForPath = (path: string, watch: FolderWatch) =>
|
|
|
+ watch.collectionMapping == "root"
|
|
|
+ ? dirname(watch.folderPath)
|
|
|
+ : parentDirectoryName(path);
|
|
|
|
|
|
-function isSyncedOrIgnoredFile(file: ElectronFile, mapping: WatchMapping) {
|
|
|
- return (
|
|
|
- mapping.ignoredFiles.includes(file.path) ||
|
|
|
- mapping.syncedFiles.find((f) => f.path === file.path)
|
|
|
- );
|
|
|
-}
|
|
|
+const parentDirectoryName = (path: string) => basename(dirname(path));
|