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"));
}
}