diff --git a/src/main/java/org/cryptomator/common/CommonsModule.java b/src/main/java/org/cryptomator/common/CommonsModule.java index 296b2c039..a1e3c0950 100644 --- a/src/main/java/org/cryptomator/common/CommonsModule.java +++ b/src/main/java/org/cryptomator/common/CommonsModule.java @@ -14,15 +14,12 @@ import org.cryptomator.common.settings.SettingsProvider; import org.cryptomator.common.vaults.VaultComponent; import org.cryptomator.common.vaults.VaultListModule; import org.cryptomator.cryptolib.common.MasterkeyFileAccess; -import org.cryptomator.event.VaultEvent; import org.cryptomator.integrations.revealpath.RevealPathService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Named; import javax.inject.Singleton; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Comparator; @@ -131,12 +128,6 @@ public abstract class CommonsModule { return executorService; } - @Provides - @Singleton - static ObservableList provideVaultEventQueue() { - return FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - } - private static void handleUncaughtExceptionInBackgroundThread(Thread thread, Throwable throwable) { LOG.error("Uncaught exception in " + thread.getName(), throwable); } diff --git a/src/main/java/org/cryptomator/common/EventMap.java b/src/main/java/org/cryptomator/common/EventMap.java new file mode 100644 index 000000000..f34f592a5 --- /dev/null +++ b/src/main/java/org/cryptomator/common/EventMap.java @@ -0,0 +1,148 @@ +package org.cryptomator.common; + +import org.cryptomator.cryptofs.event.BrokenDirFileEvent; +import org.cryptomator.cryptofs.event.BrokenFileNodeEvent; +import org.cryptomator.cryptofs.event.ConflictResolutionFailedEvent; +import org.cryptomator.cryptofs.event.ConflictResolvedEvent; +import org.cryptomator.cryptofs.event.DecryptionFailedEvent; +import org.cryptomator.cryptofs.event.FilesystemEvent; +import org.cryptomator.event.VaultEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javafx.beans.InvalidationListener; +import javafx.collections.FXCollections; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableMap; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * Map containing {@link VaultEvent}s. + * The map is keyed by the ciphertext path of the affected resource _and_ the {@link FilesystemEvent}s class in order to group same events + */ +@Singleton +public class EventMap implements ObservableMap { + + public record EventKey(Path ciphertextPath, Class c) {} + + private final ObservableMap delegate; + + @Inject + public EventMap() { + delegate = FXCollections.observableHashMap(); + } + + @Override + public void addListener(MapChangeListener mapChangeListener) { + delegate.addListener(mapChangeListener); + } + + @Override + public void removeListener(MapChangeListener mapChangeListener) { + delegate.removeListener(mapChangeListener); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + @Override + public VaultEvent get(Object key) { + return delegate.get(key); + } + + @Override + public @Nullable VaultEvent put(EventKey key, VaultEvent value) { + return delegate.put(key, value); + } + + @Override + public VaultEvent remove(Object key) { + return delegate.remove(key); + } + + @Override + public void putAll(@NotNull Map m) { + delegate.putAll(m); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public @NotNull Set keySet() { + return delegate.keySet(); + } + + @Override + public @NotNull Collection values() { + return delegate.values(); + } + + @Override + public @NotNull Set> entrySet() { + return delegate.entrySet(); + } + + @Override + public void addListener(InvalidationListener invalidationListener) { + delegate.addListener(invalidationListener); + } + + @Override + public void removeListener(InvalidationListener invalidationListener) { + delegate.removeListener(invalidationListener); + } + + public synchronized void put(VaultEvent e) { + //compute key + var key = computeKey(e.actualEvent()); + //if-else + var nullOrEntry = delegate.get(key); + if (nullOrEntry == null) { + delegate.put(key, e); + } else { + delegate.put(key, nullOrEntry.incrementCount(e.timestamp())); + } + } + + public synchronized VaultEvent remove(VaultEvent similar) { + //compute key + var key = computeKey(similar.actualEvent()); + return this.remove(key); + } + + private EventKey computeKey(FilesystemEvent e) { + var p = switch (e) { + case DecryptionFailedEvent(Path ciphertextPath, _) -> ciphertextPath; + case ConflictResolvedEvent(_, _, _, Path resolvedCiphertext) -> resolvedCiphertext; + case ConflictResolutionFailedEvent(_, Path conflictingCiphertext, _) -> conflictingCiphertext; + case BrokenDirFileEvent(Path ciphertext) -> ciphertext; + case BrokenFileNodeEvent(_, Path ciphertext) -> ciphertext; + }; + return new EventKey(p, e.getClass()); + } +} diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index 2c168ae2d..b4fce3022 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -10,6 +10,7 @@ package org.cryptomator.common.vaults; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.Constants; +import org.cryptomator.common.EventMap; import org.cryptomator.common.mount.Mounter; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; @@ -44,12 +45,10 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.collections.ObservableList; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.ReadOnlyFileSystemException; -import java.time.Instant; import java.util.EnumSet; import java.util.Objects; import java.util.Set; @@ -79,7 +78,7 @@ public class Vault { private final ObjectBinding mountPoint; private final Mounter mounter; private final Settings settings; - private final ObservableList eventList; + private final EventMap eventMap; private final BooleanProperty showingStats; private final AtomicReference mountHandle = new AtomicReference<>(null); @@ -92,7 +91,7 @@ public class Vault { @Named("lastKnownException") ObjectProperty lastKnownException, // VaultStats stats, // Mounter mounter, Settings settings, // - ObservableList eventList) { + EventMap eventMap) { this.vaultSettings = vaultSettings; this.configCache = configCache; this.cryptoFileSystem = cryptoFileSystem; @@ -109,7 +108,7 @@ public class Vault { this.mountPoint = Bindings.createObjectBinding(this::getMountPoint, state); this.mounter = mounter; this.settings = settings; - this.eventList = eventList; + this.eventMap = eventMap; this.showingStats = new SimpleBooleanProperty(false); this.quickAccessEntry = new AtomicReference<>(null); } @@ -260,10 +259,12 @@ public class Vault { } } + private void consumeVaultEvent(FilesystemEvent e) { - //TODO: here we could implement a buffer to prevent event spam (due to many filesystem requests) - var timestamp = Instant.now(); - Platform.runLater(() -> eventList.addLast(new VaultEvent(timestamp, this, e))); + var wrapper = new VaultEvent(this, e); + Platform.runLater(() -> { + eventMap.put(wrapper); + }); } // ****************************************************************************** diff --git a/src/main/java/org/cryptomator/event/VaultEvent.java b/src/main/java/org/cryptomator/event/VaultEvent.java index e42f24339..7ec47d647 100644 --- a/src/main/java/org/cryptomator/event/VaultEvent.java +++ b/src/main/java/org/cryptomator/event/VaultEvent.java @@ -5,10 +5,10 @@ import org.cryptomator.cryptofs.event.FilesystemEvent; import java.time.Instant; -public record VaultEvent(Instant timestamp, Vault v, FilesystemEvent actualEvent) implements Comparable { +public record VaultEvent(Instant timestamp, Vault v, FilesystemEvent actualEvent, int count) implements Comparable { public VaultEvent(Vault v, FilesystemEvent actualEvent) { - this(Instant.now(), v, actualEvent); + this(Instant.now(), v, actualEvent, 1); } @Override @@ -20,4 +20,8 @@ public record VaultEvent(Instant timestamp, Vault v, FilesystemEvent actualEvent return this.equals(other) ? 0 : this.actualEvent.getClass().getName().compareTo(other.actualEvent.getClass().getName()); } } + + public VaultEvent incrementCount(Instant timestamp) { + return new VaultEvent(timestamp, v, actualEvent, count+1); + } } diff --git a/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java index 8da0e1d36..5e39670ec 100644 --- a/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java +++ b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java @@ -1,5 +1,6 @@ package org.cryptomator.ui.eventview; +import org.cryptomator.common.EventMap; import org.cryptomator.common.Nullable; import org.cryptomator.common.ObservableUtil; import org.cryptomator.cryptofs.CryptoPath; @@ -26,7 +27,6 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableValue; -import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.geometry.Side; import javafx.scene.control.Button; @@ -50,7 +50,7 @@ public class EventListCellController implements FxController { private static final DateTimeFormatter LOCAL_DATE_FORMATTER = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withZone(ZoneId.systemDefault()); private static final DateTimeFormatter LOCAL_TIME_FORMATTER = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault()); - private final ObservableList events; + private final EventMap eventMap; @Nullable private final RevealPathService revealService; private final ResourceBundle resourceBundle; @@ -65,9 +65,7 @@ public class EventListCellController implements FxController { private final ObservableValue description; private final ObservableValue icon; private final BooleanProperty actionsButtonVisible; - - @FXML - private Tooltip eventTooltip; + private final Tooltip eventTooltip; @FXML HBox root; @@ -77,8 +75,8 @@ public class EventListCellController implements FxController { Button eventActionsButton; @Inject - public EventListCellController(ObservableList events, Optional revealService, ResourceBundle resourceBundle) { - this.events = events; + public EventListCellController(EventMap eventMap, Optional revealService, ResourceBundle resourceBundle) { + this.eventMap = eventMap; this.revealService = revealService.orElseGet(() -> null); this.resourceBundle = resourceBundle; this.event = new SimpleObjectProperty<>(null); @@ -111,7 +109,7 @@ public class EventListCellController implements FxController { eventActionsMenu.hide(); eventActionsMenu.getItems().clear(); eventTooltip.setText(item.v().getDisplayName()); - addAction("generic.action.dismiss", () -> events.remove(item)); + addAction("generic.action.dismiss", () -> eventMap.remove(item)); switch (item.actualEvent()) { case ConflictResolvedEvent fse -> this.adjustToConflictResolvedEvent(fse); case ConflictResolutionFailedEvent fse -> this.adjustToConflictEvent(fse); diff --git a/src/main/java/org/cryptomator/ui/eventview/EventViewController.java b/src/main/java/org/cryptomator/ui/eventview/EventViewController.java index d33fcda4e..2a58381e5 100644 --- a/src/main/java/org/cryptomator/ui/eventview/EventViewController.java +++ b/src/main/java/org/cryptomator/ui/eventview/EventViewController.java @@ -1,5 +1,6 @@ package org.cryptomator.ui.eventview; +import org.cryptomator.common.EventMap; import org.cryptomator.common.vaults.Vault; import org.cryptomator.event.VaultEvent; import org.cryptomator.ui.common.FxController; @@ -8,6 +9,7 @@ import javax.inject.Inject; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; +import javafx.collections.MapChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; @@ -21,6 +23,7 @@ import java.util.ResourceBundle; @EventViewScoped public class EventViewController implements FxController { + private final EventMap eventMap; private final ObservableList eventList; private final FilteredList filteredEventList; private final ObservableList vaults; @@ -35,8 +38,9 @@ public class EventViewController implements FxController { ListView eventListView; @Inject - public EventViewController(ObservableList eventList, ObservableList vaults, ResourceBundle resourceBundle, EventListCellFactory cellFactory) { - this.eventList = eventList; + public EventViewController(EventMap eventMap, ObservableList vaults, ResourceBundle resourceBundle, EventListCellFactory cellFactory) { + this.eventMap = eventMap; + this.eventList = FXCollections.observableArrayList(); this.filteredEventList = eventList.filtered(_ -> true); this.vaults = vaults; this.reversedEventList = new SortedList<>(filteredEventList, Comparator.reverseOrder()); @@ -55,6 +59,9 @@ public class EventViewController implements FxController { choiceBoxEntries.addAll(c.getAddedSubList()); } }); + + eventList.addAll(eventMap.values()); + eventMap.addListener((MapChangeListener) this::updateList); eventListView.setCellFactory(cellFactory); eventListView.setItems(reversedEventList); @@ -63,6 +70,18 @@ public class EventViewController implements FxController { vaultFilterChoiceBox.setConverter(new VaultConverter(resourceBundle)); } + private void updateList(MapChangeListener.Change change) { + if (change.wasAdded() && change.wasRemoved()) { + //entry updated + eventList.remove(change.getValueRemoved()); + eventList.addLast(change.getValueAdded()); + } else if (change.wasAdded()) { + eventList.addLast(change.getValueAdded()); + } else { //removed + eventList.remove(change.getValueRemoved()); + } + } + private void applyVaultFilter(ObservableValue v, Vault oldV, Vault newV) { if (newV == null) { filteredEventList.setPredicate(_ -> true); @@ -72,8 +91,8 @@ public class EventViewController implements FxController { } @FXML - void clearEventList() { - eventList.clear(); + void clearEvents() { + eventMap.clear(); } private static class VaultConverter extends StringConverter { diff --git a/src/main/resources/fxml/eventview.fxml b/src/main/resources/fxml/eventview.fxml index 6d1c59c0e..675b75f4f 100644 --- a/src/main/resources/fxml/eventview.fxml +++ b/src/main/resources/fxml/eventview.fxml @@ -21,7 +21,7 @@ -