diff --git a/pom.xml b/pom.xml index 6b76ffe..e91c46d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.cryptomator integrations-linux - 1.5.1 + 1.5.2 integrations-linux Provides optional Linux services used by Cryptomator @@ -47,17 +47,17 @@ 1.4.1 - 5.11.3 + 5.11.4 3.13.0 - 3.5.1 + 3.5.2 3.5.0 3.3.1 - 3.10.1 + 3.11.2 3.2.7 3.1.3 - 11.1.0 + 11.1.1 1.7.0 diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index 16bdbba..6570d88 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -15,15 +15,10 @@ import javax.xml.validation.Validator; import java.io.IOException; import java.io.StringReader; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; import java.util.List; import java.util.UUID; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; /** * Implemenation of the {@link QuickAccessService} for KDE desktop environments using Dolphin file browser. @@ -32,12 +27,10 @@ @CheckAvailability @OperatingSystem(OperatingSystem.Value.LINUX) @Priority(90) -public class DolphinPlaces implements QuickAccessService { +public class DolphinPlaces extends FileConfiguredQuickAccess implements QuickAccessService { - private static final int MAX_FILE_SIZE = 1 << 15; //xml is quite verbose + private static final int MAX_FILE_SIZE = 1 << 20; //1MiB, xml is quite verbose private static final Path PLACES_FILE = Path.of(System.getProperty("user.home"), ".local/share/user-places.xbel"); - private static final Path TMP_FILE = Path.of(System.getProperty("java.io.tmpdir"), "user-places.xbel.cryptomator.tmp"); - private static final Lock MODIFY_LOCK = new ReentrantLock(); private static final String ENTRY_TEMPLATE = """ %s @@ -51,7 +44,6 @@ public class DolphinPlaces implements QuickAccessService { """; - private static final Validator XML_VALIDATOR; static { @@ -64,96 +56,82 @@ public class DolphinPlaces implements QuickAccessService { } } + //SPI constructor + public DolphinPlaces() { + super(PLACES_FILE, MAX_FILE_SIZE); + } @Override - public QuickAccessService.QuickAccessEntry add(Path target, String displayName) throws QuickAccessServiceException { - String id = UUID.randomUUID().toString(); + EntryAndConfig addEntryToConfig(String config, Path target, String displayName) throws QuickAccessServiceException { try { - MODIFY_LOCK.lock(); - if (Files.size(PLACES_FILE) > MAX_FILE_SIZE) { - throw new IOException("File %s exceeds size of %d bytes".formatted(PLACES_FILE, MAX_FILE_SIZE)); - } - var placesContent = Files.readString(PLACES_FILE); + String id = UUID.randomUUID().toString(); //validate - XML_VALIDATOR.validate(new StreamSource(new StringReader(placesContent))); + XML_VALIDATOR.validate(new StreamSource(new StringReader(config))); // modify - int insertIndex = placesContent.lastIndexOf("",">"); + } + + private class DolphinPlacesEntry extends FileConfiguredQuickAccessEntry implements QuickAccessEntry { private final String id; - private volatile boolean isRemoved = false; DolphinPlacesEntry(String id) { this.id = id; } @Override - public void remove() throws QuickAccessServiceException { + public String removeEntryFromConfig(String config) throws QuickAccessServiceException { try { - MODIFY_LOCK.lock(); - if (isRemoved) { - return; - } - if (Files.size(PLACES_FILE) > MAX_FILE_SIZE) { - throw new IOException("File %s exceeds size of %d bytes".formatted(PLACES_FILE, MAX_FILE_SIZE)); - } - var placesContent = Files.readString(PLACES_FILE); - int idIndex = placesContent.lastIndexOf(id); + int idIndex = config.lastIndexOf(id); if (idIndex == -1) { - isRemoved = true; - return; //we assume someone has removed our entry + return config; //assume someone has removed our entry, nothing to do } //validate - XML_VALIDATOR.validate(new StreamSource(new StringReader(placesContent))); + XML_VALIDATOR.validate(new StreamSource(new StringReader(config))); //modify - int openingTagIndex = indexOfEntryOpeningTag(placesContent, idIndex); - var contentToWrite1 = placesContent.substring(0, openingTagIndex).stripTrailing(); + int openingTagIndex = indexOfEntryOpeningTag(config, idIndex); + var contentToWrite1 = config.substring(0, openingTagIndex).stripTrailing(); - int closingTagEndIndex = placesContent.indexOf('>', placesContent.indexOf("', config.indexOf(" tag."); } } diff --git a/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java b/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java new file mode 100644 index 0000000..70ed6c1 --- /dev/null +++ b/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java @@ -0,0 +1,106 @@ +package org.cryptomator.linux.quickaccess; + +import org.cryptomator.integrations.quickaccess.QuickAccessService; +import org.cryptomator.integrations.quickaccess.QuickAccessServiceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +abstract class FileConfiguredQuickAccess implements QuickAccessService { + + private static final Logger LOG = LoggerFactory.getLogger(FileConfiguredQuickAccess.class); + + private final int maxFileSize; + private final Path configFile; + private final Path tmpFile; + private final Lock modifyLock = new ReentrantReadWriteLock().writeLock(); + + FileConfiguredQuickAccess(Path configFile, int maxFileSize) { + this.configFile = configFile; + this.maxFileSize = maxFileSize; + this.tmpFile = configFile.resolveSibling("." + configFile.getFileName() + ".cryptomator.tmp"); + Runtime.getRuntime().addShutdownHook(new Thread(this::cleanup)); + } + + @Override + public QuickAccessEntry add(Path target, String displayName) throws QuickAccessServiceException { + try { + modifyLock.lock(); + var entryAndConfig = addEntryToConfig(readConfig(), target, displayName); + persistConfig(entryAndConfig.config()); + return entryAndConfig.entry(); + } catch (IOException e) { + throw new QuickAccessServiceException("Failed to add entry to %s.".formatted(configFile), e); + } finally { + modifyLock.unlock(); + } + } + + record EntryAndConfig(FileConfiguredQuickAccessEntry entry, String config) { + } + + abstract EntryAndConfig addEntryToConfig(String config, Path target, String displayName) throws QuickAccessServiceException; + + + protected abstract class FileConfiguredQuickAccessEntry implements QuickAccessEntry { + + private volatile boolean isRemoved = false; + + @Override + public void remove() throws QuickAccessServiceException { + try { + modifyLock.lock(); + if (isRemoved) { + return; + } + checkFileSize(); + var config = readConfig(); + var adjustedConfig = removeEntryFromConfig(config); + persistConfig(adjustedConfig); + isRemoved = true; + } catch (IOException e) { + throw new QuickAccessServiceException("Failed to remove entry to %s.".formatted(configFile), e); + } finally { + modifyLock.unlock(); + } + } + + abstract String removeEntryFromConfig(String config) throws QuickAccessServiceException; + } + + private String readConfig() throws IOException { + return Files.readString(configFile, StandardCharsets.UTF_8); + } + + private void persistConfig(String newConfig) throws IOException { + Files.writeString(tmpFile, newConfig, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + try { + Files.move(tmpFile, configFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(tmpFile, configFile, StandardCopyOption.REPLACE_EXISTING); + } + } + + private void checkFileSize() throws IOException { + if (Files.size(configFile) > maxFileSize) { + throw new IOException("File %s exceeds size of %d bytes".formatted(configFile, maxFileSize)); + } + } + + private void cleanup() { + try { + Files.deleteIfExists(tmpFile); + } catch (IOException e) { + LOG.warn("Unable to delete {}. Need to be deleted manually.", tmpFile); + } + } +} diff --git a/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java b/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java index 1daf5e4..63bd268 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java @@ -7,78 +7,50 @@ import org.cryptomator.integrations.quickaccess.QuickAccessService; import org.cryptomator.integrations.quickaccess.QuickAccessServiceException; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.Objects; +import java.util.stream.Collectors; @Priority(100) @CheckAvailability @OperatingSystem(OperatingSystem.Value.LINUX) @DisplayName("GNOME Nautilus Bookmarks") -public class NautilusBookmarks implements QuickAccessService { +public class NautilusBookmarks extends FileConfiguredQuickAccess implements QuickAccessService { private static final int MAX_FILE_SIZE = 4096; private static final Path BOOKMARKS_FILE = Path.of(System.getProperty("user.home"), ".config/gtk-3.0/bookmarks"); - private static final Path TMP_FILE = BOOKMARKS_FILE.resolveSibling("bookmarks.cryptomator.tmp"); - private static final Lock BOOKMARKS_LOCK = new ReentrantReadWriteLock().writeLock(); + + //SPI constructor + public NautilusBookmarks() { + super(BOOKMARKS_FILE, MAX_FILE_SIZE); + } @Override - public QuickAccessService.QuickAccessEntry add(Path target, String displayName) throws QuickAccessServiceException { + EntryAndConfig addEntryToConfig(String config, Path target, String displayName) throws QuickAccessServiceException { var uriPath = target.toAbsolutePath().toString().replace(" ", "%20"); String entryLine = "file://" + uriPath + " " + displayName; - try { - BOOKMARKS_LOCK.lock(); - if (Files.size(BOOKMARKS_FILE) > MAX_FILE_SIZE) { - throw new IOException("File %s exceeds size of %d bytes".formatted(BOOKMARKS_FILE, MAX_FILE_SIZE)); - } - //by reading all lines, we ensure that each line is terminated with EOL - var entries = Files.readAllLines(BOOKMARKS_FILE, StandardCharsets.UTF_8); - entries.add(entryLine); - Files.write(TMP_FILE, entries, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - Files.move(TMP_FILE, BOOKMARKS_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - return new NautilusQuickAccessEntry(entryLine); - } catch (IOException e) { - throw new QuickAccessServiceException("Adding entry to Nautilus bookmarks file failed.", e); - } finally { - BOOKMARKS_LOCK.unlock(); - } + var entry = new NautilusQuickAccessEntry(entryLine); + var adjustedConfig = config.stripTrailing() + + "\n" + + entryLine; + return new EntryAndConfig(entry, adjustedConfig); } - static class NautilusQuickAccessEntry implements QuickAccessEntry { + class NautilusQuickAccessEntry extends FileConfiguredQuickAccessEntry implements QuickAccessEntry { private final String line; - private volatile boolean isRemoved = false; NautilusQuickAccessEntry(String line) { this.line = line; } @Override - public void remove() throws QuickAccessServiceException { - try { - BOOKMARKS_LOCK.lock(); - if (isRemoved) { - return; - } - if (Files.size(BOOKMARKS_FILE) > MAX_FILE_SIZE) { - throw new IOException("File %s exceeds size of %d bytes".formatted(BOOKMARKS_FILE, MAX_FILE_SIZE)); - } - var entries = Files.readAllLines(BOOKMARKS_FILE); - if (entries.remove(line)) { - Files.write(TMP_FILE, entries, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - Files.move(TMP_FILE, BOOKMARKS_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } - isRemoved = true; - } catch (IOException e) { - throw new QuickAccessServiceException("Removing entry from Nautilus bookmarks file failed", e); - } finally { - BOOKMARKS_LOCK.unlock(); - } + public String removeEntryFromConfig(String config) throws QuickAccessServiceException { + return config.lines() // + .map(l -> l.equals(line) ? null : l) // + .filter(Objects::nonNull) // + .collect(Collectors.joining("\n")); } }