Skip to content

Commit f3abf04

Browse files
committed
implement base class for editing file-based configuration for quickAccessEntries
1 parent 73c3302 commit f3abf04

File tree

3 files changed

+152
-127
lines changed

3 files changed

+152
-127
lines changed

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

Lines changed: 26 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,10 @@
1515
import javax.xml.validation.Validator;
1616
import java.io.IOException;
1717
import java.io.StringReader;
18-
import java.nio.charset.StandardCharsets;
19-
import java.nio.file.AtomicMoveNotSupportedException;
2018
import java.nio.file.Files;
2119
import java.nio.file.Path;
22-
import java.nio.file.StandardCopyOption;
23-
import java.nio.file.StandardOpenOption;
2420
import java.util.List;
2521
import java.util.UUID;
26-
import java.util.concurrent.locks.Lock;
27-
import java.util.concurrent.locks.ReentrantLock;
2822

2923
/**
3024
* Implemenation of the {@link QuickAccessService} for KDE desktop environments using Dolphin file browser.
@@ -33,12 +27,10 @@
3327
@CheckAvailability
3428
@OperatingSystem(OperatingSystem.Value.LINUX)
3529
@Priority(90)
36-
public class DolphinPlaces implements QuickAccessService {
30+
public class DolphinPlaces extends FileConfiguredQuickAccess implements QuickAccessService {
3731

38-
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
3933
private static final Path PLACES_FILE = Path.of(System.getProperty("user.home"), ".local/share/user-places.xbel");
40-
private static final Path TMP_FILE = Path.of(System.getProperty("java.io.tmpdir"), "user-places.xbel.cryptomator.tmp");
41-
private static final Lock MODIFY_LOCK = new ReentrantLock();
4234
private static final String ENTRY_TEMPLATE = """
4335
<bookmark href=\"%s\">
4436
<title>%s</title>
@@ -52,7 +44,6 @@ public class DolphinPlaces implements QuickAccessService {
5244
</info>
5345
</bookmark>""";
5446

55-
5647
private static final Validator XML_VALIDATOR;
5748

5849
static {
@@ -65,84 +56,58 @@ public class DolphinPlaces implements QuickAccessService {
6556
}
6657
}
6758

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

6964
@Override
70-
public QuickAccessService.QuickAccessEntry add(Path target, String displayName) throws QuickAccessServiceException {
71-
String id = UUID.randomUUID().toString();
65+
EntryAndConfig addEntryToConfig(String config, Path target, String displayName) throws QuickAccessServiceException {
7266
try {
73-
MODIFY_LOCK.lock();
74-
if (Files.size(PLACES_FILE) > MAX_FILE_SIZE) {
75-
throw new IOException("File %s exceeds size of %d bytes".formatted(PLACES_FILE, MAX_FILE_SIZE));
76-
}
77-
var placesContent = Files.readString(PLACES_FILE);
67+
String id = UUID.randomUUID().toString();
7868
//validate
79-
XML_VALIDATOR.validate(new StreamSource(new StringReader(placesContent)));
69+
XML_VALIDATOR.validate(new StreamSource(new StringReader(config)));
8070
// modify
81-
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
82-
try (var writer = Files.newBufferedWriter(TMP_FILE, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
83-
writer.write(placesContent, 0, insertIndex);
84-
writer.newLine();
85-
writer.write(ENTRY_TEMPLATE.formatted(target.toUri(), displayName, id).indent(1));
86-
writer.newLine();
87-
writer.write(placesContent, insertIndex, placesContent.length() - insertIndex);
88-
}
89-
// save
90-
persistTmpFile();
91-
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(), displayName, id).indent(1) //
75+
+ "\n" //
76+
+ config.substring(insertIndex);
77+
return new EntryAndConfig(new DolphinPlacesEntry(id), adjustedConfig);
9278
} catch (SAXException | IOException e) {
9379
throw new QuickAccessServiceException("Adding entry to KDE places file failed.", e);
94-
} finally {
95-
MODIFY_LOCK.unlock();
9680
}
9781
}
9882

99-
private static class DolphinPlacesEntry implements QuickAccessEntry {
83+
private class DolphinPlacesEntry extends FileConfiguredQuickAccessEntry implements QuickAccessEntry {
10084

10185
private final String id;
102-
private volatile boolean isRemoved = false;
10386

10487
DolphinPlacesEntry(String id) {
10588
this.id = id;
10689
}
10790

10891
@Override
109-
public void remove() throws QuickAccessServiceException {
92+
public String removeEntryFromConfig(String config) throws QuickAccessServiceException {
11093
try {
111-
MODIFY_LOCK.lock();
112-
if (isRemoved) {
113-
return;
114-
}
115-
if (Files.size(PLACES_FILE) > MAX_FILE_SIZE) {
116-
throw new IOException("File %s exceeds size of %d bytes".formatted(PLACES_FILE, MAX_FILE_SIZE));
117-
}
118-
var placesContent = Files.readString(PLACES_FILE);
119-
int idIndex = placesContent.lastIndexOf(id);
94+
int idIndex = config.lastIndexOf(id);
12095
if (idIndex == -1) {
121-
isRemoved = true;
122-
return; //we assume someone has removed our entry
96+
return config; //assume someone has removed our entry, nothing to do
12397
}
12498
//validate
125-
XML_VALIDATOR.validate(new StreamSource(new StringReader(placesContent)));
99+
XML_VALIDATOR.validate(new StreamSource(new StringReader(config)));
126100
//modify
127-
int openingTagIndex = indexOfEntryOpeningTag(placesContent, idIndex);
128-
var contentToWrite1 = placesContent.substring(0, openingTagIndex).stripTrailing();
101+
int openingTagIndex = indexOfEntryOpeningTag(config, idIndex);
102+
var contentToWrite1 = config.substring(0, openingTagIndex).stripTrailing();
129103

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

134-
try (var writer = Files.newBufferedWriter(TMP_FILE, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
135-
writer.write(contentToWrite1);
136-
writer.newLine();
137-
writer.write(contentToWrite2);
138-
}
139-
// save
140-
persistTmpFile();
141-
isRemoved = true;
108+
return contentToWrite1 + "\n" + contentToWrite2;
142109
} catch (IOException | SAXException e) {
143110
throw new QuickAccessServiceException("Removing entry from KDE places file failed.", e);
144-
} finally {
145-
MODIFY_LOCK.unlock();
146111
}
147112
}
148113

@@ -158,14 +123,6 @@ private int indexOfEntryOpeningTag(String placesContent, int idIndex) {
158123
}
159124
}
160125

161-
static void persistTmpFile() throws IOException {
162-
try {
163-
Files.move(TMP_FILE, PLACES_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
164-
} catch (AtomicMoveNotSupportedException e) {
165-
Files.move(TMP_FILE, PLACES_FILE, StandardCopyOption.REPLACE_EXISTING);
166-
}
167-
}
168-
169126
@CheckAvailability
170127
public static boolean isSupported() {
171128
return Files.exists(PLACES_FILE);
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.resolve("." + 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(tmpFile, 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 & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,88 +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;
12-
import java.nio.file.AtomicMoveNotSupportedException;
1310
import java.nio.file.Files;
1411
import java.nio.file.Path;
15-
import java.nio.file.StandardCopyOption;
16-
import java.nio.file.StandardOpenOption;
17-
import java.util.concurrent.locks.Lock;
18-
import java.util.concurrent.locks.ReentrantReadWriteLock;
12+
import java.util.Objects;
13+
import java.util.stream.Collectors;
1914

2015
@Priority(100)
2116
@CheckAvailability
2217
@OperatingSystem(OperatingSystem.Value.LINUX)
2318
@DisplayName("GNOME Nautilus Bookmarks")
24-
public class NautilusBookmarks implements QuickAccessService {
19+
public class NautilusBookmarks extends FileConfiguredQuickAccess implements QuickAccessService {
2520

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

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

54-
static class NautilusQuickAccessEntry implements QuickAccessEntry {
40+
class NautilusQuickAccessEntry extends FileConfiguredQuickAccessEntry implements QuickAccessEntry {
5541

5642
private final String line;
57-
private volatile boolean isRemoved = false;
5843

5944
NautilusQuickAccessEntry(String line) {
6045
this.line = line;
6146
}
6247

6348
@Override
64-
public void remove() throws QuickAccessServiceException {
65-
try {
66-
BOOKMARKS_LOCK.lock();
67-
if (isRemoved) {
68-
return;
69-
}
70-
if (Files.size(BOOKMARKS_FILE) > MAX_FILE_SIZE) {
71-
throw new IOException("File %s exceeds size of %d bytes".formatted(BOOKMARKS_FILE, MAX_FILE_SIZE));
72-
}
73-
var entries = Files.readAllLines(BOOKMARKS_FILE);
74-
if (entries.remove(line)) {
75-
Files.write(TMP_FILE, entries, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
76-
persistTmpFile();
77-
}
78-
isRemoved = true;
79-
} catch (IOException e) {
80-
throw new QuickAccessServiceException("Removing entry from Nautilus bookmarks file failed", e);
81-
} finally {
82-
BOOKMARKS_LOCK.unlock();
83-
}
84-
}
85-
}
86-
87-
static void persistTmpFile() throws IOException {
88-
try {
89-
Files.move(TMP_FILE, BOOKMARKS_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
90-
} catch (AtomicMoveNotSupportedException e) {
91-
Files.move(TMP_FILE, BOOKMARKS_FILE, StandardCopyOption.REPLACE_EXISTING);
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"));
9254
}
9355
}
9456

0 commit comments

Comments
 (0)