Skip to content

Commit 8066bc5

Browse files
committed
impl draft for dolphin places area
1 parent 08a438f commit 8066bc5

File tree

6 files changed

+612
-0
lines changed

6 files changed

+612
-0
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package org.cryptomator.linux.quickaccess;
2+
3+
import org.cryptomator.integrations.common.CheckAvailability;
4+
import org.cryptomator.integrations.common.DisplayName;
5+
import org.cryptomator.integrations.common.OperatingSystem;
6+
import org.cryptomator.integrations.common.Priority;
7+
import org.cryptomator.integrations.quickaccess.QuickAccessService;
8+
import org.cryptomator.integrations.quickaccess.QuickAccessServiceException;
9+
import org.xml.sax.SAXException;
10+
11+
import javax.xml.XMLConstants;
12+
import javax.xml.transform.Source;
13+
import javax.xml.transform.stream.StreamSource;
14+
import javax.xml.validation.SchemaFactory;
15+
import javax.xml.validation.Validator;
16+
import java.io.IOException;
17+
import java.io.StringReader;
18+
import java.nio.charset.StandardCharsets;
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
21+
import java.nio.file.StandardCopyOption;
22+
import java.nio.file.StandardOpenOption;
23+
import java.util.UUID;
24+
import java.util.concurrent.TimeUnit;
25+
import java.util.concurrent.locks.Lock;
26+
import java.util.concurrent.locks.ReentrantReadWriteLock;
27+
28+
/**
29+
* Implemenation of the {@link QuickAccessService} for KDE desktop environments using Dolphin file browser.
30+
*/
31+
@DisplayName("KDE Dolphin Places")
32+
@OperatingSystem(OperatingSystem.Value.LINUX)
33+
@Priority(90)
34+
public class DolphinPlaces implements QuickAccessService {
35+
36+
private static final int MAX_FILE_SIZE = 1 << 15; //xml is quite verbose
37+
private static final Path PLACES_FILE = Path.of(System.getProperty("user.home"), ".local/share/user-places.xbel");
38+
private static final Path TMP_FILE = PLACES_FILE.resolveSibling("user-places.xbel.cryptomator.tmp");
39+
private static final Lock MODIFY_LOCK = new ReentrantReadWriteLock().writeLock();
40+
private static final String ENTRY_TEMPLATE = """
41+
<bookmark href="%s">
42+
<title>%s</title>
43+
<info>
44+
<metadata owner="http://freedesktop.org">
45+
<bookmark:icon name="drive-harddisk"/>
46+
</metadata>
47+
<metadata owner="https://cryptomator.org">
48+
<id>%s</id>
49+
</metadata>
50+
</info>
51+
</bookmark>""";
52+
53+
54+
private static final Validator xmlValidator;
55+
56+
static {
57+
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
58+
try (var schemaDefinition = DolphinPlaces.class.getResourceAsStream("/xbel-1.0.xsd")) {
59+
Source schemaFile = new StreamSource(schemaDefinition);
60+
xmlValidator = factory.newSchema(schemaFile).newValidator();
61+
} catch (IOException | SAXException e) {
62+
throw new IllegalStateException("Failed to load included XBEL schema definition file.", e);
63+
}
64+
}
65+
66+
67+
@Override
68+
public QuickAccessService.QuickAccessEntry add(Path target, String displayName) throws QuickAccessServiceException {
69+
String id = UUID.randomUUID().toString();
70+
try {
71+
MODIFY_LOCK.lock();
72+
if (Files.size(PLACES_FILE) > MAX_FILE_SIZE) {
73+
throw new IOException("File %s exceeds size of %d bytes".formatted(PLACES_FILE, MAX_FILE_SIZE));
74+
}
75+
var placesContent = Files.readString(PLACES_FILE);
76+
//validate
77+
xmlValidator.validate(new StreamSource(new StringReader(placesContent)));
78+
// modify
79+
int insertIndex = placesContent.lastIndexOf("</xbel>"); //cannot be -1 due to validation
80+
try (var writer = Files.newBufferedWriter(TMP_FILE, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
81+
writer.write(placesContent, 0, insertIndex);
82+
writer.newLine();
83+
writer.write(ENTRY_TEMPLATE.formatted(target.toUri(), displayName, id).indent(1));
84+
writer.newLine();
85+
writer.write(placesContent, insertIndex, placesContent.length() - insertIndex);
86+
}
87+
// save
88+
Files.move(TMP_FILE, PLACES_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
89+
return new DolphinPlacesEntry(id);
90+
} catch (SAXException | IOException e) {
91+
throw new QuickAccessServiceException("Adding entry to KDE places file failed.", e);
92+
} finally {
93+
MODIFY_LOCK.unlock();
94+
}
95+
}
96+
97+
private static class DolphinPlacesEntry implements QuickAccessEntry {
98+
99+
private final String id;
100+
private volatile boolean isRemoved = false;
101+
102+
DolphinPlacesEntry(String id) {
103+
this.id = id;
104+
}
105+
106+
@Override
107+
public void remove() throws QuickAccessServiceException {
108+
try {
109+
MODIFY_LOCK.lock();
110+
if (isRemoved) {
111+
return;
112+
}
113+
if (Files.size(PLACES_FILE) > MAX_FILE_SIZE) {
114+
throw new IOException("File %s exceeds size of %d bytes".formatted(PLACES_FILE, MAX_FILE_SIZE));
115+
}
116+
var placesContent = Files.readString(PLACES_FILE);
117+
int idIndex = placesContent.lastIndexOf(id);
118+
if (idIndex == -1) {
119+
isRemoved = true;
120+
return; //we assume someone has removed our entry
121+
}
122+
//validate
123+
xmlValidator.validate(new StreamSource(new StringReader(placesContent)));
124+
//modify
125+
var placesContentPart1 = placesContent.substring(0, idIndex);
126+
int openingTagIndex = placesContentPart1.lastIndexOf("<bookmark href=");
127+
var contentToWrite1 = placesContentPart1.substring(0, openingTagIndex).stripTrailing();
128+
129+
int closingTagIndex = placesContent.indexOf("</bookmark>", idIndex);
130+
var part2Tmp = placesContent.substring(closingTagIndex + "</bookmark>".length()).split("\\v*", 2); //removing leading vertical whitespaces
131+
var contentToWrite2 = part2Tmp.length == 1 ? part2Tmp[0] : part2Tmp[1];
132+
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) {
142+
throw new QuickAccessServiceException("Removing entry from KDE places file failed.", e);
143+
} finally {
144+
MODIFY_LOCK.unlock();
145+
}
146+
}
147+
}
148+
149+
@CheckAvailability
150+
public static boolean isSupported() {
151+
try {
152+
var nautilusExistsProc = new ProcessBuilder().command("test", "`command -v dolphin`").start();
153+
if (nautilusExistsProc.waitFor(5000, TimeUnit.MILLISECONDS)) {
154+
return nautilusExistsProc.exitValue() == 0;
155+
}
156+
} catch (IOException | InterruptedException e) {
157+
//NO-OP
158+
}
159+
return false;
160+
}
161+
}

src/main/resources/xbel-1.0.dtd

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<!-- This is the XML Bookmarks Exchange Language, version 1.0. It
2+
should be used with the formal public identifier:
3+
4+
+//IDN python.org//DTD XML Bookmark Exchange Language 1.0//EN//XML
5+
6+
One valid system identifier at which this DTD will remain
7+
available is:
8+
9+
http://pyxml.sourceforge.net/topics/dtds/xbel-1.0.dtd
10+
11+
More information on the DTD, including reference
12+
documentation, is available at:
13+
14+
http://www.python.org/topics/xml/xbel/
15+
16+
Attributes which take date/time values should encode the value
17+
according to the W3C NOTE on date/time formats:
18+
19+
http://www.w3.org/TR/NOTE-datetime
20+
-->
21+
22+
23+
<!-- Customization entities. Define these before "including" this DTD
24+
to create "subclassed" DTDs.
25+
-->
26+
<!ENTITY % local.node.att "">
27+
<!ENTITY % local.url.att "">
28+
<!ENTITY % local.nodes.mix "">
29+
30+
<!ENTITY % node.att "id ID #IMPLIED
31+
added CDATA #IMPLIED
32+
%local.node.att;">
33+
34+
<!ENTITY % url.att "href CDATA #REQUIRED
35+
visited CDATA #IMPLIED
36+
modified CDATA #IMPLIED
37+
%local.url.att;">
38+
39+
<!ENTITY % nodes.mix "bookmark|folder|alias|separator
40+
%local.nodes.mix;">
41+
42+
43+
<!ELEMENT xbel (title?, info?, desc?, (%nodes.mix;)*)>
44+
<!ATTLIST xbel
45+
%node.att;
46+
version CDATA #FIXED "1.0"
47+
>
48+
<!ELEMENT title (#PCDATA)>
49+
50+
<!--=================== Info ======================================-->
51+
52+
<!ELEMENT info (metadata+)>
53+
54+
<!ELEMENT metadata EMPTY>
55+
<!ATTLIST metadata
56+
owner CDATA #REQUIRED
57+
>
58+
59+
<!--=================== Folder ====================================-->
60+
61+
<!ELEMENT folder (title?, info?, desc?, (%nodes.mix;)*)>
62+
<!ATTLIST folder
63+
%node.att;
64+
folded (yes|no) 'yes'
65+
>
66+
67+
<!--=================== Bookmark ==================================-->
68+
69+
<!ELEMENT bookmark (title?, info?, desc?)>
70+
<!ATTLIST bookmark
71+
%node.att;
72+
%url.att;
73+
>
74+
75+
<!ELEMENT desc (#PCDATA)>
76+
77+
<!--=================== Separator =================================-->
78+
79+
<!ELEMENT separator EMPTY>
80+
81+
<!--=================== Alias =====================================-->
82+
83+
<!-- <alias> elements correspond to Netscape bookmark aliases. The
84+
required "ref" attribute must refer to a <bookmark> or <folder>
85+
element. Note that MSIE aliases can refer to folders, so that is
86+
supported in XBEL. Applications must be careful about traversing
87+
aliases to folders to avoid improper recursion through circular
88+
data structures.
89+
-->
90+
91+
<!ELEMENT alias EMPTY>
92+
<!ATTLIST alias
93+
ref IDREF #REQUIRED
94+
>

0 commit comments

Comments
 (0)