├── src ├── test │ ├── resources │ │ └── mockito-extensions │ │ │ └── org.mockito.plugins.MockMaker │ └── java │ │ └── org │ │ └── cryptomator │ │ └── integrations │ │ ├── common │ │ ├── InitExceptionTestClass.java │ │ ├── JarBuilder.java │ │ ├── ClassLoaderFactoryTest.java │ │ └── IntegrationsLoaderTest.java │ │ ├── mount │ │ └── MountpointTest.java │ │ └── update │ │ └── SemVerComparatorTest.java └── main │ ├── java │ ├── org │ │ └── cryptomator │ │ │ └── integrations │ │ │ ├── mount │ │ │ ├── package-info.java │ │ │ ├── MountFailedException.java │ │ │ ├── UnmountFailedException.java │ │ │ ├── Mountpoint.java │ │ │ ├── Mount.java │ │ │ ├── MountCapability.java │ │ │ ├── MountService.java │ │ │ └── MountBuilder.java │ │ │ ├── uiappearance │ │ │ ├── Theme.java │ │ │ ├── UiAppearanceListener.java │ │ │ ├── UiAppearanceException.java │ │ │ ├── package-info.java │ │ │ └── UiAppearanceProvider.java │ │ │ ├── tray │ │ │ ├── SeparatorItem.java │ │ │ ├── TrayMenuItem.java │ │ │ ├── SubMenuItem.java │ │ │ ├── ActionItem.java │ │ │ ├── TrayMenuException.java │ │ │ ├── TrayIconLoader.java │ │ │ ├── TrayIntegrationProvider.java │ │ │ └── TrayMenuController.java │ │ │ ├── update │ │ │ ├── DownloadUpdateInfo.java │ │ │ ├── UpdateFailedException.java │ │ │ ├── NoopUpdateStep.java │ │ │ ├── UpdateInfo.java │ │ │ ├── UpdateStepAdapter.java │ │ │ ├── UpdateMechanism.java │ │ │ ├── SemVerComparator.java │ │ │ ├── UpdateStep.java │ │ │ ├── DownloadUpdateMechanism.java │ │ │ └── DownloadUpdateStep.java │ │ │ ├── Localization.java │ │ │ ├── keychain │ │ │ ├── KeychainAccessException.java │ │ │ └── KeychainAccessProvider.java │ │ │ ├── quickaccess │ │ │ ├── QuickAccessServiceException.java │ │ │ └── QuickAccessService.java │ │ │ ├── autostart │ │ │ ├── ToggleAutoStartFailedException.java │ │ │ └── AutoStartProvider.java │ │ │ ├── revealpath │ │ │ ├── RevealFailedException.java │ │ │ └── RevealPathService.java │ │ │ └── common │ │ │ ├── DisplayName.java │ │ │ ├── Priority.java │ │ │ ├── LocalizedDisplayName.java │ │ │ ├── OperatingSystem.java │ │ │ ├── NamedServiceProvider.java │ │ │ ├── CheckAvailability.java │ │ │ ├── ClassLoaderFactory.java │ │ │ └── IntegrationsLoader.java │ └── module-info.java │ └── resources │ └── IntegrationsApi.properties ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml ├── misc.xml └── inspectionProfiles │ └── Project_Default.xml ├── .gitignore ├── CHANGELOG.md ├── .github ├── workflows │ ├── codeql-analysis.yml │ └── build.yml └── dependabot.yml ├── README.md ├── pom.xml └── LICENSE /src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/mount/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Mount service package 3 | */ 4 | package org.cryptomator.integrations.mount; -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/uiappearance/Theme.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.uiappearance; 2 | 3 | public enum Theme { 4 | LIGHT, 5 | DARK; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/tray/SeparatorItem.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.tray; 2 | 3 | public record SeparatorItem() implements TrayMenuItem { 4 | } 5 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/tray/TrayMenuItem.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.tray; 2 | 3 | public sealed interface TrayMenuItem permits ActionItem, SubMenuItem, SeparatorItem { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/tray/SubMenuItem.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.tray; 2 | 3 | import java.util.List; 4 | 5 | public record SubMenuItem(String title, List items) implements TrayMenuItem { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/uiappearance/UiAppearanceListener.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.uiappearance; 2 | 3 | @FunctionalInterface 4 | public interface UiAppearanceListener { 5 | 6 | void systemAppearanceChanged(Theme newTheme); 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/tray/ActionItem.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.tray; 2 | 3 | public record ActionItem(String title, Runnable action, boolean enabled) implements TrayMenuItem { 4 | 5 | public ActionItem(String title, Runnable action) { 6 | this(title, action, true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/update/DownloadUpdateInfo.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.update; 2 | 3 | public record DownloadUpdateInfo( 4 | DownloadUpdateMechanism updateMechanism, 5 | String version, 6 | DownloadUpdateMechanism.Asset asset 7 | ) implements UpdateInfo { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/tray/TrayMenuException.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.tray; 2 | 3 | public class TrayMenuException extends Exception { 4 | 5 | public TrayMenuException(String message) { 6 | super(message); 7 | } 8 | 9 | public TrayMenuException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/Localization.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations; 2 | 3 | import java.util.ResourceBundle; 4 | 5 | public enum Localization { 6 | INSTANCE; 7 | 8 | private final ResourceBundle resourceBundle = ResourceBundle.getBundle("IntegrationsApi"); 9 | 10 | public static ResourceBundle get() { 11 | return INSTANCE.resourceBundle; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/uiappearance/UiAppearanceException.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.uiappearance; 2 | 3 | public class UiAppearanceException extends Exception{ 4 | 5 | public UiAppearanceException(String message) { 6 | super(message); 7 | } 8 | 9 | public UiAppearanceException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/keychain/KeychainAccessException.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.keychain; 2 | 3 | public class KeychainAccessException extends Exception { 4 | 5 | public KeychainAccessException(String message) { 6 | super(message); 7 | } 8 | 9 | public KeychainAccessException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/IntegrationsApi.properties: -------------------------------------------------------------------------------- 1 | org.cryptomator.api.update.download.new=Download... 2 | org.cryptomator.api.update.download.indeterminateProgress=Downloading... 3 | org.cryptomator.api.update.download.progress=Downloading... %1.0f%% 4 | org.cryptomator.api.update.download.done=Downloaded. 5 | 6 | org.cryptomator.api.update.updateStep.EXIT=Exiting... 7 | org.cryptomator.api.update.updateStep.RETRY=Retry -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/quickaccess/QuickAccessServiceException.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.quickaccess; 2 | 3 | public class QuickAccessServiceException extends Exception { 4 | 5 | public QuickAccessServiceException(String message) { 6 | super(message); 7 | } 8 | 9 | public QuickAccessServiceException(String message, Throwable t) { 10 | super(message, t); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/autostart/ToggleAutoStartFailedException.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.autostart; 2 | 3 | public class ToggleAutoStartFailedException extends Exception { 4 | 5 | public ToggleAutoStartFailedException(String message) { 6 | super(message); 7 | } 8 | 9 | public ToggleAutoStartFailedException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.jar 3 | 4 | # Maven # 5 | target/ 6 | pom.xml.versionsBackup 7 | 8 | # IntelliJ Settings Files (https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems) # 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/dictionaries 12 | .idea/**/libraries/ 13 | .idea/.name 14 | .idea/encodings.xml 15 | .idea/compiler.xml 16 | .idea/jarRepositories.xml 17 | *.iml 18 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/mount/MountFailedException.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.mount; 2 | 3 | public class MountFailedException extends Exception { 4 | 5 | public MountFailedException(String msg) { 6 | super(msg); 7 | } 8 | 9 | public MountFailedException(Exception cause) { 10 | super(cause); 11 | } 12 | 13 | public MountFailedException(String msg, Exception cause) { 14 | super(msg, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/mount/UnmountFailedException.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.mount; 2 | 3 | public class UnmountFailedException extends Exception { 4 | 5 | public UnmountFailedException(String msg) { 6 | super(msg); 7 | } 8 | 9 | public UnmountFailedException(Exception cause) { 10 | super(cause); 11 | } 12 | 13 | public UnmountFailedException(String msg, Exception cause) { 14 | super(msg, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/revealpath/RevealFailedException.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.revealpath; 2 | 3 | public class RevealFailedException extends Exception { 4 | 5 | public RevealFailedException(String msg) { 6 | super(msg); 7 | } 8 | 9 | public RevealFailedException(Exception cause) { 10 | super(cause); 11 | } 12 | 13 | public RevealFailedException(String msg, Exception cause) { 14 | super(msg, cause); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/uiappearance/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Package for getting the OS color theme and listening to theme changes. 3 | * 4 | * @deprecated Cryptomator uses since version 1.14.0 the JavaFX framework in version 22, which provides via Platform.Preferences the system color scheme 5 | */ 6 | @Deprecated(since = "1.6.0") 7 | @ApiStatus.ScheduledForRemoval(inVersion = "1.7.0") 8 | package org.cryptomator.integrations.uiappearance; 9 | 10 | import org.jetbrains.annotations.ApiStatus; -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/update/UpdateFailedException.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.update; 2 | 3 | import org.jetbrains.annotations.ApiStatus; 4 | 5 | import java.io.IOException; 6 | 7 | @ApiStatus.Experimental 8 | public class UpdateFailedException extends IOException { 9 | 10 | public UpdateFailedException(String message) { 11 | super(message); 12 | } 13 | 14 | public UpdateFailedException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/integrations/common/InitExceptionTestClass.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.common; 2 | 3 | @CheckAvailability 4 | public class InitExceptionTestClass { 5 | 6 | private static final String TEST; 7 | 8 | static { 9 | TEST = throwSomething(); 10 | } 11 | 12 | public InitExceptionTestClass() { 13 | 14 | } 15 | 16 | static String throwSomething() { 17 | throw new RuntimeException("STATIC FAIL"); 18 | } 19 | 20 | @CheckAvailability 21 | public static boolean test() { 22 | return true; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/update/NoopUpdateStep.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.update; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | record NoopUpdateStep(String description) implements UpdateStep { 6 | 7 | @Override 8 | public void start() {} 9 | 10 | @Override 11 | public double preparationProgress() { 12 | return -1.0; 13 | } 14 | 15 | @Override 16 | public void cancel() {} 17 | 18 | @Override 19 | public void await() {} 20 | 21 | @Override 22 | public boolean await(long timeout, TimeUnit unit) { 23 | return true; // always done 24 | } 25 | 26 | @Override 27 | public UpdateStep nextStep() { 28 | return null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/autostart/AutoStartProvider.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.autostart; 2 | 3 | import org.cryptomator.integrations.common.IntegrationsLoader; 4 | import org.jetbrains.annotations.Blocking; 5 | 6 | import java.util.Optional; 7 | 8 | public interface AutoStartProvider { 9 | 10 | /** 11 | * Loads the best-suited AutoStartProvider. 12 | * 13 | * @return preferred AutoStartProvider (if any) 14 | * @since 1.1.0 15 | */ 16 | static Optional get() { 17 | return IntegrationsLoader.load(AutoStartProvider.class); 18 | } 19 | 20 | @Blocking 21 | void enable() throws ToggleAutoStartFailedException; 22 | 23 | @Blocking 24 | void disable() throws ToggleAutoStartFailedException; 25 | 26 | boolean isEnabled(); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/common/DisplayName.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.common; 2 | 3 | import org.jetbrains.annotations.ApiStatus; 4 | 5 | import java.lang.annotation.Documented; 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | /** 12 | * A humanreadable name of the annotated class. 13 | *

14 | * Checked in the default implementation of the {@link NamedServiceProvider#getName()} with lower priority. 15 | * 16 | * @see NamedServiceProvider 17 | * @see LocalizedDisplayName 18 | */ 19 | @Documented 20 | @Retention(RetentionPolicy.RUNTIME) 21 | @Target(ElementType.TYPE) 22 | public @interface DisplayName { 23 | String value(); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/common/Priority.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.common; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Repeatable; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * Integration Priority. 12 | *

13 | * If multiple implementations for an integration can be provided, the provider with the highest priority will be used. 14 | * 15 | * @since 1.1.0 16 | */ 17 | @Documented 18 | @Retention(RetentionPolicy.RUNTIME) 19 | @Target({ElementType.TYPE}) 20 | public @interface Priority { 21 | int DEFAULT = 0; 22 | int FALLBACK = Integer.MIN_VALUE; 23 | 24 | int value() default DEFAULT; 25 | } 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 6 | 7 | The changelog starts with version 1.7.0. 8 | Changes to prior versions can be found on the [Github release page](https://github.com/cryptomator/integrations-api/releases). 9 | 10 | ## [Unreleased](https://github.com/cryptomator/integrations-api/compare/1.7.0...HEAD) 11 | 12 | ### Added 13 | 14 | * Experimental [Update API](https://github.com/cryptomator/integrations-api/blob/a522f36cf45884127e2431dd18222391669d5992/src/main/java/org/cryptomator/integrations/update/UpdateMechanism.java) (#72) 15 | 16 | ## [1.7.0](https://github.com/cryptomator/integrations-api/releases/tag/1.7.0) - 2025-09-17 17 | 18 | ### Changed 19 | 20 | * **[BREAKING]** Updated required build JDK to 21 (a88fa29d9cc05a0e39ab15420517d0f25cff6f35) 21 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/mount/Mountpoint.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.mount; 2 | 3 | import java.net.URI; 4 | import java.nio.file.Path; 5 | 6 | /** 7 | * A {@link Mount}'s mount point. There are two types of mount points: Path-based and URI-based. 8 | */ 9 | public sealed interface Mountpoint permits Mountpoint.WithPath, Mountpoint.WithUri { 10 | 11 | /** 12 | * Gets an URI representation of this mount point. 13 | * 14 | * @return an URI pointing to this mount point 15 | */ 16 | URI uri(); 17 | 18 | static Mountpoint forUri(URI uri) { 19 | return new WithUri(uri); 20 | } 21 | 22 | static Mountpoint forPath(Path path) { 23 | return new WithPath(path); 24 | } 25 | 26 | record WithUri(URI uri) implements Mountpoint { 27 | } 28 | 29 | record WithPath(Path path) implements Mountpoint { 30 | 31 | public URI uri() { 32 | return path.toUri(); 33 | } 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/update/UpdateInfo.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.update; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public interface UpdateInfo> { 6 | 7 | /** 8 | * @return The version string of the available update. 9 | */ 10 | String version(); 11 | 12 | /** 13 | * @return The update mechanism that provided this update info. 14 | */ 15 | UpdateMechanism updateMechanism(); 16 | 17 | /** 18 | * Typesafe equivalent to {@code updateMechanism().firstStep(this)}. 19 | * @return Result of {@link UpdateMechanism#firstStep(UpdateInfo)}. 20 | * @throws UpdateFailedException If no update process can be started, e.g. due to network or I/O issues. 21 | */ 22 | @NotNull 23 | default UpdateStep useToPrepareFirstStep() throws UpdateFailedException { 24 | @SuppressWarnings("unchecked") T self = (T) this; 25 | return updateMechanism().firstStep(self); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | 2 | name: "CodeQL" 3 | 4 | on: 5 | push: 6 | branches: [develop, main] 7 | pull_request: 8 | branches: [develop] 9 | schedule: 10 | - cron: '0 8 * * 0' 11 | 12 | jobs: 13 | analyse: 14 | name: Analyse 15 | runs-on: ubuntu-latest 16 | # dependeabot has on push events only read-only access, but codeql requires write access 17 | if: ${{ !(github.actor == 'dependabot[bot]' && contains(fromJSON('["push"]'), github.event_name)) }} 18 | steps: 19 | - uses: actions/checkout@v5 20 | with: 21 | fetch-depth: 2 22 | - uses: actions/setup-java@v5 23 | with: 24 | distribution: 'temurin' 25 | java-version: 25 26 | cache: 'maven' 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@v4 29 | with: 30 | languages: java 31 | - name: Build 32 | run: mvn -B compile 33 | - name: Perform CodeQL Analysis 34 | uses: github/codeql-action/analyze@v4 35 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/tray/TrayIconLoader.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.tray; 2 | 3 | import org.jetbrains.annotations.ApiStatus; 4 | 5 | /** 6 | * A callback used by the {@link TrayMenuController} to load tray icons in the format required by the implementation. 7 | */ 8 | @ApiStatus.Experimental 9 | sealed public interface TrayIconLoader permits TrayIconLoader.PngData, TrayIconLoader.FreedesktopIconName { 10 | 11 | @FunctionalInterface 12 | non-sealed interface PngData extends TrayIconLoader { 13 | 14 | /** 15 | * Loads an icon from a byte array holding a loaded PNG file. 16 | * 17 | * @param data png data 18 | */ 19 | void loadPng(byte[] data); 20 | } 21 | 22 | @FunctionalInterface 23 | non-sealed interface FreedesktopIconName extends TrayIconLoader { 24 | 25 | /** 26 | * Loads an icon by looking it up {@code iconName} inside of {@code $XDG_DATA_DIRS/icons}. 27 | * 28 | * @param iconName the icon name 29 | */ 30 | void lookupByName(String iconName); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | day: "monday" 8 | time: "06:00" 9 | timezone: "Etc/UTC" 10 | groups: 11 | java-test-dependencies: 12 | patterns: 13 | - "org.junit.jupiter:*" 14 | - "org.mockito:*" 15 | maven-build-plugins: 16 | patterns: 17 | - "org.apache.maven.plugins:*" 18 | - "org.sonatype.central:central-publishing-maven-plugin" 19 | java-production-dependencies: 20 | patterns: 21 | - "*" 22 | exclude-patterns: 23 | - "org.apache.maven.plugins:*" 24 | - "org.sonatype.central:central-publishing-maven-plugin" 25 | - "org.junit.jupiter:*" 26 | - "org.mockito:*" 27 | 28 | 29 | - package-ecosystem: "github-actions" 30 | directory: "/" # even for `.github/workflows` 31 | schedule: 32 | interval: "monthly" 33 | groups: 34 | github-actions: 35 | patterns: 36 | - "*" -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/common/LocalizedDisplayName.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.common; 2 | 3 | import org.jetbrains.annotations.ApiStatus; 4 | 5 | import java.lang.annotation.Documented; 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | /** 12 | * A humanreadable, localized name of the annotated class. 13 | *

14 | * Checked in the default implementation of the {@link NamedServiceProvider#getName()} with highest priority. 15 | * 16 | * @see NamedServiceProvider 17 | * @see DisplayName 18 | */ 19 | @Documented 20 | @Retention(RetentionPolicy.RUNTIME) 21 | @Target(ElementType.TYPE) 22 | public @interface LocalizedDisplayName { 23 | 24 | /** 25 | * Name of the localization bundle, where the display name is loaded from. 26 | * 27 | * @return Name of the localization bundle 28 | */ 29 | String bundle(); 30 | 31 | /** 32 | * The localization key containing the display name. 33 | * 34 | * @return Localization key to use 35 | */ 36 | String key(); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/tray/TrayIntegrationProvider.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.tray; 2 | 3 | import org.cryptomator.integrations.common.IntegrationsLoader; 4 | 5 | import java.util.Optional; 6 | 7 | /** 8 | * Allows to perform OS-specific tasks when the app gets minimized to or restored from a tray icon. 9 | */ 10 | public interface TrayIntegrationProvider { 11 | 12 | /** 13 | * Loads the best-suited TrayIntegrationProvider. 14 | * 15 | * @return preferred TrayIntegrationProvider (if any) 16 | * @since 1.1.0 17 | */ 18 | static Optional get() { 19 | return IntegrationsLoader.load(TrayIntegrationProvider.class); 20 | } 21 | 22 | /** 23 | * Performs tasks required when the application is no longer showing any window and only accessible via 24 | * system tray (or comparable facilities). 25 | */ 26 | void minimizedToTray(); 27 | 28 | /** 29 | * Performs tasks required when the application becomes "visible", i.e. is transitioning from a background task 30 | * that lived in the system tray (or comparable facilities). 31 | */ 32 | void restoredFromTray(); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/integrations/common/JarBuilder.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.common; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.util.jar.Attributes; 7 | import java.util.jar.JarEntry; 8 | import java.util.jar.JarFile; 9 | import java.util.jar.JarOutputStream; 10 | import java.util.jar.Manifest; 11 | 12 | public class JarBuilder implements AutoCloseable { 13 | 14 | private final Manifest manifest = new Manifest(); 15 | private final JarOutputStream jos; 16 | 17 | public JarBuilder(JarOutputStream jos) { 18 | this.jos = jos; 19 | manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); 20 | } 21 | 22 | public static JarBuilder withTarget(OutputStream out) throws IOException { 23 | return new JarBuilder(new JarOutputStream(out)); 24 | } 25 | 26 | public void addFile(String path, InputStream content) throws IOException { 27 | jos.putNextEntry(new JarEntry(path)); 28 | content.transferTo(jos); 29 | jos.closeEntry(); 30 | } 31 | 32 | @Override 33 | public void close() throws IOException { 34 | jos.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME)); 35 | manifest.write(jos); 36 | jos.closeEntry(); 37 | jos.close(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cryptomator Integrations API 2 | 3 | This library defines the API of various integrations used by Cryptomator. The implementations are then loaded during runtime via the ServiceLoader API. 4 | 5 | To add an integration to Cryptomator, simply create a library, implement the provider interface you're interested in, add your implementation's fully qualified class name to the provider configuration file and publish your library as a jar. 6 | 7 | For example let's say you want to add a new keychain integration. You just need these three steps: 8 | 1. Create a class, e.g. `com.example.mycryptomatorplugin.PwManager3000Integration` which extends `org.cryptomator.integrations.keychain.KeychainAccessProvider` and implement the methods according to the interface. 9 | 1. Create a provider configuration file at `META-INF/services/org.cryptomator.integrations.keychain.KeychainAccessProvider` and add your implementation (`com.example.mycryptomatorplugin.PwManager3000Integration`) 10 | 1. Publish your library as a jar file and include it to Cryptomator's class path at runtime (PRs are welcome) 11 | 12 | Implementations of the Integrations API can be found here: 13 | - https://github.com/cryptomator/integrations-win 14 | - https://github.com/cryptomator/integrations-mac 15 | - https://github.com/cryptomator/integrations-linux 16 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/revealpath/RevealPathService.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.revealpath; 2 | 3 | import org.cryptomator.integrations.common.IntegrationsLoader; 4 | 5 | import java.nio.file.Path; 6 | import java.util.stream.Stream; 7 | 8 | public interface RevealPathService { 9 | 10 | /** 11 | * Loads all supported service providers. 12 | * 13 | * @return Stream of supported RevealPathService implementations (may be empty) 14 | */ 15 | static Stream get() { 16 | return IntegrationsLoader.loadAll(RevealPathService.class).filter(RevealPathService::isSupported); 17 | } 18 | 19 | /** 20 | * Reveal the path in the system default file manager. 21 | *

22 | * If the path points to a file, the parent of the file is opened and the file is selected in the file manager window. 23 | * If the path points to a directory, the directory is opened and its content shown in the file manager window. 24 | * 25 | * @param p Path to reveal 26 | * @throws RevealFailedException if revealing the path failed 27 | */ 28 | void reveal(Path p) throws RevealFailedException; 29 | 30 | /** 31 | * Indicates, if this provider can be used. 32 | * 33 | * @return true, if this provider is supported in the current OS environment 34 | * @implSpec This check needs to return fast and in constant time 35 | */ 36 | boolean isSupported(); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/mount/Mount.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.mount; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * Handle to control the lifecycle of a mounted file system. 7 | *

8 | * Created by {@link MountBuilder} 9 | */ 10 | public interface Mount extends AutoCloseable { 11 | 12 | /** 13 | * Returns the absolute OS path, where this mount can be accessed. 14 | * 15 | * @return Absolute path to the mountpoint. 16 | */ 17 | Mountpoint getMountpoint(); 18 | 19 | /** 20 | * Unmounts the mounted Volume. 21 | *

22 | * If possible, attempt a graceful unmount. 23 | * 24 | * @throws UnmountFailedException If the unmount was not successful. 25 | * @see #unmountForced() 26 | */ 27 | void unmount() throws UnmountFailedException; 28 | 29 | /** 30 | * If supported, force-unmount the volume. 31 | * 32 | * @throws UnmountFailedException If the unmount was not successful. 33 | * @throws UnsupportedOperationException If {@link MountCapability#UNMOUNT_FORCED} is not supported 34 | */ 35 | default void unmountForced() throws UnmountFailedException { 36 | throw new UnsupportedOperationException(); 37 | } 38 | 39 | /** 40 | * Unmounts (if required) and releases any resources. 41 | * 42 | * @throws UnmountFailedException Thrown if unmounting failed 43 | * @throws IOException Thrown if cleaning up resources failed 44 | */ 45 | default void close() throws UnmountFailedException, IOException { 46 | unmount(); 47 | } 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | import org.cryptomator.integrations.quickaccess.QuickAccessService; 2 | import org.cryptomator.integrations.mount.MountService; 3 | import org.cryptomator.integrations.revealpath.RevealPathService; 4 | import org.cryptomator.integrations.tray.TrayMenuController; 5 | import org.cryptomator.integrations.autostart.AutoStartProvider; 6 | import org.cryptomator.integrations.keychain.KeychainAccessProvider; 7 | import org.cryptomator.integrations.tray.TrayIntegrationProvider; 8 | import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; 9 | import org.cryptomator.integrations.update.UpdateMechanism; 10 | 11 | 12 | module org.cryptomator.integrations.api { 13 | requires static org.jetbrains.annotations; 14 | requires org.slf4j; 15 | requires com.fasterxml.jackson.databind; 16 | requires java.net.http; 17 | 18 | exports org.cryptomator.integrations.autostart; 19 | exports org.cryptomator.integrations.common; 20 | exports org.cryptomator.integrations.keychain; 21 | exports org.cryptomator.integrations.mount; 22 | exports org.cryptomator.integrations.revealpath; 23 | exports org.cryptomator.integrations.tray; 24 | exports org.cryptomator.integrations.uiappearance; 25 | exports org.cryptomator.integrations.quickaccess; 26 | exports org.cryptomator.integrations.update; 27 | 28 | uses AutoStartProvider; 29 | uses KeychainAccessProvider; 30 | uses MountService; 31 | uses RevealPathService; 32 | uses TrayIntegrationProvider; 33 | uses TrayMenuController; 34 | uses UiAppearanceProvider; 35 | uses QuickAccessService; 36 | uses UpdateMechanism; 37 | } -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/common/OperatingSystem.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.common; 2 | 3 | import org.jetbrains.annotations.ApiStatus; 4 | 5 | import java.lang.annotation.Documented; 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Repeatable; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.RetentionPolicy; 10 | import java.lang.annotation.Target; 11 | 12 | /** 13 | * Restricts the annotated integration provider to one or more operating system(s). 14 | * 15 | * @since 1.1.0 16 | */ 17 | @Documented 18 | @Retention(RetentionPolicy.RUNTIME) 19 | @Target({ElementType.TYPE}) 20 | @Repeatable(OperatingSystem.OperatingSystems.class) 21 | public @interface OperatingSystem { 22 | Value value() default Value.UNKNOWN; 23 | 24 | @Documented 25 | @Retention(RetentionPolicy.RUNTIME) 26 | @Target({ElementType.TYPE}) 27 | @interface OperatingSystems { 28 | OperatingSystem[] value(); 29 | } 30 | 31 | enum Value { 32 | LINUX, 33 | MAC, 34 | WINDOWS, 35 | UNKNOWN; 36 | 37 | private static final String OS_NAME = System.getProperty("os.name", "").toLowerCase(); 38 | 39 | public static Value current() { 40 | if (OS_NAME.contains("linux")) { 41 | return LINUX; 42 | } else if (OS_NAME.contains("mac")) { 43 | return MAC; 44 | } else if (OS_NAME.contains("windows")) { 45 | return WINDOWS; 46 | } else { 47 | return UNKNOWN; 48 | } 49 | } 50 | 51 | public static boolean isCurrent(OperatingSystem os) { 52 | return current().equals(os.value()); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/common/NamedServiceProvider.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.common; 2 | 3 | import org.slf4j.LoggerFactory; 4 | 5 | import java.util.MissingResourceException; 6 | import java.util.ResourceBundle; 7 | 8 | /** 9 | * A service provider with a human-readable, possibly localized name. 10 | */ 11 | public interface NamedServiceProvider { 12 | 13 | /** 14 | * Get the name of this service provider. 15 | * 16 | * @return The name of the service provider 17 | * @implNote The default implementation looks first for a {@link LocalizedDisplayName} and loads the name from the specified resource bundle/key. If the annotation is not present or loading the resource throws an exception, the code looks for {@link DisplayName} and uses its value. If none of the former annotations are present, it falls back to the qualified class name. 18 | * @see DisplayName 19 | * @see LocalizedDisplayName 20 | */ 21 | default String getName() { 22 | var localizedDisplayName = this.getClass().getAnnotation(LocalizedDisplayName.class); 23 | if (localizedDisplayName != null) { 24 | try { 25 | return ResourceBundle.getBundle(localizedDisplayName.bundle()) // 26 | .getString(localizedDisplayName.key()); 27 | } catch (MissingResourceException e) { 28 | var clazz = this.getClass(); 29 | var logger = LoggerFactory.getLogger(clazz); 30 | logger.warn("Failed to load localized display name for {}. Falling back to not-localized display name/class name.", clazz.getName(), e); 31 | } 32 | } 33 | 34 | var displayName = this.getClass().getAnnotation(DisplayName.class); 35 | if (displayName != null) { 36 | return displayName.value(); 37 | } else { 38 | return this.getClass().getName(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/integrations/mount/MountpointTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.mount; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.condition.DisabledOnOs; 7 | import org.junit.jupiter.api.condition.EnabledOnOs; 8 | import org.junit.jupiter.api.condition.OS; 9 | 10 | import java.net.URI; 11 | import java.nio.file.Path; 12 | 13 | public class MountpointTest { 14 | 15 | @Test 16 | @DisplayName("MountPoint.forPath()") 17 | @DisabledOnOs(OS.WINDOWS) 18 | public void testForPath() { 19 | var path = Path.of("/foo/bar"); 20 | var mountPoint = Mountpoint.forPath(path); 21 | 22 | if (mountPoint instanceof Mountpoint.WithPath m) { 23 | Assertions.assertEquals(path, m.path()); 24 | Assertions.assertEquals(URI.create("file:///foo/bar"), mountPoint.uri()); 25 | } else { 26 | Assertions.fail(); 27 | } 28 | } 29 | 30 | @Test 31 | @DisplayName("MountPoint.forPath() (Windows)") 32 | @EnabledOnOs(OS.WINDOWS) 33 | public void testForPathWindows() { 34 | var path = Path.of("D:\\foo\\bar"); 35 | var mountPoint = Mountpoint.forPath(path); 36 | 37 | if (mountPoint instanceof Mountpoint.WithPath m) { 38 | Assertions.assertEquals(path, m.path()); 39 | Assertions.assertEquals(URI.create("file:///D:/foo/bar"), mountPoint.uri()); 40 | } else { 41 | Assertions.fail(); 42 | } 43 | } 44 | 45 | @Test 46 | @DisplayName("MountPoint.forUri()") 47 | public void testForUri() { 48 | var uri = URI.create("webdav://localhost:8080/foo/bar"); 49 | var mountPoint = Mountpoint.forUri(uri); 50 | 51 | Assertions.assertTrue(mountPoint instanceof Mountpoint.WithUri); 52 | Assertions.assertEquals(uri, mountPoint.uri()); 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/update/UpdateStepAdapter.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.update; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | import java.io.IOException; 6 | import java.time.Duration; 7 | import java.util.concurrent.Callable; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | public abstract class UpdateStepAdapter implements Callable, Runnable, UpdateStep { 11 | 12 | protected final Thread thread; 13 | protected volatile UpdateStep result; 14 | protected volatile Exception exception; 15 | 16 | public UpdateStepAdapter() { 17 | this.thread = Thread.ofVirtual().name("UpdateStep", 0).unstarted(this); 18 | } 19 | 20 | @Override 21 | public final void run() { 22 | try { 23 | this.result = this.call(); 24 | } catch (Exception e) { 25 | this.exception = e; 26 | } 27 | } 28 | 29 | @Override 30 | public void start() throws IllegalThreadStateException { 31 | thread.start(); 32 | } 33 | 34 | @Override 35 | public double preparationProgress() { 36 | return -1.0; 37 | } 38 | 39 | @Override 40 | public void cancel() { 41 | thread.interrupt(); 42 | } 43 | 44 | @Override 45 | public void await() throws InterruptedException { 46 | thread.join(); 47 | } 48 | 49 | @Override 50 | public boolean await(long timeout, TimeUnit unit) throws InterruptedException { 51 | return thread.join(Duration.of(timeout, unit.toChronoUnit())); 52 | } 53 | 54 | @Override 55 | public @Nullable UpdateStep nextStep() throws IllegalStateException, IOException { 56 | if (!isDone()) { 57 | throw new IllegalStateException("Update step not completed yet"); 58 | } 59 | return switch (exception) { 60 | case null -> result; 61 | case IOException e -> throw e; 62 | default -> throw new IOException("Update step failed", exception); 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/common/CheckAvailability.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.common; 2 | 3 | import org.jetbrains.annotations.ApiStatus; 4 | 5 | import java.lang.annotation.Documented; 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Inherited; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.RetentionPolicy; 10 | import java.lang.annotation.Target; 11 | 12 | /** 13 | * Identifies 0..n public methods to check preconditions for the integration to work. These are the rules: 14 | * 15 | *

    16 | *
  • Both the type and the method(s) must be annotated with {@code @CheckAvailability}
  • 17 | *
  • Only public no-arg boolean methods are considered
  • 18 | *
  • Methods may be {@code static}, in which case they get invoked before instantiating the service
  • 19 | *
  • Should the method throw an exception, it has the same effect as returning {@code false}
  • 20 | *
  • No specific execution order is guaranteed in case of multiple annotated methods
  • 21 | *
  • Annotations must be present on classes or ancestor classes, not on interfaces
  • 22 | *
23 | * 24 | * Example: 25 | *
26 |  * {@code
27 |  * @CheckAvailability
28 |  * public class Foo {
29 |  *	@CheckAvailability
30 |  *	public static boolean isSupported() {
31 |  *		return "enabled".equals(System.getProperty("plugin.status"));
32 |  *	}
33 |  * }
34 |  * }
35 |  * 
36 | *

37 | * Annotations are discovered at runtime using reflection, so make sure to make relevant classes accessible to this 38 | * module ({@code opens X to org.cryptomator.integrations.api}). 39 | * 40 | * @since 1.1.0 41 | */ 42 | @Documented 43 | @Retention(RetentionPolicy.RUNTIME) 44 | @Target({ElementType.TYPE, ElementType.METHOD}) 45 | @Inherited 46 | public @interface CheckAvailability { 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.tray; 2 | 3 | import org.cryptomator.integrations.common.IntegrationsLoader; 4 | import org.jetbrains.annotations.ApiStatus; 5 | 6 | import java.util.List; 7 | import java.util.Optional; 8 | import java.util.function.Consumer; 9 | 10 | /** 11 | * Displays a tray icon and menu 12 | * 13 | * @since 1.1.0 14 | */ 15 | @ApiStatus.Experimental 16 | public interface TrayMenuController { 17 | 18 | static Optional get() { 19 | return IntegrationsLoader.load(TrayMenuController.class); 20 | } 21 | 22 | /** 23 | * Displays an icon on the system tray. 24 | * 25 | * @param iconLoader A callback responsible for retrieving the icon in the required format 26 | * @param defaultAction Action to perform when interacting with the icon directly instead of its menu 27 | * @param tooltip Text shown when hovering 28 | * @throws TrayMenuException thrown when adding the tray icon failed 29 | */ 30 | void showTrayIcon(Consumer iconLoader, Runnable defaultAction, String tooltip) throws TrayMenuException; 31 | 32 | /** 33 | * Updates the icon on the system tray. 34 | * 35 | * @param iconLoader A callback responsible for retrieving the icon in the required format 36 | * @throws IllegalStateException thrown when called before an icon has been added 37 | */ 38 | void updateTrayIcon(Consumer iconLoader); 39 | 40 | /** 41 | * Show the given options in the tray menu. 42 | *

43 | * This method may be called multiple times, e.g. when the vault list changes. 44 | * 45 | * @param items Menu items 46 | * @throws TrayMenuException thrown when updating the tray menu failed 47 | */ 48 | void updateTrayMenu(List items) throws TrayMenuException; 49 | 50 | /** 51 | * Action to run before the tray menu opens. 52 | *

53 | * This method is used to set up an event listener for when the menu is opened, 54 | * e.g. so that the vault list can be updated to reflect volume mount state changes 55 | * which occur while Cryptomator is in the system tray (and not open). 56 | * 57 | * @param listener 58 | * @throws IllegalStateException thrown when adding listeners fails (i.e. there's no tray menu) 59 | */ 60 | void onBeforeOpenMenu(Runnable listener); 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/quickaccess/QuickAccessService.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.quickaccess; 2 | 3 | import org.cryptomator.integrations.common.IntegrationsLoader; 4 | import org.cryptomator.integrations.common.NamedServiceProvider; 5 | import org.jetbrains.annotations.Blocking; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.nio.file.Path; 9 | import java.util.stream.Stream; 10 | 11 | /** 12 | * Service adding a system path link to a quick access area of the OS or an application (e.g. file manager). 13 | * 14 | * @apiNote On purpose this service does not define, what an "link to a quick access area" is. The defintion depends on the OS. For example, the quick access area can be the home screen/desktop and the link would be an icon leading to the linked path. 15 | */ 16 | @FunctionalInterface 17 | public interface QuickAccessService extends NamedServiceProvider { 18 | 19 | /** 20 | * Creates an entry in the quick access area. 21 | * 22 | * @param target The filesystem path the quick access entry points to 23 | * @param displayName The display name of the quick access entry 24 | * @return a {@link QuickAccessEntry }, used to remove the entry again 25 | * @throws QuickAccessServiceException if adding an entry to the quick access area fails 26 | * @apiNote It depends on the service implementation wether the display name is used or not. 27 | */ 28 | @Blocking 29 | QuickAccessEntry add(@NotNull Path target, @NotNull String displayName) throws QuickAccessServiceException; 30 | 31 | /** 32 | * An entry of the quick access area, created by a service implementation. 33 | */ 34 | @FunctionalInterface 35 | interface QuickAccessEntry { 36 | 37 | /** 38 | * Removes this entry from the quick access area. 39 | * 40 | * @throws QuickAccessServiceException if removal fails. 41 | * @implSpec Service implementations should make this function idempotent, i.e. after the method is called once and succeeded, consecutive calls should not change anything or throw an error. 42 | */ 43 | @Blocking 44 | void remove() throws QuickAccessServiceException; 45 | 46 | } 47 | 48 | /** 49 | * Loads all supported service providers. 50 | * 51 | * @return Stream of supported {@link QuickAccessService} implementations (may be empty) 52 | */ 53 | static Stream get() { 54 | return IntegrationsLoader.loadAll(QuickAccessService.class); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/uiappearance/UiAppearanceProvider.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.uiappearance; 2 | 3 | import org.cryptomator.integrations.common.IntegrationsLoader; 4 | import org.jetbrains.annotations.ApiStatus; 5 | 6 | import java.util.Optional; 7 | 8 | /** 9 | * This is the interface used by Cryptomator to get os specific UI appearances and themes. 10 | * 11 | * @deprecated Cryptomator uses since version 1.14.0 the JavaFX framework in version 22, which provides via Platform.Preferences the system color scheme 12 | */ 13 | @Deprecated(since = "1.6.0") 14 | @ApiStatus.ScheduledForRemoval(inVersion = "1.7.0") 15 | public interface UiAppearanceProvider { 16 | 17 | /** 18 | * Loads the best-suited UiAppearanceProvider. 19 | * 20 | * @return preferred UiAppearanceProvider (if any) 21 | * @since 1.1.0 22 | */ 23 | static Optional get() { 24 | return IntegrationsLoader.load(UiAppearanceProvider.class); 25 | } 26 | 27 | /** 28 | * Gets the best-matching theme for the OS's current L&F. This might be an approximation, as the OS might support more variations than we do. 29 | * 30 | * @implSpec Should default to {@link Theme#LIGHT} if the OS theme can't be determined, should not throw exceptions. 31 | * @return The current OS theme 32 | */ 33 | Theme getSystemTheme(); 34 | 35 | /** 36 | * Adjusts parts of the UI to the desired theme, that can not be directly controlled from within Java. 37 | * This might be required for window decorations or tray icons. Can be no-op. 38 | * 39 | * @implSpec A best-effort attempt should be made. If adjustments fail, do not throw an exception. 40 | * @param theme What theme to adjust to 41 | */ 42 | void adjustToTheme(Theme theme); 43 | 44 | /** 45 | * Registers a listener that gets notified when the system theme changes. 46 | * 47 | * @param listener The listener 48 | * @throws UiAppearanceException If registering the listener failed. 49 | */ 50 | void addListener(UiAppearanceListener listener) throws UiAppearanceException; 51 | 52 | /** 53 | * Removes a previously registered listener. 54 | *

55 | * If the given listener has not been previously registered (i.e. it was never added) then this method call is a no-op. 56 | * 57 | * @param listener The listener 58 | * @throws UiAppearanceException If removing the listener failed. 59 | */ 60 | void removeListener(UiAppearanceListener listener) throws UiAppearanceException; 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/mount/MountCapability.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.mount; 2 | 3 | import java.nio.file.Path; 4 | 5 | /** 6 | * Describes what aspects of the mount implementation can or should be used. 7 | *

8 | * This may be used to show or hide different configuration options depending on the chosen mount provider. 9 | */ 10 | public enum MountCapability { 11 | /** 12 | * The builder supports {@link MountBuilder#setFileSystemName(String)}. 13 | */ 14 | FILE_SYSTEM_NAME, 15 | 16 | /** 17 | * The builder supports {@link MountBuilder#setLoopbackHostName(String)}. 18 | */ 19 | LOOPBACK_HOST_NAME, 20 | 21 | /** 22 | * The service provider supports {@link MountService#getDefaultLoopbackPort()} 23 | * and the builder requires {@link MountBuilder#setLoopbackPort(int)}. 24 | */ 25 | LOOPBACK_PORT, 26 | 27 | /** 28 | * The service provider supports {@link MountService#getDefaultMountFlags()} 29 | * and the builder requires {@link MountBuilder#setMountFlags(String)}. 30 | */ 31 | MOUNT_FLAGS, 32 | 33 | /** 34 | * With the exception of a provider-supplied default mount point, the mount point must be an existing dir. 35 | *

36 | * This option is mutually exclusive with {@link #MOUNT_WITHIN_EXISTING_PARENT}. 37 | * 38 | * @see #MOUNT_TO_SYSTEM_CHOSEN_PATH 39 | */ 40 | MOUNT_TO_EXISTING_DIR, 41 | 42 | /** 43 | * With the exception of a provider-supplied default mount point, the mount point must be a non-existing 44 | * child within an existing parent. 45 | *

46 | * This option is mutually exclusive with {@link #MOUNT_TO_EXISTING_DIR}. 47 | * 48 | * @see #MOUNT_TO_SYSTEM_CHOSEN_PATH 49 | */ 50 | MOUNT_WITHIN_EXISTING_PARENT, 51 | 52 | /** 53 | * The mount point may be a drive letter. 54 | * 55 | * @see #MOUNT_TO_EXISTING_DIR 56 | * @see #MOUNT_WITHIN_EXISTING_PARENT 57 | * @see #MOUNT_TO_SYSTEM_CHOSEN_PATH 58 | */ 59 | MOUNT_AS_DRIVE_LETTER, 60 | 61 | /** 62 | * The service provider supports suggesting a default mount point, if no mount point is set via {@link MountBuilder#setMountpoint(Path)}. 63 | */ 64 | MOUNT_TO_SYSTEM_CHOSEN_PATH, 65 | 66 | /** 67 | * The builder supports {@link MountBuilder#setReadOnly(boolean)}. 68 | */ 69 | READ_ONLY, 70 | 71 | /** 72 | * The mount supports {@link Mount#unmountForced()}. 73 | */ 74 | UNMOUNT_FORCED, 75 | 76 | /** 77 | * The builder requires {@link MountBuilder#setVolumeId(String)}. 78 | */ 79 | VOLUME_ID, 80 | 81 | /** 82 | * The builder supports {@link MountBuilder#setVolumeName(String)}. 83 | */ 84 | VOLUME_NAME 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/mount/MountService.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.mount; 2 | 3 | import org.cryptomator.integrations.common.IntegrationsLoader; 4 | import org.jetbrains.annotations.Contract; 5 | import org.jetbrains.annotations.Range; 6 | 7 | import java.nio.file.Path; 8 | import java.util.Set; 9 | import java.util.stream.Stream; 10 | 11 | /** 12 | * A mechanism to mount a file system. 13 | * 14 | * @since 1.2.0 15 | */ 16 | public interface MountService { 17 | 18 | /** 19 | * Loads all supported mount providers. 20 | * 21 | * @return Stream of supported MountProviders (may be empty) 22 | */ 23 | static Stream get() { 24 | return IntegrationsLoader.loadAll(MountService.class).filter(MountService::isSupported); 25 | } 26 | 27 | /** 28 | * Name of this provider. 29 | * 30 | * @return A human readable name of this provider 31 | */ 32 | String displayName(); 33 | 34 | /** 35 | * Indicates, if this provider can be used. 36 | * 37 | * @return true, if this provider is supported in the current OS environment 38 | * @implSpec This check needs to return fast and in constant time 39 | */ 40 | boolean isSupported(); 41 | 42 | /** 43 | * Default mount flags. May be empty. 44 | * 45 | * @return Concatenated String of valid mount flags 46 | * @throws UnsupportedOperationException If {@link MountCapability#MOUNT_FLAGS} is not supported 47 | */ 48 | default String getDefaultMountFlags() { 49 | throw new UnsupportedOperationException(); 50 | } 51 | 52 | /** 53 | * The default TCP port of the loopback address used by this provider. 54 | * 55 | * @return fixed TCP port or 0 to use a system-assigned port 56 | * @throws UnsupportedOperationException If {@link MountCapability#LOOPBACK_PORT} is not supported 57 | */ 58 | @Range(from = 0, to = Short.MAX_VALUE) 59 | default int getDefaultLoopbackPort() { 60 | throw new UnsupportedOperationException(); 61 | } 62 | 63 | /** 64 | * Mount capabilites supported by this provider. 65 | * 66 | * @return Set of supported {@link MountCapability}s 67 | */ 68 | Set capabilities(); 69 | 70 | /** 71 | * Tests whether this provider supports the given capability. 72 | * 73 | * @param capability The capability 74 | * @return {@code true} if supported 75 | */ 76 | default boolean hasCapability(MountCapability capability) { 77 | return capabilities().contains(capability); 78 | } 79 | 80 | 81 | /** 82 | * Creates a new mount builder. 83 | * 84 | * @param fileSystemRoot The root of the VFS to be mounted 85 | * @return New mount builder 86 | */ 87 | @Contract("_ -> new") 88 | MountBuilder forFileSystem(Path fileSystemRoot); 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/integrations/update/SemVerComparatorTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.update; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | import org.junit.jupiter.params.provider.CsvSource; 7 | 8 | import java.util.Comparator; 9 | 10 | public class SemVerComparatorTest { 11 | 12 | private final Comparator semVerComparator = SemVerComparator.INSTANCE; 13 | 14 | // equal versions 15 | 16 | @ParameterizedTest 17 | @CsvSource({ 18 | "1.23.4, 1.23.4", 19 | "1.23.4-alpha, 1.23.4-alpha", 20 | "1.23.4+20170101, 1.23.4+20171231", 21 | "1.23.4-alpha+20170101, 1.23.4-alpha+20171231" 22 | }) 23 | public void compareEqualVersions(String left, String right) { 24 | Assertions.assertEquals(0, Integer.signum(semVerComparator.compare(left, right))); 25 | } 26 | 27 | // newer versions in first argument 28 | 29 | @ParameterizedTest 30 | @CsvSource({ 31 | "1.23.5, 1.23.4", 32 | "1.24.4, 1.23.4", 33 | "1.23.4, 1.23", 34 | "1.23.4, 1.23.4-SNAPSHOT", 35 | "1.23.4, 1.23.4-56.78", 36 | "1.23.4-beta, 1.23.4-alpha", 37 | "1.23.4-alpha.1, 1.23.4-alpha", 38 | "1.23.4-56.79, 1.23.4-56.78", 39 | "1.23.4-alpha, 1.23.4-1", 40 | }) 41 | public void compareHigherToLowerVersions(String higherVersion, String lowerVersion) { 42 | Assertions.assertEquals(1, Integer.signum(semVerComparator.compare(higherVersion, lowerVersion))); 43 | } 44 | 45 | // newer versions in second argument 46 | 47 | @ParameterizedTest 48 | @CsvSource({ 49 | "1.23.4, 1.23.5", 50 | "1.23.4, 1.24.4", 51 | "1.23, 1.23.4", 52 | "1.23.4-SNAPSHOT, 1.23.4", 53 | "1.23.4-56.78, 1.23.4", 54 | "1.23.4-alpha, 1.23.4-beta", 55 | "1.23.4-alpha, 1.23.4-alpha.1", 56 | "1.23.4-56.78, 1.23.4-56.79", 57 | "1.23.4-1, 1.23.4-alpha", 58 | }) 59 | public void compareLowerToHigherVersions(String lowerVersion, String higherVersion) { 60 | Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare(lowerVersion, higherVersion))); 61 | } 62 | 63 | // test vector from https://semver.org/spec/v2.0.0.html#spec-item-11: 64 | // Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. 65 | @ParameterizedTest 66 | @CsvSource({ 67 | "1.0.0-alpha, 1.0.0-alpha.1", 68 | "1.0.0-alpha.1, 1.0.0-alpha.beta", 69 | "1.0.0-alpha.beta, 1.0.0-beta", 70 | "1.0.0-beta, 1.0.0-beta.2", 71 | "1.0.0-beta.2, 1.0.0-beta.11", 72 | "1.0.0-beta.11, 1.0.0-rc.1", 73 | "1.0.0-rc.1, 1.0.0" 74 | }) 75 | public void testPrecedenceSpec(String left, String right) { 76 | Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare(left, right))); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/common/ClassLoaderFactory.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.common; 2 | 3 | import org.jetbrains.annotations.Contract; 4 | import org.jetbrains.annotations.VisibleForTesting; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.io.IOException; 9 | import java.io.UncheckedIOException; 10 | import java.net.MalformedURLException; 11 | import java.net.URL; 12 | import java.net.URLClassLoader; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.util.Arrays; 16 | import java.util.stream.Collectors; 17 | 18 | class ClassLoaderFactory { 19 | 20 | private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderFactory.class); 21 | private static final String USER_HOME = System.getProperty("user.home"); 22 | private static final String PLUGIN_DIR_KEY = "cryptomator.pluginDir"; 23 | private static final String JAR_SUFFIX = ".jar"; 24 | 25 | /** 26 | * Attempts to find {@code .jar} files in the path specified in {@value #PLUGIN_DIR_KEY} system property. 27 | * A new class loader instance is returned that loads classes from the given classes. 28 | * 29 | * @return A new URLClassLoader that is aware of all {@code .jar} files in the plugin dir 30 | */ 31 | @Contract(value = "-> new", pure = true) 32 | public static URLClassLoader forPluginDir() { 33 | String val = System.getProperty(PLUGIN_DIR_KEY, ""); 34 | final Path p; 35 | if (val.startsWith("~/")) { 36 | p = Path.of(USER_HOME).resolve(val.substring(2)); 37 | } else { 38 | p = Path.of(val); 39 | } 40 | return forPluginDirWithPath(p); 41 | } 42 | 43 | @VisibleForTesting 44 | @Contract(value = "_ -> new", pure = true) 45 | static URLClassLoader forPluginDirWithPath(Path path) throws UncheckedIOException { 46 | var jars = findJars(path); 47 | if (LOG.isDebugEnabled()) { 48 | String jarList = Arrays.stream(jars).map(URL::getPath).collect(Collectors.joining(", ")); 49 | LOG.debug("Found jars in cryptomator.pluginDir: {}", jarList); 50 | } 51 | return URLClassLoader.newInstance(jars); 52 | } 53 | 54 | @VisibleForTesting 55 | static URL[] findJars(Path path) { 56 | try (var stream = Files.walk(path)) { 57 | return stream.filter(ClassLoaderFactory::isJarFile).map(ClassLoaderFactory::toUrl).toArray(URL[]::new); 58 | } catch (IOException | UncheckedIOException e) { 59 | // unable to locate any jars // TODO: log a warning? 60 | return new URL[0]; 61 | } 62 | } 63 | 64 | private static URL toUrl(Path path) throws UncheckedIOException { 65 | try { 66 | return path.toUri().toURL(); 67 | } catch (MalformedURLException e) { 68 | throw new UncheckedIOException(e); 69 | } 70 | } 71 | 72 | private static boolean isJarFile(Path path) { 73 | return Files.isRegularFile(path) && path.getFileName().toString().toLowerCase().endsWith(JAR_SUFFIX); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/update/UpdateMechanism.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.update; 2 | 3 | import org.cryptomator.integrations.common.IntegrationsLoader; 4 | import org.cryptomator.integrations.common.NamedServiceProvider; 5 | import org.jetbrains.annotations.ApiStatus; 6 | import org.jetbrains.annotations.Blocking; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.net.http.HttpClient; 11 | import java.util.Optional; 12 | 13 | @ApiStatus.Experimental 14 | public interface UpdateMechanism> extends NamedServiceProvider { 15 | 16 | String UPDATE_MECHANISM_PROPERTY = "cryptomator.updateMechanism"; 17 | 18 | @SuppressWarnings("rawtypes") 19 | static Optional get() { 20 | return Optional.ofNullable(System.getProperty(UPDATE_MECHANISM_PROPERTY)) 21 | .flatMap(name -> IntegrationsLoader.loadSpecific(UpdateMechanism.class, name)); 22 | } 23 | 24 | /** 25 | * Checks whether an update is available by comparing the given version strings. 26 | * @param updateVersion The version string of the update, e.g. "1.2.3". 27 | * @param installedVersion The version string of the currently installed application, e.g. "1.2.3-beta4". 28 | * @return true if an update is available, false otherwise. Always true for SNAPSHOT versions. 29 | */ 30 | static boolean isUpdateAvailable(String updateVersion, String installedVersion) { 31 | if (installedVersion.contains("SNAPSHOT")) { 32 | return true; // SNAPSHOT versions are always considered to be outdated. 33 | } else { 34 | return SemVerComparator.INSTANCE.compare(updateVersion, installedVersion) > 0; 35 | } 36 | } 37 | 38 | /** 39 | * Checks whether an update is available. 40 | * @param currentVersion The full version string of the currently installed application, e.g. "1.2.3-beta4". 41 | * @param httpClient An HTTP client that can be used to check for updates. 42 | * @return An {@link UpdateInfo} if an update is available, or null otherwise. 43 | * @throws UpdateFailedException If the availability of an update could not be determined 44 | */ 45 | @Blocking 46 | @Nullable 47 | T checkForUpdate(String currentVersion, HttpClient httpClient) throws UpdateFailedException; 48 | 49 | /** 50 | * Returns the first step to prepare the update. This can be anything like downloading the update, checking signatures, etc. 51 | * @param updateInfo The {@link UpdateInfo} representing the update to be prepared. 52 | * @return a new {@link UpdateStep} that can be used to monitor the progress of the update preparation. The task will complete when the preparation is done. 53 | * @throws UpdateFailedException If no update process can be started, e.g. due to network or I/O issues. 54 | */ 55 | @NotNull 56 | UpdateStep firstStep(T updateInfo) throws UpdateFailedException; 57 | 58 | } -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/update/SemVerComparator.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.update; 2 | 3 | import java.util.Comparator; 4 | import java.util.regex.Pattern; 5 | 6 | /** 7 | * Compares version strings according to SemVer 2.0.0. 8 | */ 9 | public class SemVerComparator implements Comparator { 10 | 11 | public static final SemVerComparator INSTANCE = new SemVerComparator(); 12 | 13 | private static final Pattern VERSION_SEP = Pattern.compile("\\."); // http://semver.org/spec/v2.0.0.html#spec-item-2 14 | private static final String PRE_RELEASE_SEP = "-"; // http://semver.org/spec/v2.0.0.html#spec-item-9 15 | private static final String BUILD_SEP = "+"; // http://semver.org/spec/v2.0.0.html#spec-item-10 16 | 17 | @Override 18 | public int compare(String version1, String version2) { 19 | // "Build metadata SHOULD be ignored when determining version precedence. 20 | // Thus, two versions that differ only in the build metadata, have the same precedence." 21 | String trimmedV1 = substringBefore(version1, BUILD_SEP); 22 | String trimmedV2 = substringBefore(version2, BUILD_SEP); 23 | 24 | if (trimmedV1.equals(trimmedV2)) { 25 | return 0; 26 | } 27 | 28 | String v1MajorMinorPatch = substringBefore(trimmedV1, PRE_RELEASE_SEP); 29 | String v2MajorMinorPatch = substringBefore(trimmedV2, PRE_RELEASE_SEP); 30 | String v1PreReleaseVersion = substringAfter(trimmedV1, PRE_RELEASE_SEP); 31 | String v2PreReleaseVersion = substringAfter(trimmedV2, PRE_RELEASE_SEP); 32 | return compare(v1MajorMinorPatch, v1PreReleaseVersion, v2MajorMinorPatch, v2PreReleaseVersion); 33 | } 34 | 35 | private static int compare(String v1MajorMinorPatch, String v1PreReleaseVersion, String v2MajorMinorPatch, String v2PreReleaseVersion) { 36 | int comparisonResult = compareNumericallyThenLexicographically(v1MajorMinorPatch, v2MajorMinorPatch); 37 | if (comparisonResult == 0) { 38 | if (v1PreReleaseVersion.isEmpty()) { 39 | return 1; // 1.0.0 > 1.0.0-BETA 40 | } else if (v2PreReleaseVersion.isEmpty()) { 41 | return -1; // 1.0.0-BETA < 1.0.0 42 | } else { 43 | return compareNumericallyThenLexicographically(v1PreReleaseVersion, v2PreReleaseVersion); 44 | } 45 | } else { 46 | return comparisonResult; 47 | } 48 | } 49 | 50 | private static int compareNumericallyThenLexicographically(String version1, String version2) { 51 | final String[] vComps1 = VERSION_SEP.split(version1); 52 | final String[] vComps2 = VERSION_SEP.split(version2); 53 | final int commonCompCount = Math.min(vComps1.length, vComps2.length); 54 | 55 | for (int i = 0; i < commonCompCount; i++) { 56 | int subversionComparisonResult; 57 | try { 58 | final int v1 = Integer.parseInt(vComps1[i]); 59 | final int v2 = Integer.parseInt(vComps2[i]); 60 | subversionComparisonResult = v1 - v2; 61 | } catch (NumberFormatException ex) { 62 | // ok, lets compare this fragment lexicographically 63 | subversionComparisonResult = vComps1[i].compareTo(vComps2[i]); 64 | } 65 | if (subversionComparisonResult != 0) { 66 | return subversionComparisonResult; 67 | } 68 | } 69 | 70 | // all in common so far? longest version string is considered the higher version: 71 | return vComps1.length - vComps2.length; 72 | } 73 | 74 | private static String substringBefore(String str, String separator) { 75 | int index = str.indexOf(separator); 76 | return index == -1 ? str : str.substring(0, index); 77 | } 78 | 79 | private static String substringAfter(String str, String separator) { 80 | int index = str.indexOf(separator); 81 | return index == -1 ? "" : str.substring(index + separator.length()); 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/update/UpdateStep.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.update; 2 | 3 | import org.cryptomator.integrations.Localization; 4 | import org.jetbrains.annotations.ApiStatus; 5 | import org.jetbrains.annotations.NonBlocking; 6 | import org.jetbrains.annotations.Nullable; 7 | import org.jetbrains.annotations.Range; 8 | 9 | import java.io.IOException; 10 | import java.util.concurrent.Callable; 11 | import java.util.concurrent.TimeUnit; 12 | import java.util.function.Supplier; 13 | 14 | @ApiStatus.Experimental 15 | public interface UpdateStep { 16 | 17 | /** 18 | * A magic constant indicating that the application shall terminate. 19 | *

20 | * This step can be returned as the last step of the update process, usually immediately after a restart has been scheduled. 21 | */ 22 | UpdateStep EXIT = new NoopUpdateStep(Localization.get().getString("org.cryptomator.api.update.updateStep.EXIT")); 23 | 24 | /** 25 | * A magic constant indicating that the update process shall be retried. 26 | */ 27 | UpdateStep RETRY = new NoopUpdateStep(Localization.get().getString("org.cryptomator.api.update.updateStep.RETRY")); 28 | 29 | 30 | static UpdateStep of(String name, Callable nextStep) { 31 | return new UpdateStepAdapter() { 32 | 33 | @Override 34 | public UpdateStep call() throws Exception { 35 | return nextStep.call(); 36 | } 37 | 38 | @Override 39 | public String description() { 40 | return name; 41 | } 42 | }; 43 | } 44 | 45 | /** 46 | * A short description of this update step. 47 | * @return a human-readable description of this update step. 48 | */ 49 | String description(); 50 | 51 | /** 52 | * Starts work on this update step in a non-blocking manner. 53 | * @throws IllegalThreadStateException if this step has already been started. 54 | */ 55 | @NonBlocking 56 | void start() throws IllegalThreadStateException; 57 | 58 | /** 59 | * A thread-safe method to check the progress of the update preparation. 60 | * @return a value between 0.0 and 1.0 indicating the progress of the update preparation or -1.0 indicating indeterminate progress. 61 | */ 62 | double preparationProgress(); 63 | 64 | /** 65 | * Cancels this update step and cleans up any temporary resources. 66 | */ 67 | void cancel(); 68 | 69 | /** 70 | * Blocks the current thread until this update step completed or an error occurred. 71 | * If this step failed, an exception will be rethrown as soon as attempting to invoke {@link #nextStep()}. 72 | *

73 | * If the step is already complete, this method returns immediately. 74 | * 75 | * @throws InterruptedException if the current thread is interrupted while waiting. 76 | */ 77 | void await() throws InterruptedException; 78 | 79 | /** 80 | * Blocks the current thread until this update step completed or an error occurred, or until the specified timeout expires. 81 | * If this step failed, an exception will be rethrown as soon as attempting to invoke {@link #nextStep()}. 82 | *

83 | * If the step is already complete, this method returns immediately. 84 | * 85 | * @param timeout the maximum time to wait 86 | * @param unit the time unit of the {@code timeout} argument 87 | * @return true if the update is prepared 88 | */ 89 | boolean await(long timeout, TimeUnit unit) throws InterruptedException; 90 | 91 | default boolean isDone() { 92 | try { 93 | return await(0, TimeUnit.MILLISECONDS); 94 | } catch (InterruptedException e) { 95 | Thread.currentThread().interrupt(); 96 | return false; 97 | } 98 | } 99 | 100 | /** 101 | * After running this step to completion, this method returns the next step of the update process. 102 | * 103 | * @return the next {@link UpdateStep step} of the update process or null if this was the final step. 104 | * @throws IllegalStateException if this step didn't complete yet or other preconditions aren't met. 105 | * @throws IOException indicating an error before reaching the next step, e.g. during execution of this step. 106 | * @implSpec The returned {@link UpdateStep} must either be stateless or a new instance must be returned on each call. 107 | */ 108 | @Nullable 109 | UpdateStep nextStep() throws IllegalStateException, IOException; 110 | 111 | } -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.keychain; 2 | 3 | import org.cryptomator.integrations.common.IntegrationsLoader; 4 | import org.cryptomator.integrations.common.NamedServiceProvider; 5 | import org.jetbrains.annotations.ApiStatus; 6 | import org.jetbrains.annotations.Blocking; 7 | import org.jetbrains.annotations.Nls; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.util.stream.Stream; 11 | 12 | /** 13 | * This is the interface used by Cryptomator to store passwords securely in external keychains, such as system keychains or password managers. 14 | */ 15 | public interface KeychainAccessProvider extends NamedServiceProvider { 16 | 17 | /** 18 | * Loads all available KeychainAccessProvider. 19 | * 20 | * @return a stream of {@link #isSupported() supported} KeychainAccessProviders 21 | * @since 1.1.0 22 | */ 23 | static Stream get() { 24 | return IntegrationsLoader.loadAll(KeychainAccessProvider.class).filter(KeychainAccessProvider::isSupported); 25 | } 26 | 27 | /** 28 | * Associates a passphrase with a given key and a name for that key. 29 | *

30 | * Note: Caller is responsible for zeroing the passphrase array after use. 31 | * 32 | * @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}. 33 | * @param displayName The according name to the key. That's the name of the vault displayed in the UI. 34 | * It's passed to the keychain as an additional information about the vault besides the key. 35 | * The parameter does not need to be unique or be checked by the keychain. 36 | * @param passphrase The secret to store in this keychain. 37 | * @throws KeychainAccessException If storing the password failed 38 | */ 39 | @Blocking 40 | void storePassphrase(String key, @Nullable String displayName, CharSequence passphrase) throws KeychainAccessException; 41 | 42 | /** 43 | * @param key Unique key previously used while {@link #storePassphrase(String, String, CharSequence)} storing a passphrase}. 44 | * @return The stored passphrase for the given key or null if no value for the given key could be found. 45 | * @throws KeychainAccessException If loading the password failed 46 | */ 47 | @Blocking 48 | char[] loadPassphrase(String key) throws KeychainAccessException; 49 | 50 | /** 51 | * Deletes a passphrase with a given key. 52 | * 53 | * @param key Unique key previously used while {@link #storePassphrase(String, String, CharSequence)} storing a passphrase}. 54 | * @throws KeychainAccessException If deleting the password failed 55 | */ 56 | void deletePassphrase(String key) throws KeychainAccessException; 57 | 58 | /** 59 | * Updates a passphrase with a given key and stores a name for that key. Noop, if there is no item for the given key. 60 | *

61 | * Note: Caller is responsible for zeroing the passphrase array after use. 62 | * 63 | * @param key Unique key previously used while {@link #storePassphrase(String, String, CharSequence)} storing a passphrase}. 64 | * @param displayName The according name to the key. That's the name of the vault displayed in the UI. 65 | * It's passed to the keychain as an additional information about the vault besides the key. 66 | * The parameter does not need to be unique or be checked by the keychain. 67 | * @param passphrase The secret to be updated in this keychain. 68 | * @throws KeychainAccessException If changing the password failed 69 | */ 70 | @Blocking 71 | void changePassphrase(String key, @Nullable String displayName, CharSequence passphrase) throws KeychainAccessException; 72 | 73 | /** 74 | * @return true if this KeychainAccessIntegration works on the current machine. 75 | * @implSpec This method must not throw any exceptions and should fail fast 76 | * returning false if it can't determine availability of the checked strategy 77 | */ 78 | boolean isSupported(); 79 | 80 | /** 81 | * @return true if the keychain to be accessed is locked. Accesing a locked keychain 82 | * requires to unlock the keychain. The keychain backend will show an unlock dialog. 83 | * returning false if the keychain to be accessed is unlocked 84 | */ 85 | boolean isLocked(); 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/mount/MountBuilder.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.mount; 2 | 3 | import org.jetbrains.annotations.Contract; 4 | import org.jetbrains.annotations.Range; 5 | 6 | import java.nio.file.Path; 7 | 8 | /** 9 | * Builder to mount a filesystem. 10 | *

11 | * The setter may attempt to validate the input, but {@link #mount()} may still fail due to missing or invalid (combination of) options. 12 | * This holds especially for {@link MountBuilder#setMountFlags(String)}; 13 | */ 14 | public interface MountBuilder { 15 | 16 | /** 17 | * Sets the file system name. 18 | * 19 | * @param fileSystemName file system name 20 | * @return this 21 | * @throws UnsupportedOperationException If {@link MountCapability#FILE_SYSTEM_NAME} is not supported 22 | */ 23 | @Contract("_ -> this") 24 | default MountBuilder setFileSystemName(String fileSystemName) { 25 | throw new UnsupportedOperationException(); 26 | } 27 | 28 | /** 29 | * Use the given host name as the loopback address. 30 | * 31 | * @param hostName string conforming with the uri host part 32 | * @return this 33 | * @throws UnsupportedOperationException If {@link MountCapability#LOOPBACK_HOST_NAME} is not supported 34 | */ 35 | @Contract("_ -> this") 36 | default MountBuilder setLoopbackHostName(String hostName) { 37 | throw new UnsupportedOperationException(); 38 | } 39 | 40 | /** 41 | * Use the given TCP port of the loopback address. 42 | * 43 | * @param port Fixed TCP port or 0 to use a system-assigned port 44 | * @return this 45 | * @throws UnsupportedOperationException If {@link MountCapability#LOOPBACK_PORT} is not supported 46 | */ 47 | @Contract("_ -> this") 48 | default MountBuilder setLoopbackPort(@Range(from = 0, to = Short.MAX_VALUE) int port) { 49 | throw new UnsupportedOperationException(); 50 | } 51 | 52 | /** 53 | * Sets the mount point. 54 | *

55 | * Unless the mount service provider supports {@link MountCapability#MOUNT_TO_SYSTEM_CHOSEN_PATH}, setting a mount point is required. 56 | * 57 | * @param mountPoint Where to mount the volume 58 | * @return this 59 | */ 60 | @Contract("_ -> this") 61 | default MountBuilder setMountpoint(Path mountPoint) { 62 | throw new UnsupportedOperationException(); 63 | } 64 | 65 | /** 66 | * Sets mount flags. 67 | * 68 | * @param mountFlags Mount flags 69 | * @return this 70 | * @throws UnsupportedOperationException If {@link MountCapability#MOUNT_FLAGS} is not supported 71 | * @see MountService#getDefaultMountFlags() 72 | */ 73 | @Contract("_ -> this") 74 | default MountBuilder setMountFlags(String mountFlags) { 75 | throw new UnsupportedOperationException(); 76 | } 77 | 78 | 79 | /** 80 | * Instructs the mount to be read-only. 81 | * 82 | * @param mountReadOnly Whether to mount read-only. 83 | * @return this 84 | * @throws UnsupportedOperationException If {@link MountCapability#READ_ONLY} is not supported 85 | */ 86 | @Contract("_ -> this") 87 | default MountBuilder setReadOnly(boolean mountReadOnly) { 88 | throw new UnsupportedOperationException(); 89 | } 90 | 91 | /** 92 | * Sets a unique volume id. 93 | *

94 | * The volume id is used as a path component, thus must conform with the os-dependent path component restrictions. 95 | * 96 | * @param volumeId String conforming with the os-dependent path component restrictions 97 | * @return this 98 | * @throws UnsupportedOperationException If {@link MountCapability#VOLUME_ID} is not supported 99 | */ 100 | @Contract("_ -> this") 101 | default MountBuilder setVolumeId(String volumeId) { 102 | throw new UnsupportedOperationException(); 103 | } 104 | 105 | /** 106 | * Sets a volume name. 107 | *

108 | * The volume name is intended to be human-readable. The input string might be altered to replace non-conforming characters and thus is not suited to identify the volume. 109 | * 110 | * @param volumeName String conforming with the os-dependent naming restrictions 111 | * @return this 112 | * @throws UnsupportedOperationException If {@link MountCapability#VOLUME_NAME} is not supported 113 | */ 114 | @Contract("_ -> this") 115 | default MountBuilder setVolumeName(String volumeName) { 116 | throw new UnsupportedOperationException(); 117 | } 118 | 119 | /** 120 | * Mounts the file system. 121 | * 122 | * @return A mount handle 123 | * @throws MountFailedException If mounting failed 124 | */ 125 | @Contract(" -> new") 126 | Mount mount() throws MountFailedException; 127 | 128 | } 129 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | pull_request_target: 5 | types: [labeled] 6 | 7 | env: 8 | JAVA_VERSION: 25 9 | 10 | jobs: 11 | build: 12 | name: Build and Test 13 | runs-on: ubuntu-latest 14 | permissions: 15 | id-token: write # Required for the attestations step 16 | attestations: write # Required for the attestations step 17 | steps: 18 | - uses: actions/checkout@v5 19 | - uses: actions/setup-java@v5 20 | with: 21 | distribution: 'temurin' 22 | java-version: ${{ env.JAVA_VERSION }} 23 | cache: 'maven' 24 | - name: Ensure to use tagged version 25 | if: startsWith(github.ref, 'refs/tags/') 26 | run: mvn versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/} 27 | - name: Build and Test 28 | run: mvn -B verify --no-transfer-progress 29 | - name: Attest 30 | if: startsWith(github.ref, 'refs/tags/') 31 | uses: actions/attest-build-provenance@v3 32 | with: 33 | subject-path: | 34 | target/*.jar 35 | target/*.pom 36 | - uses: actions/upload-artifact@v5 37 | with: 38 | name: artifacts 39 | path: target/*.jar 40 | 41 | deploy-central: 42 | name: Deploy to Maven Central 43 | runs-on: ubuntu-latest 44 | permissions: {} 45 | needs: [build] 46 | if: github.repository_owner == 'cryptomator' && (startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[deploy]')) 47 | steps: 48 | - uses: actions/checkout@v5 49 | - uses: actions/setup-java@v5 50 | with: 51 | distribution: 'temurin' 52 | java-version: ${{ env.JAVA_VERSION }} 53 | cache: 'maven' 54 | server-id: central 55 | server-username: MAVEN_CENTRAL_USERNAME 56 | server-password: MAVEN_CENTRAL_PASSWORD 57 | - name: Ensure to use tagged version 58 | if: startsWith(github.ref, 'refs/tags/') 59 | run: mvn versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/} 60 | - name: Verify project version is -SNAPSHOT 61 | if: startsWith(github.ref, 'refs/tags/') == false 62 | run: | 63 | PROJECT_VERSION=$(mvn help:evaluate "-Dexpression=project.version" -q -DforceStdout) 64 | test "${PROJECT_VERSION: -9}" = "-SNAPSHOT" 65 | - name: Deploy to Maven Central 66 | run: mvn deploy -B -DskipTests -Psign,deploy-central --no-transfer-progress 67 | env: 68 | MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 69 | MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 70 | MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} 71 | MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import 72 | MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }} 73 | 74 | deploy-github: 75 | name: Deploy to GitHub Packages 76 | runs-on: ubuntu-latest 77 | permissions: 78 | packages: write # Required for the deploy to GitHub Packages step 79 | needs: [build] 80 | if: github.repository_owner == 'cryptomator' && (startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[deploy]')) 81 | steps: 82 | - uses: actions/checkout@v5 83 | - uses: actions/setup-java@v5 84 | with: 85 | java-version: ${{ env.JAVA_VERSION }} 86 | distribution: 'temurin' 87 | cache: 'maven' 88 | - name: Ensure to use tagged version 89 | if: startsWith(github.ref, 'refs/tags/') 90 | run: mvn versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/} 91 | - name: Verify project version is -SNAPSHOT 92 | if: startsWith(github.ref, 'refs/tags/') == false 93 | run: | 94 | PROJECT_VERSION=$(mvn help:evaluate "-Dexpression=project.version" -q -DforceStdout) 95 | test "${PROJECT_VERSION: -9}" = "-SNAPSHOT" 96 | - name: Deploy to GitHub Packages 97 | run: mvn deploy -B -DskipTests -Psign,deploy-github --no-transfer-progress 98 | env: 99 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 100 | MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} 101 | MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import 102 | MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }} 103 | 104 | release: 105 | name: Release 106 | runs-on: ubuntu-latest 107 | permissions: 108 | contents: write # Required for the release step 109 | needs: [deploy-central, deploy-github] 110 | if: startsWith(github.ref, 'refs/tags/') 111 | steps: 112 | - name: Create Release 113 | uses: softprops/action-gh-release@v2 114 | with: 115 | prerelease: true 116 | token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} 117 | generate_release_notes: true 118 | body: | 119 | For a list of all notable changes, read the [changelog](/CHANGELOG.md). 120 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/integrations/common/ClassLoaderFactoryTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.common; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Nested; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.io.TempDir; 9 | import org.mockito.Mockito; 10 | 11 | import java.io.ByteArrayInputStream; 12 | import java.io.IOException; 13 | import java.net.URL; 14 | import java.net.URLClassLoader; 15 | import java.nio.file.Files; 16 | import java.nio.file.Path; 17 | import java.util.Arrays; 18 | import java.util.Comparator; 19 | 20 | public class ClassLoaderFactoryTest { 21 | 22 | @Nested 23 | @DisplayName("When two .jars exist in the plugin dir") 24 | public class WithJars { 25 | 26 | private static final byte[] FOO_CONTENTS = "foo = 42".getBytes(); 27 | private static final byte[] BAR_CONTENTS = "bar = 23".getBytes(); 28 | private Path pluginDir; 29 | 30 | @BeforeEach 31 | public void setup(@TempDir Path tmpDir) throws IOException { 32 | Files.createDirectory(tmpDir.resolve("plugin1")); 33 | try (var out = Files.newOutputStream(tmpDir.resolve("plugin1/foo.jar")); 34 | var jar = JarBuilder.withTarget(out)) { 35 | jar.addFile("foo.properties", new ByteArrayInputStream(FOO_CONTENTS)); 36 | } 37 | 38 | Files.createDirectory(tmpDir.resolve("plugin2")); 39 | try (var out = Files.newOutputStream(tmpDir.resolve("plugin2/bar.jar")); 40 | var jar = JarBuilder.withTarget(out)) { 41 | jar.addFile("bar.properties", new ByteArrayInputStream(BAR_CONTENTS)); 42 | } 43 | 44 | this.pluginDir = tmpDir; 45 | } 46 | 47 | @Test 48 | @DisplayName("can load resources from both jars") 49 | public void testForPluginDirWithPath() throws IOException { 50 | try (var cl = ClassLoaderFactory.forPluginDirWithPath(pluginDir); 51 | var fooIn = cl.getResourceAsStream("foo.properties"); 52 | var barIn = cl.getResourceAsStream("bar.properties")) { 53 | var fooContents = fooIn.readAllBytes(); 54 | var barContents = barIn.readAllBytes(); 55 | 56 | Assertions.assertArrayEquals(FOO_CONTENTS, fooContents); 57 | Assertions.assertArrayEquals(BAR_CONTENTS, barContents); 58 | } 59 | } 60 | 61 | @Test 62 | @DisplayName("can load resources when path is set in cryptomator.pluginDir") 63 | public void testForPluginDirFromSysProp() throws IOException { 64 | System.setProperty("cryptomator.pluginDir", pluginDir.toString()); 65 | 66 | try (var cl = ClassLoaderFactory.forPluginDir(); 67 | var fooIn = cl.getResourceAsStream("foo.properties"); 68 | var barIn = cl.getResourceAsStream("bar.properties")) { 69 | var fooContents = fooIn.readAllBytes(); 70 | var barContents = barIn.readAllBytes(); 71 | 72 | Assertions.assertArrayEquals(FOO_CONTENTS, fooContents); 73 | Assertions.assertArrayEquals(BAR_CONTENTS, barContents); 74 | } 75 | } 76 | } 77 | 78 | @Test 79 | @DisplayName("read path from cryptomator.pluginDir") 80 | public void testReadPluginDirFromSysProp() { 81 | var ucl = Mockito.mock(URLClassLoader.class, "ucl"); 82 | var absPath = "/there/will/be/plugins"; 83 | try (var mockedClass = Mockito.mockStatic(ClassLoaderFactory.class)) { 84 | mockedClass.when(() -> ClassLoaderFactory.forPluginDir()).thenCallRealMethod(); 85 | mockedClass.when(() -> ClassLoaderFactory.forPluginDirWithPath(Path.of(absPath))).thenReturn(ucl); 86 | 87 | System.setProperty("cryptomator.pluginDir", absPath); 88 | var result = ClassLoaderFactory.forPluginDir(); 89 | 90 | Assertions.assertSame(ucl, result); 91 | } 92 | } 93 | 94 | @Test 95 | @DisplayName("read path from cryptomator.pluginDir and replace ~/ with user.home") 96 | public void testReadPluginDirFromSysPropAndReplaceHome() { 97 | var ucl = Mockito.mock(URLClassLoader.class, "ucl"); 98 | var relPath = "~/there/will/be/plugins"; 99 | var absPath = Path.of(System.getProperty("user.home")).resolve("there/will/be/plugins"); 100 | try (var mockedClass = Mockito.mockStatic(ClassLoaderFactory.class)) { 101 | mockedClass.when(() -> ClassLoaderFactory.forPluginDir()).thenCallRealMethod(); 102 | mockedClass.when(() -> ClassLoaderFactory.forPluginDirWithPath(absPath)).thenReturn(ucl); 103 | 104 | System.setProperty("cryptomator.pluginDir", relPath); 105 | var result = ClassLoaderFactory.forPluginDir(); 106 | 107 | Assertions.assertSame(ucl, result); 108 | } 109 | } 110 | 111 | @Test 112 | @DisplayName("findJars returns empty list if not containing jars") 113 | public void testFindJars1(@TempDir Path tmpDir) throws IOException { 114 | Files.createDirectories(tmpDir.resolve("dir1")); 115 | Files.createFile(tmpDir.resolve("file1")); 116 | 117 | var urls = ClassLoaderFactory.findJars(tmpDir); 118 | 119 | Assertions.assertArrayEquals(new URL[0], urls); 120 | } 121 | 122 | @Test 123 | @DisplayName("findJars returns urls of found jars") 124 | public void testFindJars2(@TempDir Path tmpDir) throws IOException { 125 | Files.createDirectories(tmpDir.resolve("dir1")); 126 | Files.createDirectories(tmpDir.resolve("dir2")); 127 | Files.createDirectories(tmpDir.resolve("dir1").resolve("dir2")); 128 | Files.createFile(tmpDir.resolve("a.jar")); 129 | Files.createFile(tmpDir.resolve("a.txt")); 130 | Files.createFile(tmpDir.resolve("dir2").resolve("b.jar")); 131 | 132 | var urls = ClassLoaderFactory.findJars(tmpDir); 133 | 134 | Arrays.sort(urls, Comparator.comparing(URL::toString)); 135 | Assertions.assertArrayEquals(new URL[] { 136 | new URL(tmpDir.toUri() + "a.jar"), 137 | new URL(tmpDir.toUri() + "dir2/b.jar") 138 | }, urls); 139 | } 140 | 141 | } -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/update/DownloadUpdateMechanism.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.update; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.jetbrains.annotations.Blocking; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.net.URI; 15 | import java.net.http.HttpClient; 16 | import java.net.http.HttpRequest; 17 | import java.net.http.HttpResponse; 18 | import java.nio.file.Files; 19 | import java.nio.file.Path; 20 | import java.util.HexFormat; 21 | import java.util.List; 22 | 23 | public abstract class DownloadUpdateMechanism implements UpdateMechanism { 24 | 25 | private static final Logger LOG = LoggerFactory.getLogger(DownloadUpdateMechanism.class); 26 | private static final String LATEST_VERSION_API_URL = "https://api.cryptomator.org/connect/apps/desktop/latest-version?format=1"; 27 | private static final ObjectMapper MAPPER = new ObjectMapper(); 28 | 29 | @Override 30 | public DownloadUpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) { 31 | try { 32 | HttpRequest request = HttpRequest.newBuilder().uri(URI.create(LATEST_VERSION_API_URL)).build(); 33 | HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); 34 | if (response.statusCode() != 200) { 35 | LOG.warn("Failed to fetch release: HTTP {}", response.statusCode()); 36 | return null; 37 | } 38 | var release = MAPPER.readValue(response.body(), LatestVersionResponse.class); 39 | return checkForUpdate(currentVersion, release); 40 | } catch (InterruptedException e) { 41 | Thread.currentThread().interrupt(); 42 | LOG.debug("Update check interrupted."); 43 | return null; 44 | } catch (IOException e) { 45 | LOG.warn("Update check failed", e); 46 | return null; 47 | } 48 | } 49 | 50 | /** 51 | * Returns the first step to prepare the update. This downloads the {@link DownloadUpdateInfo#asset() asset} to a temporary location and verifies its checksum. 52 | * @param updateInfo The {@link DownloadUpdateInfo} retrieved from {@link #checkForUpdate(String, HttpClient)}. 53 | * @return a new {@link UpdateStep} that can be used to monitor the download progress. 54 | * @throws UpdateFailedException When failing to prepare a temporary download location. 55 | */ 56 | @Override 57 | public UpdateStep firstStep(DownloadUpdateInfo updateInfo) throws UpdateFailedException { 58 | try { 59 | Path workDir = Files.createTempDirectory("cryptomator-update"); 60 | return new FirstStep(workDir, updateInfo); 61 | } catch (IOException e) { 62 | throw new UpdateFailedException("Failed to create temporary directory for update", e); 63 | } 64 | } 65 | 66 | /** 67 | * Second step that is executed after the download has completed in the {@link #firstStep(DownloadUpdateInfo) first step}. 68 | * @param workDir A temporary working directory to which the asset has been downloaded. 69 | * @param assetPath The path of the downloaded asset. 70 | * @param updateInfo The {@link DownloadUpdateInfo} representing the update. 71 | * @return The next step of the update process. 72 | * @throws IllegalStateException if preconditions aren't met. 73 | * @throws IOException indicating an error preventing the next step from starting. 74 | * @implSpec The returned {@link UpdateStep} must either be stateless or a new instance must be returned on each call. 75 | */ 76 | public abstract UpdateStep secondStep(Path workDir, Path assetPath, DownloadUpdateInfo updateInfo) throws IllegalStateException, IOException; 77 | 78 | @Nullable 79 | @Blocking 80 | protected abstract DownloadUpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response); 81 | 82 | @JsonIgnoreProperties(ignoreUnknown = true) 83 | public record LatestVersionResponse( 84 | @JsonProperty("latestVersion") LatestVersion latestVersion, 85 | @JsonProperty("assets") List assets 86 | ) {} 87 | 88 | @JsonIgnoreProperties(ignoreUnknown = true) 89 | public record LatestVersion( 90 | @JsonProperty("mac") String macVersion, 91 | @JsonProperty("win") String winVersion, 92 | @JsonProperty("linux") String linuxVersion 93 | ) {} 94 | 95 | @JsonIgnoreProperties(ignoreUnknown = true) 96 | public record Asset( 97 | @JsonProperty("name") String name, 98 | @JsonProperty("digest") String digest, 99 | @JsonProperty("size") long size, 100 | @JsonProperty("downloadUrl") String downloadUrl 101 | ) {} 102 | 103 | private class FirstStep extends DownloadUpdateStep { 104 | private final Path workDir; 105 | private final DownloadUpdateInfo updateInfo; 106 | 107 | public FirstStep(Path workDir, DownloadUpdateInfo updateInfo) { 108 | var uri = URI.create(updateInfo.asset().downloadUrl); 109 | var destination = workDir.resolve(updateInfo.asset().name); 110 | var digest = updateInfo.asset().digest().startsWith("sha256:") 111 | ? HexFormat.of().withLowerCase().parseHex(updateInfo.asset().digest.substring(7)) // remove "sha256:" prefix 112 | : null; 113 | var size = updateInfo.asset().size; 114 | super(uri, destination, digest, size); 115 | this.workDir = workDir; 116 | this.updateInfo = updateInfo; 117 | } 118 | 119 | @Override 120 | public @Nullable UpdateStep nextStep() throws IllegalStateException, IOException { 121 | if (!isDone()) { 122 | throw new IllegalStateException("Download not yet completed."); 123 | } else if (downloadException != null) { 124 | throw new UpdateFailedException("Download failed.", downloadException); 125 | } 126 | return secondStep(workDir, destination, updateInfo); 127 | } 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/update/DownloadUpdateStep.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.update; 2 | 3 | import org.cryptomator.integrations.Localization; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.io.FilterInputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.InterruptedIOException; 10 | import java.net.URI; 11 | import java.net.http.HttpClient; 12 | import java.net.http.HttpRequest; 13 | import java.net.http.HttpResponse; 14 | import java.nio.channels.Channels; 15 | import java.nio.channels.FileChannel; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.nio.file.StandardOpenOption; 19 | import java.security.MessageDigest; 20 | import java.security.NoSuchAlgorithmException; 21 | import java.time.Duration; 22 | import java.util.concurrent.CountDownLatch; 23 | import java.util.concurrent.TimeUnit; 24 | import java.util.concurrent.atomic.AtomicLong; 25 | import java.util.concurrent.atomic.LongAdder; 26 | 27 | public abstract class DownloadUpdateStep implements UpdateStep { 28 | 29 | protected final URI source; 30 | protected final Path destination; 31 | private final byte[] checksum; 32 | private final AtomicLong totalBytes; 33 | private final LongAdder loadedBytes = new LongAdder(); 34 | private final Thread downloadThread; 35 | private final CountDownLatch downloadCompleted = new CountDownLatch(1); 36 | protected volatile IOException downloadException; 37 | 38 | /** 39 | * Creates a new DownloadUpdateProcess instance. 40 | * @param source The URI from which the update will be downloaded. 41 | * @param destination The path where to save the downloaded file. 42 | * @param checksum (optional) The expected SHA-256 checksum of the downloaded file, can be null if not required. 43 | * @param estDownloadSize The estimated size of the download in bytes. 44 | */ 45 | protected DownloadUpdateStep(URI source, Path destination, @Nullable byte[] checksum, long estDownloadSize) { 46 | this.source = source; 47 | this.destination = destination; 48 | this.checksum = checksum; 49 | this.totalBytes = new AtomicLong(estDownloadSize); 50 | this.downloadThread = Thread.ofVirtual().unstarted(this::download); 51 | } 52 | 53 | @Override 54 | public String description() { 55 | return switch (downloadThread.getState()) { 56 | case NEW -> Localization.get().getString("org.cryptomator.api.update.download.new"); 57 | case TERMINATED -> Localization.get().getString("org.cryptomator.api.update.download.done"); 58 | default -> { 59 | double progress = preparationProgress(); 60 | if (progress < 0.0) { 61 | yield Localization.get().getString("org.cryptomator.api.update.download.indeterminateProgress"); 62 | } else { 63 | yield Localization.get().getString("org.cryptomator.api.update.download.progress").formatted(progress * 100.0); 64 | } 65 | } 66 | }; 67 | } 68 | 69 | @Override 70 | public void start() { 71 | downloadThread.start(); 72 | } 73 | 74 | @Override 75 | public double preparationProgress() { 76 | long total = totalBytes.get(); 77 | if (total <= 0) { 78 | return -1.0; 79 | } else { 80 | return (double) loadedBytes.sum() / totalBytes.get(); 81 | } 82 | } 83 | 84 | @Override 85 | public void await() throws InterruptedException { 86 | downloadCompleted.await(); 87 | } 88 | 89 | @Override 90 | public boolean await(long timeout, TimeUnit unit) throws InterruptedException { 91 | return downloadCompleted.await(timeout, unit); 92 | } 93 | 94 | @Override 95 | public void cancel() { 96 | downloadThread.interrupt(); 97 | try { 98 | Files.deleteIfExists(destination); 99 | } catch (IOException e) { 100 | // ignore, this is a best-effort cleanup 101 | } 102 | } 103 | 104 | protected void download() { 105 | var request = HttpRequest.newBuilder().uri(source).GET().build(); 106 | try (HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).connectTimeout(Duration.ofSeconds(10)).build()) { 107 | downloadInternal(client, request); 108 | } catch (IOException e) { 109 | downloadException = e; 110 | } finally { 111 | downloadCompleted.countDown(); 112 | } 113 | } 114 | 115 | /** 116 | * Downloads the update from the given URI and saves it to the specified filename in the working directory. 117 | * @param client the HttpClient to use for the download 118 | * @param request the HttpRequest which downloads the file 119 | * @throws IOException indicating I/O errors during the download or file writing process or due to checksum mismatch 120 | */ 121 | protected void downloadInternal(HttpClient client, HttpRequest request) throws IOException { 122 | try { 123 | // make download request 124 | var response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); 125 | if (response.statusCode() != 200) { 126 | throw new IOException("Failed to download update, status code: " + response.statusCode()); 127 | } 128 | 129 | // update totalBytes 130 | response.headers().firstValueAsLong("Content-Length").ifPresent(totalBytes::set); 131 | 132 | // prepare checksum calculation 133 | MessageDigest sha256; 134 | try { 135 | sha256 = MessageDigest.getInstance("SHA-256"); 136 | } catch (NoSuchAlgorithmException e) { 137 | throw new AssertionError("Every implementation of the Java platform is required to support [...] SHA-256", e); 138 | } 139 | 140 | // write bytes to file 141 | try (var in = new DownloadInputStream(response.body(), loadedBytes, sha256); 142 | var src = Channels.newChannel(in); 143 | var dst = FileChannel.open(destination, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) { 144 | dst.transferFrom(src, 0, Long.MAX_VALUE); 145 | } 146 | 147 | // verify checksum if provided 148 | byte[] calculatedChecksum = sha256.digest(); 149 | if (checksum != null && !MessageDigest.isEqual(calculatedChecksum, checksum)) { 150 | throw new IOException("Checksum verification failed for downloaded file: " + destination); 151 | } 152 | } catch (InterruptedException e) { 153 | throw new InterruptedIOException("Download interrupted"); 154 | } 155 | } 156 | 157 | /** 158 | * An InputStream decorator that counts the number of bytes read and updates a MessageDigest for checksum calculation. 159 | */ 160 | private static class DownloadInputStream extends FilterInputStream { 161 | 162 | private final LongAdder counter; 163 | private final MessageDigest digest; 164 | 165 | protected DownloadInputStream(InputStream in, LongAdder counter, MessageDigest digest) { 166 | super(in); 167 | this.counter = counter; 168 | this.digest = digest; 169 | } 170 | 171 | @Override 172 | public int read(byte[] b, int off, int len) throws IOException { 173 | int n = super.read(b, off, len); 174 | if (n != -1) { 175 | digest.update(b, off, n); 176 | counter.add(n); 177 | } 178 | return n; 179 | } 180 | 181 | @Override 182 | public int read() throws IOException { 183 | int b = super.read(); 184 | if (b != -1) { 185 | digest.update((byte) b); 186 | counter.increment(); 187 | } 188 | return b; 189 | } 190 | 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | org.cryptomator 7 | integrations-api 8 | 1.8.0-SNAPSHOT 9 | 10 | Cryptomator Integrations API 11 | Defines optional service interfaces that may be used by Cryptomator 12 | https://github.com/cryptomator/integrations-api 13 | 14 | scm:git:git@github.com:cryptomator/integrations-api.git 15 | scm:git:git@github.com:cryptomator/integrations-api.git 16 | git@github.com:cryptomator/integrations-api.git 17 | 18 | 19 | 20 | Sebastian Stenzel 21 | sebastian.stenzel@skymatic.de 22 | +1 23 | Skymatic GmbH 24 | http://skymatic.de 25 | 26 | 27 | 28 | 29 | UTF-8 30 | 25 31 | 32 | 2.0.17 33 | 2.20.0 34 | 26.0.2-1 35 | 36 | 37 | 6.0.0 38 | 5.20.0 39 | 40 | 41 | 3.14.1 42 | 3.3.1 43 | 3.5.4 44 | 3.12.0 45 | 3.2.8 46 | 0.9.0 47 | 48 | 49 | 50 | 51 | GNU Affero General Public License (AGPL) version 3.0 52 | https://www.gnu.org/licenses/agpl.txt 53 | repo 54 | 55 | 56 | 57 | 58 | 59 | org.slf4j 60 | slf4j-api 61 | ${slf4j.version} 62 | 63 | 64 | com.fasterxml.jackson.core 65 | jackson-databind 66 | ${jackson.version} 67 | 68 | 69 | 70 | org.jetbrains 71 | annotations 72 | ${jetbrains-annotation.version} 73 | provided 74 | 75 | 76 | org.slf4j 77 | slf4j-simple 78 | ${slf4j.version} 79 | test 80 | 81 | 82 | org.junit.jupiter 83 | junit-jupiter 84 | ${junit.version} 85 | test 86 | 87 | 88 | org.mockito 89 | mockito-core 90 | ${mockito.version} 91 | test 92 | 93 | 94 | 95 | 96 | 97 | 98 | org.apache.maven.plugins 99 | maven-compiler-plugin 100 | ${mvn-compiler.version} 101 | 102 | ${jdk.version} 103 | 104 | 105 | 106 | maven-source-plugin 107 | ${mvn-source.version} 108 | 109 | 110 | attach-sources 111 | 112 | jar-no-fork 113 | 114 | 115 | 116 | 117 | 118 | org.apache.maven.plugins 119 | maven-surefire-plugin 120 | ${mvn-surefire.version} 121 | 122 | 123 | maven-javadoc-plugin 124 | ${mvn-javadoc.version} 125 | 126 | 127 | attach-javadocs 128 | 129 | jar 130 | 131 | 132 | 133 | 134 | true 135 | 25 136 | 137 | 138 | 139 | apiNote 140 | a 141 | API Note: 142 | 143 | 144 | implSpec 145 | a 146 | Implementation Requirements: 147 | 148 | 149 | implNote 150 | a 151 | Implementation Note: 152 | 153 | 154 | param 155 | 156 | 157 | return 158 | 159 | 160 | throws 161 | 162 | 163 | since 164 | 165 | 166 | version 167 | 168 | 169 | serialData 170 | 171 | 172 | see 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | sign 183 | 184 | 185 | 186 | maven-gpg-plugin 187 | ${mvn-gpg.version} 188 | 189 | 190 | sign-artifacts 191 | verify 192 | 193 | sign 194 | 195 | 196 | bc 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | deploy-central 207 | 208 | 209 | 210 | org.sonatype.central 211 | central-publishing-maven-plugin 212 | ${central-publishing.version} 213 | true 214 | 215 | central 216 | true 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | deploy-github 225 | 226 | 227 | github 228 | GitHub Packages 229 | https://maven.pkg.github.com/cryptomator/integrations-api 230 | 231 | 232 | 233 | 234 | 235 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.common; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | import org.jetbrains.annotations.VisibleForTesting; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.lang.reflect.Method; 10 | import java.lang.reflect.Modifier; 11 | import java.util.Arrays; 12 | import java.util.Comparator; 13 | import java.util.Objects; 14 | import java.util.Optional; 15 | import java.util.ServiceConfigurationError; 16 | import java.util.ServiceLoader; 17 | import java.util.stream.Stream; 18 | 19 | public class IntegrationsLoader { 20 | 21 | private static final Logger LOG = LoggerFactory.getLogger(IntegrationsLoader.class); 22 | 23 | private IntegrationsLoader() { 24 | } 25 | 26 | /** 27 | * Loads the best suited service provider, i.e. the one with the highest priority that is supported. 28 | *

29 | * If two services are available with the same priority, it is unspecified which one will be returned. 30 | * 31 | * @param clazz Service class 32 | * @param Type of the service 33 | * @return Highest priority service provider or empty if no supported service provider was found 34 | */ 35 | public static Optional load(Class clazz) { 36 | return loadAll(clazz).findFirst(); 37 | } 38 | 39 | /** 40 | * Loads a specific service provider by its implementation class name. 41 | * @param clazz Service class 42 | * @param implementationClassName fully qualified class name of the implementation 43 | * @return Optional of the service provider if found 44 | * @param Type of the service 45 | */ 46 | public static Optional loadSpecific(Class clazz, String implementationClassName) { 47 | return ServiceLoader.load(clazz, ClassLoaderFactory.forPluginDir()).stream() 48 | .filter(provider -> provider.type().getName().equals(implementationClassName)) 49 | .map(ServiceLoader.Provider::get) 50 | .findAny(); 51 | } 52 | 53 | /** 54 | * Loads all suited service providers ordered by priority in descending order. 55 | *

56 | * Only services declared in the `org.cryptomator.integrations.api` module can be loaded with this method. 57 | * Foreign services need to use {@link IntegrationsLoader#loadAll(ServiceLoader, Class)}. 58 | * 59 | * @param clazz Service class 60 | * @param Type of the service 61 | * @return An ordered stream of all suited service providers 62 | */ 63 | public static Stream loadAll(Class clazz) { 64 | return loadAll(ServiceLoader.load(clazz, ClassLoaderFactory.forPluginDir()), clazz); 65 | } 66 | 67 | /** 68 | * Loads all suited service providers ordered by priority in descending order. 69 | *

70 | * This method allows arbitrary services to be loaded, as long as the provided service loader has module access to them. 71 | * 72 | * @param serviceLoader Loader with own module scope 73 | * @param clazz Service class 74 | * @param Type of the service 75 | * @return An ordered stream of all suited service providers 76 | */ 77 | public static Stream loadAll(ServiceLoader serviceLoader, @NotNull Class clazz) { 78 | Objects.requireNonNull(clazz, "Service to load not specified."); 79 | return serviceLoader.stream() 80 | .peek(serviceProvider -> logFoundServiceProvider(clazz, serviceProvider.type())) 81 | .filter(IntegrationsLoader::isSupportedOperatingSystem) 82 | .filter(IntegrationsLoader::passesStaticAvailabilityCheck) 83 | .sorted(Comparator.comparingInt(IntegrationsLoader::getPriority).reversed()) 84 | .flatMap(IntegrationsLoader::instantiateServiceProvider) 85 | .filter(IntegrationsLoader::passesInstanceAvailabilityCheck) 86 | .peek(impl -> logServiceIsAvailable(clazz, impl.getClass())); 87 | } 88 | 89 | private static void logFoundServiceProvider(Class apiType, Class implType) { 90 | if (LOG.isDebugEnabled()) { 91 | LOG.debug("{}: Found implementation: {} in jar {}", apiType.getSimpleName(), implType.getName(), implType.getProtectionDomain().getCodeSource().getLocation().getPath()); 92 | } 93 | } 94 | 95 | private static int getPriority(ServiceLoader.Provider provider) { 96 | var prio = provider.type().getAnnotation(Priority.class); 97 | return prio == null ? Priority.DEFAULT : prio.value(); 98 | } 99 | 100 | private static boolean isSupportedOperatingSystem(ServiceLoader.Provider provider) { 101 | var annotations = provider.type().getAnnotationsByType(OperatingSystem.class); 102 | return annotations.length == 0 || Arrays.stream(annotations).anyMatch(OperatingSystem.Value::isCurrent); 103 | } 104 | 105 | private static Stream instantiateServiceProvider(ServiceLoader.Provider provider) { 106 | try { 107 | return Stream.of(provider.get()); 108 | } catch (ServiceConfigurationError err) { 109 | //ServiceLoader.Provider::get throws this error if (from javadoc) 110 | // * the public static "provider()" method of a provider factory returns null 111 | // * the service provider cannot be instantiated due to an error/throw 112 | LOG.warn("Unable to load service provider {}.", provider.type().getName(), err); 113 | return Stream.empty(); 114 | } 115 | } 116 | 117 | private static boolean passesStaticAvailabilityCheck(ServiceLoader.Provider provider) { 118 | return passesStaticAvailabilityCheck(provider.type()); 119 | } 120 | 121 | @VisibleForTesting 122 | static boolean passesStaticAvailabilityCheck(Class type) { 123 | return silentlyPassesAvailabilityCheck(type, null); 124 | } 125 | 126 | @VisibleForTesting 127 | static boolean passesInstanceAvailabilityCheck(Object instance) { 128 | return silentlyPassesAvailabilityCheck(instance.getClass(), instance); 129 | } 130 | 131 | private static void logServiceIsAvailable(Class apiType, Class implType) { 132 | if (LOG.isDebugEnabled()) { 133 | LOG.debug("{}: Implementation is available: {}", apiType.getSimpleName(), implType.getName()); 134 | } 135 | } 136 | 137 | private static boolean silentlyPassesAvailabilityCheck(Class type, @Nullable T instance) { 138 | try { 139 | return passesAvailabilityCheck(type, instance); 140 | } catch (ExceptionInInitializerError | NoClassDefFoundError | RuntimeException e) { 141 | LOG.warn("Unable to load service provider {}.", type.getName(), e); 142 | return false; 143 | } 144 | } 145 | 146 | private static boolean passesAvailabilityCheck(Class type, @Nullable T instance) { 147 | if (!type.isAnnotationPresent(CheckAvailability.class)) { 148 | return true; // if type is not annotated, skip tests 149 | } 150 | if (!type.getModule().isExported(type.getPackageName(), IntegrationsLoader.class.getModule())) { 151 | LOG.error("Can't run @CheckAvailability tests for class {}. Make sure to export {} to {}!", type.getName(), type.getPackageName(), IntegrationsLoader.class.getPackageName()); 152 | return false; 153 | } 154 | return Arrays.stream(type.getMethods()) 155 | .filter(m -> isAvailabilityCheck(m, instance == null)) 156 | .allMatch(m -> passesAvailabilityCheck(m, instance)); 157 | } 158 | 159 | private static boolean passesAvailabilityCheck(Method m, @Nullable Object instance) { 160 | assert Boolean.TYPE.equals(m.getReturnType()); 161 | try { 162 | return (boolean) m.invoke(instance); 163 | } catch (ReflectiveOperationException e) { 164 | LOG.warn("Failed to invoke @CheckAvailability test {}#{}", m.getDeclaringClass(), m.getName(), e); 165 | return false; 166 | } 167 | } 168 | 169 | private static boolean isAvailabilityCheck(Method m, boolean isStatic) { 170 | return m.isAnnotationPresent(CheckAvailability.class) 171 | && Boolean.TYPE.equals(m.getReturnType()) 172 | && m.getParameterCount() == 0 173 | && Modifier.isStatic(m.getModifiers()) == isStatic; 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/integrations/common/IntegrationsLoaderTest.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.integrations.common; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Nested; 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class IntegrationsLoaderTest { 9 | 10 | @Nested 11 | @DisplayName("@CheckAvailability on static methods") 12 | public class StaticAvailabilityChecks { 13 | 14 | @CheckAvailability 15 | private static class StaticTrue { 16 | @CheckAvailability 17 | public static boolean test() { 18 | return true; 19 | } 20 | } 21 | 22 | @CheckAvailability 23 | private static class StaticFalse { 24 | @CheckAvailability 25 | public static boolean test() { 26 | return false; 27 | } 28 | } 29 | 30 | @Test 31 | @DisplayName("no @CheckAvailability will always pass") 32 | public void testPassesAvailabilityCheck0() { 33 | // @formatter:off 34 | class C1 {} 35 | @CheckAvailability class C2 {} 36 | class C3 { 37 | @CheckAvailability public static boolean test() { return false; } 38 | } 39 | // @formatter:on 40 | 41 | Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class)); 42 | Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C2.class)); 43 | Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C3.class)); 44 | } 45 | 46 | @Test 47 | @DisplayName("@CheckAvailability on non-conforming methods will be skipped") 48 | public void testPassesAvailabilityCheck1() { 49 | // @formatter:off 50 | @CheckAvailability class C1 { 51 | @CheckAvailability private static boolean test1() { return false; } 52 | @CheckAvailability public static boolean test2(String foo) { return false; } 53 | @CheckAvailability public static String test3() { return "false"; } 54 | } 55 | // @formatter:on 56 | 57 | Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class)); 58 | } 59 | 60 | @Test 61 | @DisplayName("@CheckAvailability on static method") 62 | public void testPassesAvailabilityCheck2() { 63 | Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(StaticTrue.class)); 64 | Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(StaticFalse.class)); 65 | } 66 | 67 | @Test 68 | @DisplayName("@CheckAvailability on inherited static method") 69 | public void testPassesAvailabilityCheck3() { 70 | // @formatter:off 71 | class C1 extends StaticTrue {} 72 | class C2 extends StaticFalse {} 73 | // @formatter:on 74 | 75 | Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class)); 76 | Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C2.class)); 77 | } 78 | 79 | @Test 80 | @DisplayName("multiple @CheckAvailability methods") 81 | public void testPassesAvailabilityCheck4() { 82 | // @formatter:off 83 | class C1 extends StaticTrue { 84 | @CheckAvailability public static boolean test1() { return false; } 85 | } 86 | class C2 extends StaticFalse { 87 | @CheckAvailability public static boolean test1() { return true; } 88 | } 89 | @CheckAvailability class C3 { 90 | @CheckAvailability public static boolean test1() { return true; } 91 | @CheckAvailability public static boolean test2() { return false; } 92 | } 93 | // @formatter:on 94 | 95 | Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class)); 96 | Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C2.class)); 97 | Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C3.class)); 98 | } 99 | 100 | @Test 101 | @DisplayName("throwing @CheckAvailability methods are treated as false") 102 | public void testPassesAvailabilityCheckThrowing() { 103 | 104 | @CheckAvailability class C1 { 105 | @CheckAvailability public static boolean test() { throw new RuntimeException("FAIL"); } 106 | } 107 | 108 | Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class)); 109 | Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(InitExceptionTestClass.class)); 110 | Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(InitExceptionTestClass.class)); //NoClassDefFoundError due to repated call 111 | } 112 | 113 | } 114 | 115 | @Nested 116 | @DisplayName("@CheckAvailability on instance methods") 117 | public class InstanceAvailabilityChecks { 118 | 119 | @CheckAvailability 120 | private static class InstanceTrue { 121 | @CheckAvailability 122 | public boolean test() { 123 | return true; 124 | } 125 | } 126 | 127 | @CheckAvailability 128 | private static class InstanceFalse { 129 | @CheckAvailability 130 | public boolean test() { 131 | return false; 132 | } 133 | } 134 | 135 | @Test 136 | @DisplayName("no @CheckAvailability will always pass") 137 | public void testPassesAvailabilityCheck0() { 138 | // @formatter:off 139 | class C1 {} 140 | @CheckAvailability class C2 {} 141 | class C3 { 142 | @CheckAvailability public boolean test() { return false; } 143 | } 144 | // @formatter:on 145 | 146 | Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new C1())); 147 | Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new C2())); 148 | Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new C3())); 149 | } 150 | 151 | @Test 152 | @DisplayName("@CheckAvailability on non-conforming instance methods will be skipped") 153 | public void testPassesAvailabilityCheck1() { 154 | // @formatter:off 155 | @CheckAvailability class C1 { 156 | @CheckAvailability private boolean test1() { return false; } 157 | @CheckAvailability public boolean test2(String foo) { return false; } 158 | @CheckAvailability public String test3() { return "false"; } 159 | } 160 | // @formatter:on 161 | 162 | Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(C1.class)); 163 | } 164 | 165 | @Test 166 | @DisplayName("@CheckAvailability on instance method") 167 | public void testPassesAvailabilityCheck2() { 168 | Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new InstanceTrue())); 169 | Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new InstanceFalse())); 170 | } 171 | 172 | @Test 173 | @DisplayName("@CheckAvailability on inherited instance method") 174 | public void testPassesAvailabilityCheck3() { 175 | // @formatter:off 176 | class C1 extends InstanceTrue {} 177 | class C2 extends InstanceFalse {} 178 | // @formatter:on 179 | 180 | Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new C1())); 181 | Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C2())); 182 | } 183 | 184 | @Test 185 | @DisplayName("multiple @CheckAvailability methods") 186 | public void testPassesAvailabilityCheck4() { 187 | // @formatter:off 188 | class C1 extends InstanceTrue { 189 | @CheckAvailability public boolean test1() { return false; } 190 | } 191 | class C2 extends InstanceFalse { 192 | @CheckAvailability public boolean test1() { return true; } 193 | } 194 | @CheckAvailability class C3 { 195 | @CheckAvailability public boolean test1() { return true; } 196 | @CheckAvailability public boolean test2() { return false; } 197 | } 198 | // @formatter:on 199 | 200 | Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C1())); 201 | Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C2())); 202 | Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C3())); 203 | } 204 | 205 | 206 | @Test 207 | @DisplayName("throwing @CheckAvailability methods are treated as false") 208 | public void testPassesAvailabilityCheckThrowing() { 209 | 210 | @CheckAvailability 211 | class C1 { 212 | @CheckAvailability public boolean test1() { throw new RuntimeException("FAIL"); } 213 | } 214 | 215 | @CheckAvailability 216 | class C2 { 217 | @CheckAvailability public boolean test1() { return true; } 218 | @CheckAvailability public boolean test2() { throw new RuntimeException("FAIL"); } 219 | } 220 | 221 | Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C1())); 222 | Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C2())); 223 | } 224 | 225 | } 226 | 227 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------