Skip to content

Commit 51f1d63

Browse files
authored
Merge pull request #102 from cryptomator/feature/common-quick-access-base
Refactoring Common quick access base
2 parents c71e5f4 + 62236f4 commit 51f1d63

File tree

3 files changed

+167
-111
lines changed

3 files changed

+167
-111
lines changed

src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java

Lines changed: 41 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,10 @@
1515
import javax.xml.validation.Validator;
1616
import java.io.IOException;
1717
import java.io.StringReader;
18-
import java.nio.charset.StandardCharsets;
1918
import java.nio.file.Files;
2019
import java.nio.file.Path;
21-
import java.nio.file.StandardCopyOption;
22-
import java.nio.file.StandardOpenOption;
2320
import java.util.List;
2421
import java.util.UUID;
25-
import java.util.concurrent.locks.Lock;
26-
import java.util.concurrent.locks.ReentrantLock;
2722

2823
/**
2924
* Implemenation of the {@link QuickAccessService} for KDE desktop environments using Dolphin file browser.
@@ -32,12 +27,10 @@
3227
@CheckAvailability
3328
@OperatingSystem(OperatingSystem.Value.LINUX)
3429
@Priority(90)
35-
public class DolphinPlaces implements QuickAccessService {
30+
public class DolphinPlaces extends FileConfiguredQuickAccess implements QuickAccessService {
3631

37-
private static final int MAX_FILE_SIZE = 1 << 20; //xml is quite verbose
32+
private static final int MAX_FILE_SIZE = 1 << 20; //1MiB, xml is quite verbose
3833
private static final Path PLACES_FILE = Path.of(System.getProperty("user.home"), ".local/share/user-places.xbel");
39-
private static final Path TMP_FILE = Path.of(System.getProperty("java.io.tmpdir"), "user-places.xbel.cryptomator.tmp");
40-
private static final Lock MODIFY_LOCK = new ReentrantLock();
4134
private static final String ENTRY_TEMPLATE = """
4235
<bookmark href=\"%s\">
4336
<title>%s</title>
@@ -51,7 +44,6 @@ public class DolphinPlaces implements QuickAccessService {
5144
</info>
5245
</bookmark>""";
5346

54-
5547
private static final Validator XML_VALIDATOR;
5648

5749
static {
@@ -64,96 +56,82 @@ public class DolphinPlaces implements QuickAccessService {
6456
}
6557
}
6658

59+
//SPI constructor
60+
public DolphinPlaces() {
61+
super(PLACES_FILE, MAX_FILE_SIZE);
62+
}
6763

6864
@Override
69-
public QuickAccessService.QuickAccessEntry add(Path target, String displayName) throws QuickAccessServiceException {
70-
String id = UUID.randomUUID().toString();
65+
EntryAndConfig addEntryToConfig(String config, Path target, String displayName) throws QuickAccessServiceException {
7166
try {
72-
MODIFY_LOCK.lock();
73-
if (Files.size(PLACES_FILE) > MAX_FILE_SIZE) {
74-
throw new IOException("File %s exceeds size of %d bytes".formatted(PLACES_FILE, MAX_FILE_SIZE));
75-
}
76-
var placesContent = Files.readString(PLACES_FILE);
67+
String id = UUID.randomUUID().toString();
7768
//validate
78-
XML_VALIDATOR.validate(new StreamSource(new StringReader(placesContent)));
69+
XML_VALIDATOR.validate(new StreamSource(new StringReader(config)));
7970
// modify
80-
int insertIndex = placesContent.lastIndexOf("</xbel"); //cannot be -1 due to validation; we do not match the end tag, since betweent tag name and closing bracket can be whitespaces
81-
try (var writer = Files.newBufferedWriter(TMP_FILE, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
82-
writer.write(placesContent, 0, insertIndex);
83-
writer.newLine();
84-
writer.write(ENTRY_TEMPLATE.formatted(target.toUri(), displayName, id).indent(1));
85-
writer.newLine();
86-
writer.write(placesContent, insertIndex, placesContent.length() - insertIndex);
87-
}
88-
// save
89-
Files.move(TMP_FILE, PLACES_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
90-
return new DolphinPlacesEntry(id);
71+
int insertIndex = config.lastIndexOf("</xbel"); //cannot be -1 due to validation; we do not match the whole end tag, since between tag name and closing bracket can be whitespaces
72+
var adjustedConfig = config.substring(0, insertIndex) //
73+
+ "\n" //
74+
+ ENTRY_TEMPLATE.formatted(target.toUri(), escapeXML(displayName), id).indent(1) //
75+
+ "\n" //
76+
+ config.substring(insertIndex);
77+
return new EntryAndConfig(new DolphinPlacesEntry(id), adjustedConfig);
9178
} catch (SAXException | IOException e) {
9279
throw new QuickAccessServiceException("Adding entry to KDE places file failed.", e);
93-
} finally {
94-
MODIFY_LOCK.unlock();
9580
}
9681
}
9782

98-
private static class DolphinPlacesEntry implements QuickAccessEntry {
83+
private String escapeXML(String s) {
84+
return s.replace("&","&amp;") //
85+
.replace("<","&lt;") //
86+
.replace(">","&gt;");
87+
}
88+
89+
private class DolphinPlacesEntry extends FileConfiguredQuickAccessEntry implements QuickAccessEntry {
9990

10091
private final String id;
101-
private volatile boolean isRemoved = false;
10292

10393
DolphinPlacesEntry(String id) {
10494
this.id = id;
10595
}
10696

10797
@Override
108-
public void remove() throws QuickAccessServiceException {
98+
public String removeEntryFromConfig(String config) throws QuickAccessServiceException {
10999
try {
110-
MODIFY_LOCK.lock();
111-
if (isRemoved) {
112-
return;
113-
}
114-
if (Files.size(PLACES_FILE) > MAX_FILE_SIZE) {
115-
throw new IOException("File %s exceeds size of %d bytes".formatted(PLACES_FILE, MAX_FILE_SIZE));
116-
}
117-
var placesContent = Files.readString(PLACES_FILE);
118-
int idIndex = placesContent.lastIndexOf(id);
100+
int idIndex = config.lastIndexOf(id);
119101
if (idIndex == -1) {
120-
isRemoved = true;
121-
return; //we assume someone has removed our entry
102+
return config; //assume someone has removed our entry, nothing to do
122103
}
123104
//validate
124-
XML_VALIDATOR.validate(new StreamSource(new StringReader(placesContent)));
105+
XML_VALIDATOR.validate(new StreamSource(new StringReader(config)));
125106
//modify
126-
int openingTagIndex = indexOfEntryOpeningTag(placesContent, idIndex);
127-
var contentToWrite1 = placesContent.substring(0, openingTagIndex).stripTrailing();
107+
int openingTagIndex = indexOfEntryOpeningTag(config, idIndex);
108+
var contentToWrite1 = config.substring(0, openingTagIndex).stripTrailing();
128109

129-
int closingTagEndIndex = placesContent.indexOf('>', placesContent.indexOf("</bookmark", idIndex));
130-
var part2Tmp = placesContent.substring(closingTagEndIndex + 1).split("\\A\\v+", 2); //removing leading vertical whitespaces, but no indentation
110+
int closingTagEndIndex = config.indexOf('>', config.indexOf("</bookmark", idIndex));
111+
var part2Tmp = config.substring(closingTagEndIndex + 1).split("\\A\\v+", 2); //removing leading vertical whitespaces, but no indentation
131112
var contentToWrite2 = part2Tmp[part2Tmp.length - 1];
132113

133-
try (var writer = Files.newBufferedWriter(TMP_FILE, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
134-
writer.write(contentToWrite1);
135-
writer.newLine();
136-
writer.write(contentToWrite2);
137-
}
138-
// save
139-
Files.move(TMP_FILE, PLACES_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
140-
isRemoved = true;
141-
} catch (IOException | SAXException e) {
114+
return contentToWrite1 + "\n" + contentToWrite2;
115+
} catch (IOException | SAXException | IllegalStateException e) {
142116
throw new QuickAccessServiceException("Removing entry from KDE places file failed.", e);
143-
} finally {
144-
MODIFY_LOCK.unlock();
145117
}
146118
}
147119

120+
/**
121+
* Returns the start index (inclusive) of the {@link DolphinPlaces#ENTRY_TEMPLATE} entry
122+
* @param placesContent the content of the XBEL places file
123+
* @param idIndex start index (inclusive) of the entrys id tag value
124+
* @return start index of the first bookmark tag, searching backwards from idIndex
125+
*/
148126
private int indexOfEntryOpeningTag(String placesContent, int idIndex) {
149127
var xmlWhitespaceChars = List.of(' ', '\t', '\n');
150128
for (char c : xmlWhitespaceChars) {
151-
int idx = placesContent.lastIndexOf("<bookmark" + c, idIndex);
129+
int idx = placesContent.lastIndexOf("<bookmark" + c, idIndex); //with the whitespace we ensure, that no tags starting with "bookmark" (e.g. bookmarkz) are selected
152130
if (idx != -1) {
153131
return idx;
154132
}
155133
}
156-
throw new IllegalStateException("File " + PLACES_FILE + " is valid xbel file, but does not contain opening bookmark tag.");
134+
throw new IllegalStateException("Found entry id " + id + " in " + PLACES_FILE + ", but it is not a child of <bookmark> tag.");
157135
}
158136
}
159137

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package org.cryptomator.linux.quickaccess;
2+
3+
import org.cryptomator.integrations.quickaccess.QuickAccessService;
4+
import org.cryptomator.integrations.quickaccess.QuickAccessServiceException;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
8+
import java.io.IOException;
9+
import java.nio.charset.StandardCharsets;
10+
import java.nio.file.AtomicMoveNotSupportedException;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.nio.file.StandardCopyOption;
14+
import java.nio.file.StandardOpenOption;
15+
import java.util.concurrent.locks.Lock;
16+
import java.util.concurrent.locks.ReentrantReadWriteLock;
17+
18+
abstract class FileConfiguredQuickAccess implements QuickAccessService {
19+
20+
private static final Logger LOG = LoggerFactory.getLogger(FileConfiguredQuickAccess.class);
21+
22+
private final int maxFileSize;
23+
private final Path configFile;
24+
private final Path tmpFile;
25+
private final Lock modifyLock = new ReentrantReadWriteLock().writeLock();
26+
27+
FileConfiguredQuickAccess(Path configFile, int maxFileSize) {
28+
this.configFile = configFile;
29+
this.maxFileSize = maxFileSize;
30+
this.tmpFile = configFile.resolveSibling("." + configFile.getFileName() + ".cryptomator.tmp");
31+
Runtime.getRuntime().addShutdownHook(new Thread(this::cleanup));
32+
}
33+
34+
@Override
35+
public QuickAccessEntry add(Path target, String displayName) throws QuickAccessServiceException {
36+
try {
37+
modifyLock.lock();
38+
var entryAndConfig = addEntryToConfig(readConfig(), target, displayName);
39+
persistConfig(entryAndConfig.config());
40+
return entryAndConfig.entry();
41+
} catch (IOException e) {
42+
throw new QuickAccessServiceException("Failed to add entry to %s.".formatted(configFile), e);
43+
} finally {
44+
modifyLock.unlock();
45+
}
46+
}
47+
48+
record EntryAndConfig(FileConfiguredQuickAccessEntry entry, String config) {
49+
}
50+
51+
abstract EntryAndConfig addEntryToConfig(String config, Path target, String displayName) throws QuickAccessServiceException;
52+
53+
54+
protected abstract class FileConfiguredQuickAccessEntry implements QuickAccessEntry {
55+
56+
private volatile boolean isRemoved = false;
57+
58+
@Override
59+
public void remove() throws QuickAccessServiceException {
60+
try {
61+
modifyLock.lock();
62+
if (isRemoved) {
63+
return;
64+
}
65+
checkFileSize();
66+
var config = readConfig();
67+
var adjustedConfig = removeEntryFromConfig(config);
68+
persistConfig(adjustedConfig);
69+
isRemoved = true;
70+
} catch (IOException e) {
71+
throw new QuickAccessServiceException("Failed to remove entry to %s.".formatted(configFile), e);
72+
} finally {
73+
modifyLock.unlock();
74+
}
75+
}
76+
77+
abstract String removeEntryFromConfig(String config) throws QuickAccessServiceException;
78+
}
79+
80+
private String readConfig() throws IOException {
81+
return Files.readString(configFile, StandardCharsets.UTF_8);
82+
}
83+
84+
private void persistConfig(String newConfig) throws IOException {
85+
Files.writeString(tmpFile, newConfig, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
86+
try {
87+
Files.move(tmpFile, configFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
88+
} catch (AtomicMoveNotSupportedException e) {
89+
Files.move(tmpFile, configFile, StandardCopyOption.REPLACE_EXISTING);
90+
}
91+
}
92+
93+
private void checkFileSize() throws IOException {
94+
if (Files.size(configFile) > maxFileSize) {
95+
throw new IOException("File %s exceeds size of %d bytes".formatted(configFile, maxFileSize));
96+
}
97+
}
98+
99+
private void cleanup() {
100+
try {
101+
Files.deleteIfExists(tmpFile);
102+
} catch (IOException e) {
103+
LOG.warn("Unable to delete {}. Need to be deleted manually.", tmpFile);
104+
}
105+
}
106+
}

src/main/java/org/cryptomator/linux/quickaccess/NautilusBookmarks.java

Lines changed: 20 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,78 +7,50 @@
77
import org.cryptomator.integrations.quickaccess.QuickAccessService;
88
import org.cryptomator.integrations.quickaccess.QuickAccessServiceException;
99

10-
import java.io.IOException;
11-
import java.nio.charset.StandardCharsets;
1210
import java.nio.file.Files;
1311
import java.nio.file.Path;
14-
import java.nio.file.StandardCopyOption;
15-
import java.nio.file.StandardOpenOption;
16-
import java.util.concurrent.locks.Lock;
17-
import java.util.concurrent.locks.ReentrantReadWriteLock;
12+
import java.util.Objects;
13+
import java.util.stream.Collectors;
1814

1915
@Priority(100)
2016
@CheckAvailability
2117
@OperatingSystem(OperatingSystem.Value.LINUX)
2218
@DisplayName("GNOME Nautilus Bookmarks")
23-
public class NautilusBookmarks implements QuickAccessService {
19+
public class NautilusBookmarks extends FileConfiguredQuickAccess implements QuickAccessService {
2420

2521
private static final int MAX_FILE_SIZE = 4096;
2622
private static final Path BOOKMARKS_FILE = Path.of(System.getProperty("user.home"), ".config/gtk-3.0/bookmarks");
27-
private static final Path TMP_FILE = BOOKMARKS_FILE.resolveSibling("bookmarks.cryptomator.tmp");
28-
private static final Lock BOOKMARKS_LOCK = new ReentrantReadWriteLock().writeLock();
23+
24+
//SPI constructor
25+
public NautilusBookmarks() {
26+
super(BOOKMARKS_FILE, MAX_FILE_SIZE);
27+
}
2928

3029
@Override
31-
public QuickAccessService.QuickAccessEntry add(Path target, String displayName) throws QuickAccessServiceException {
30+
EntryAndConfig addEntryToConfig(String config, Path target, String displayName) throws QuickAccessServiceException {
3231
var uriPath = target.toAbsolutePath().toString().replace(" ", "%20");
3332
String entryLine = "file://" + uriPath + " " + displayName;
34-
try {
35-
BOOKMARKS_LOCK.lock();
36-
if (Files.size(BOOKMARKS_FILE) > MAX_FILE_SIZE) {
37-
throw new IOException("File %s exceeds size of %d bytes".formatted(BOOKMARKS_FILE, MAX_FILE_SIZE));
38-
}
39-
//by reading all lines, we ensure that each line is terminated with EOL
40-
var entries = Files.readAllLines(BOOKMARKS_FILE, StandardCharsets.UTF_8);
41-
entries.add(entryLine);
42-
Files.write(TMP_FILE, entries, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
43-
Files.move(TMP_FILE, BOOKMARKS_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
44-
return new NautilusQuickAccessEntry(entryLine);
45-
} catch (IOException e) {
46-
throw new QuickAccessServiceException("Adding entry to Nautilus bookmarks file failed.", e);
47-
} finally {
48-
BOOKMARKS_LOCK.unlock();
49-
}
33+
var entry = new NautilusQuickAccessEntry(entryLine);
34+
var adjustedConfig = config.stripTrailing() +
35+
"\n" +
36+
entryLine;
37+
return new EntryAndConfig(entry, adjustedConfig);
5038
}
5139

52-
static class NautilusQuickAccessEntry implements QuickAccessEntry {
40+
class NautilusQuickAccessEntry extends FileConfiguredQuickAccessEntry implements QuickAccessEntry {
5341

5442
private final String line;
55-
private volatile boolean isRemoved = false;
5643

5744
NautilusQuickAccessEntry(String line) {
5845
this.line = line;
5946
}
6047

6148
@Override
62-
public void remove() throws QuickAccessServiceException {
63-
try {
64-
BOOKMARKS_LOCK.lock();
65-
if (isRemoved) {
66-
return;
67-
}
68-
if (Files.size(BOOKMARKS_FILE) > MAX_FILE_SIZE) {
69-
throw new IOException("File %s exceeds size of %d bytes".formatted(BOOKMARKS_FILE, MAX_FILE_SIZE));
70-
}
71-
var entries = Files.readAllLines(BOOKMARKS_FILE);
72-
if (entries.remove(line)) {
73-
Files.write(TMP_FILE, entries, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
74-
Files.move(TMP_FILE, BOOKMARKS_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
75-
}
76-
isRemoved = true;
77-
} catch (IOException e) {
78-
throw new QuickAccessServiceException("Removing entry from Nautilus bookmarks file failed", e);
79-
} finally {
80-
BOOKMARKS_LOCK.unlock();
81-
}
49+
public String removeEntryFromConfig(String config) throws QuickAccessServiceException {
50+
return config.lines() //
51+
.map(l -> l.equals(line) ? null : l) //
52+
.filter(Objects::nonNull) //
53+
.collect(Collectors.joining("\n"));
8254
}
8355
}
8456

0 commit comments

Comments
 (0)