diff --git a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml index ac23b67a8..c109aa19d 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml +++ b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml @@ -83,6 +83,9 @@ + + https://github.com/cryptomator/cryptomator/releases/1.16.0 + https://github.com/cryptomator/cryptomator/releases/1.15.3 diff --git a/pom.xml b/pom.xml index c6dd8b54b..8cc419fce 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.cryptomator cryptomator - 1.16.0-SNAPSHOT + 1.17.0-SNAPSHOT Cryptomator Desktop App diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 4dd4242b3..459d3c52d 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -59,6 +59,7 @@ open module org.cryptomator.desktop { uses org.cryptomator.common.locationpresets.LocationPresetsProvider; uses SSLContextProvider; + uses org.cryptomator.event.NotificationHandler; provides TrayMenuController with AwtTrayMenuController; provides Configurator with LogbackConfiguratorFactory; 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..2e8dbf035 --- /dev/null +++ b/src/main/java/org/cryptomator/common/EventMap.java @@ -0,0 +1,160 @@ +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.Comparator; +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 + *

+ * Use {@link EventMap#put(VaultEvent)} to add an element and {@link EventMap#remove(VaultEvent)} to remove it. + *

+ * The map is size restricted to {@value MAX_SIZE} elements. If a _new_ element (i.e. not already present) is added, the least recently added is removed. + */ +@Singleton +public class EventMap implements ObservableMap { + + private static final int MAX_SIZE = 300; + + 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) { + if (size() == MAX_SIZE) { + delegate.entrySet().stream() // + .min(Comparator.comparing(entry -> entry.getValue().actualEvent().getTimestamp())) // + .ifPresent(oldestEntry -> delegate.remove(oldestEntry.getKey())); + } + delegate.put(key, e); + } else { + delegate.put(key, nullOrEntry.incrementCount(e.actualEvent())); + } + } + + 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 8c0b68e80..2e1ae4bba 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -23,6 +23,7 @@ import org.cryptomator.cryptofs.event.FilesystemEvent; import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; +import org.cryptomator.event.VaultEvent; import org.cryptomator.integrations.mount.MountFailedException; import org.cryptomator.integrations.mount.Mountpoint; import org.cryptomator.integrations.mount.UnmountFailedException; @@ -34,6 +35,7 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; +import javafx.application.Platform; import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; diff --git a/src/main/java/org/cryptomator/event/Answer.java b/src/main/java/org/cryptomator/event/Answer.java new file mode 100644 index 000000000..bfb780e52 --- /dev/null +++ b/src/main/java/org/cryptomator/event/Answer.java @@ -0,0 +1,14 @@ +package org.cryptomator.event; + +public sealed interface Answer permits Answer.DoNothing, Answer.DoSomething { + + + record DoNothing() implements Answer {} + + record DoSomething(Runnable action) implements Answer { + + void run() { + action.run(); + } + } +} diff --git a/src/main/java/org/cryptomator/event/NotificationHandler.java b/src/main/java/org/cryptomator/event/NotificationHandler.java new file mode 100644 index 000000000..983a5d2dd --- /dev/null +++ b/src/main/java/org/cryptomator/event/NotificationHandler.java @@ -0,0 +1,15 @@ +package org.cryptomator.event; + +import org.cryptomator.integrations.common.IntegrationsLoader; + +import java.util.ServiceLoader; +import java.util.stream.Stream; + +public interface NotificationHandler { + + Answer handle(VaultEvent e); + + static Stream loadAll() { + return IntegrationsLoader.loadAll(ServiceLoader.load(NotificationHandler.class), NotificationHandler.class); + } +} diff --git a/src/main/java/org/cryptomator/event/VaultEvent.java b/src/main/java/org/cryptomator/event/VaultEvent.java new file mode 100644 index 000000000..8b31747cf --- /dev/null +++ b/src/main/java/org/cryptomator/event/VaultEvent.java @@ -0,0 +1,27 @@ +package org.cryptomator.event; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptofs.event.FilesystemEvent; + +import java.time.Instant; + +public record VaultEvent(Vault v, FilesystemEvent actualEvent, int count) implements Comparable { + + public VaultEvent(Vault v, FilesystemEvent actualEvent) { + this(v, actualEvent, 1); + } + + @Override + public int compareTo(VaultEvent other) { + var timeResult = actualEvent.getTimestamp().compareTo(other.actualEvent().getTimestamp()); + if(timeResult != 0) { + return timeResult; + } else { + return this.equals(other) ? 0 : this.actualEvent.getClass().getName().compareTo(other.actualEvent.getClass().getName()); + } + } + + public VaultEvent incrementCount(FilesystemEvent update) { + return new VaultEvent(v, update, count+1); + } +} diff --git a/src/main/java/org/cryptomator/ui/eventview/UpdateEventViewController.java b/src/main/java/org/cryptomator/ui/eventview/UpdateEventViewController.java new file mode 100644 index 000000000..19c475447 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/eventview/UpdateEventViewController.java @@ -0,0 +1,14 @@ +package org.cryptomator.ui.eventview; + +import org.cryptomator.ui.common.FxController; + +import javax.inject.Inject; + +@EventViewScoped +public class UpdateEventViewController implements FxController { + + @Inject + public UpdateEventViewController() { + + } +} diff --git a/src/main/resources/fxml/vault_detail_unlocked.fxml b/src/main/resources/fxml/vault_detail_unlocked.fxml index d80d8b7b1..289814347 100644 --- a/src/main/resources/fxml/vault_detail_unlocked.fxml +++ b/src/main/resources/fxml/vault_detail_unlocked.fxml @@ -74,7 +74,6 @@ -