From 73c3302ae5f1d6689ebd42a697e0322f1e75f592 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 13 Jan 2025 12:38:38 +0100 Subject: [PATCH 1/5] fixes #97 --- .../linux/quickaccess/DolphinPlaces.java | 13 +++++++++++-- .../linux/quickaccess/NautilusBookmarks.java | 14 ++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index de18b74..e68aadc 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.io.StringReader; 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; @@ -86,7 +87,7 @@ public QuickAccessService.QuickAccessEntry add(Path target, String displayName) writer.write(placesContent, insertIndex, placesContent.length() - insertIndex); } // save - Files.move(TMP_FILE, PLACES_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + persistTmpFile(); return new DolphinPlacesEntry(id); } catch (SAXException | IOException e) { throw new QuickAccessServiceException("Adding entry to KDE places file failed.", e); @@ -136,7 +137,7 @@ public void remove() throws QuickAccessServiceException { writer.write(contentToWrite2); } // save - Files.move(TMP_FILE, PLACES_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + persistTmpFile(); isRemoved = true; } catch (IOException | SAXException e) { throw new QuickAccessServiceException("Removing entry from KDE places file failed.", e); @@ -157,6 +158,14 @@ private int indexOfEntryOpeningTag(String placesContent, int idIndex) { } } + static void persistTmpFile() throws IOException { + try { + Files.move(TMP_FILE, PLACES_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(TMP_FILE, PLACES_FILE, StandardCopyOption.REPLACE_EXISTING); + } + } + @CheckAvailability public static boolean isSupported() { return Files.exists(PLACES_FILE); diff --git a/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java b/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java index 1daf5e4..f88e6a6 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java @@ -9,6 +9,7 @@ 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; @@ -40,7 +41,8 @@ public QuickAccessService.QuickAccessEntry add(Path target, String displayName) 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); + + persistTmpFile(); return new NautilusQuickAccessEntry(entryLine); } catch (IOException e) { throw new QuickAccessServiceException("Adding entry to Nautilus bookmarks file failed.", e); @@ -71,7 +73,7 @@ public void remove() throws QuickAccessServiceException { 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); + persistTmpFile(); } isRemoved = true; } catch (IOException e) { @@ -82,6 +84,14 @@ public void remove() throws QuickAccessServiceException { } } + static void persistTmpFile() throws IOException { + try { + Files.move(TMP_FILE, BOOKMARKS_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(TMP_FILE, BOOKMARKS_FILE, StandardCopyOption.REPLACE_EXISTING); + } + } + @CheckAvailability public static boolean isSupported() { return Files.exists(BOOKMARKS_FILE); From f3abf0482e6fc63f70327e8196f5a52bc7d42f6d Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 13 Jan 2025 18:34:46 +0100 Subject: [PATCH 2/5] implement base class for editing file-based configuration for quickAccessEntries --- .../linux/quickaccess/DolphinPlaces.java | 95 +++++----------- .../FileConfiguredQuickAccess.java | 106 ++++++++++++++++++ .../linux/quickaccess/NautilusBookmarks.java | 78 ++++--------- 3 files changed, 152 insertions(+), 127 deletions(-) create mode 100644 src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index e68aadc..9fb8f94 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -15,16 +15,10 @@ import javax.xml.validation.Validator; import java.io.IOException; import java.io.StringReader; -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.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. @@ -33,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 << 20; //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 @@ -52,7 +44,6 @@ public class DolphinPlaces implements QuickAccessService { """; - private static final Validator XML_VALIDATOR; static { @@ -65,84 +56,58 @@ 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(" 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(" 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 f88e6a6..923ff53 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java @@ -7,88 +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.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; +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); - - persistTmpFile(); - 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); - persistTmpFile(); - } - isRemoved = true; - } catch (IOException e) { - throw new QuickAccessServiceException("Removing entry from Nautilus bookmarks file failed", e); - } finally { - BOOKMARKS_LOCK.unlock(); - } - } - } - - static void persistTmpFile() throws IOException { - try { - Files.move(TMP_FILE, BOOKMARKS_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } catch (AtomicMoveNotSupportedException e) { - Files.move(TMP_FILE, BOOKMARKS_FILE, StandardCopyOption.REPLACE_EXISTING); + public String removeEntryFromConfig(String config) throws QuickAccessServiceException { + return config.lines() // + .map(l -> l.equals(line) ? null : l) // + .filter(Objects::nonNull) // + .collect(Collectors.joining("\n")); } } From 70f1c256f7bd4d6ff23935ab9505a3f7a65db835 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 13 Jan 2025 19:05:42 +0100 Subject: [PATCH 3/5] fix errors --- .../linux/quickaccess/FileConfiguredQuickAccess.java | 4 ++-- .../org/cryptomator/linux/quickaccess/NautilusBookmarks.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java b/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java index 6669949..70ed6c1 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java @@ -27,7 +27,7 @@ abstract class FileConfiguredQuickAccess implements QuickAccessService { FileConfiguredQuickAccess(Path configFile, int maxFileSize) { this.configFile = configFile; this.maxFileSize = maxFileSize; - this.tmpFile = configFile.resolve("." + configFile.getFileName() + ".cryptomator.tmp"); + this.tmpFile = configFile.resolveSibling("." + configFile.getFileName() + ".cryptomator.tmp"); Runtime.getRuntime().addShutdownHook(new Thread(this::cleanup)); } @@ -78,7 +78,7 @@ public void remove() throws QuickAccessServiceException { } private String readConfig() throws IOException { - return Files.readString(tmpFile, StandardCharsets.UTF_8); + return Files.readString(configFile, StandardCharsets.UTF_8); } private void persistConfig(String newConfig) throws IOException { diff --git a/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java b/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java index 923ff53..63bd268 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java @@ -32,7 +32,7 @@ EntryAndConfig addEntryToConfig(String config, Path target, String displayName) String entryLine = "file://" + uriPath + " " + displayName; var entry = new NautilusQuickAccessEntry(entryLine); var adjustedConfig = config.stripTrailing() + - "/n" + + "\n" + entryLine; return new EntryAndConfig(entry, adjustedConfig); } From a76717ba9634121f1f06940f88d5c61299b2427f Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Tue, 14 Jan 2025 10:37:11 +0100 Subject: [PATCH 4/5] improve doc and catch possible exception --- .../cryptomator/linux/quickaccess/DolphinPlaces.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index 9fb8f94..5b0bcfa 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -106,20 +106,26 @@ public String removeEntryFromConfig(String config) throws QuickAccessServiceExce var contentToWrite2 = part2Tmp[part2Tmp.length - 1]; return contentToWrite1 + "\n" + contentToWrite2; - } catch (IOException | SAXException e) { + } catch (IOException | SAXException | IllegalStateException e) { throw new QuickAccessServiceException("Removing entry from KDE places file failed.", e); } } + /** + * Returns the start index (inclusive) of the {@link DolphinPlaces#ENTRY_TEMPLATE} entry + * @param placesContent the content of the XBEL places file + * @param idIndex start index (inclusive) of the entrys id tag value + * @return start index of the first bookmark tag, searching backwards from idIndex + */ private int indexOfEntryOpeningTag(String placesContent, int idIndex) { var xmlWhitespaceChars = List.of(' ', '\t', '\n'); for (char c : xmlWhitespaceChars) { - int idx = placesContent.lastIndexOf(" tag."); } } From 62236f4ea878d72fc3dee5647e7758db8f427f71 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Tue, 14 Jan 2025 15:33:42 +0100 Subject: [PATCH 5/5] escape special xml characters --- .../org/cryptomator/linux/quickaccess/DolphinPlaces.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index 5b0bcfa..6570d88 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -71,7 +71,7 @@ EntryAndConfig addEntryToConfig(String config, Path target, String displayName) int insertIndex = config.lastIndexOf("",">"); + } + private class DolphinPlacesEntry extends FileConfiguredQuickAccessEntry implements QuickAccessEntry { private final String id;