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 | *
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 extends T> 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 extends T> 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 |
--------------------------------------------------------------------------------