Skip to content
This repository was archived by the owner on Mar 14, 2025. It is now read-only.

Commit 677ec22

Browse files
committed
Merge branch 'develop' into release/0.3.0
2 parents f7da670 + 4072f9e commit 677ec22

File tree

6 files changed

+101
-111
lines changed

6 files changed

+101
-111
lines changed
Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
package org.cryptomator.cloudaccess.webdav;
22

3-
import org.cryptomator.cloudaccess.api.CloudItemMetadata;
4-
import org.cryptomator.cloudaccess.api.CloudPath;
3+
import com.google.common.base.Splitter;
4+
import com.google.common.collect.Streams;
55

66
import java.net.URLDecoder;
77
import java.nio.charset.StandardCharsets;
88
import java.time.Instant;
99
import java.util.Optional;
1010
import java.util.regex.Pattern;
1111

12-
import static org.cryptomator.cloudaccess.api.CloudItemType.FILE;
13-
import static org.cryptomator.cloudaccess.api.CloudItemType.FOLDER;
14-
1512
class PropfindEntryData {
13+
1614
private static final Pattern URI_PATTERN = Pattern.compile("^[a-z]+://[^/]+/(.*)$");
1715

18-
private CloudPath path;
16+
private String path;
1917

20-
private boolean file = true;
18+
private boolean collection = true;
2119
private Optional<Instant> lastModified = Optional.empty();
2220
private Optional<Long> size = Optional.empty();
2321

@@ -32,52 +30,47 @@ private String extractPath(final String pathOrUri) {
3230
}
3331
}
3432

33+
private String urlDecode(final String value) {
34+
return URLDecoder.decode(value, StandardCharsets.UTF_8);
35+
}
36+
37+
public Optional<Instant> getLastModified() {
38+
return lastModified;
39+
}
40+
3541
void setLastModified(final Optional<Instant> lastModified) {
3642
this.lastModified = lastModified;
3743
}
3844

39-
public CloudPath getPath() {
45+
public String getPath() {
4046
return path;
4147
}
4248

43-
public void setPath(final String pathOrUri) {
44-
this.path = CloudPath.of(extractPath(pathOrUri));
49+
void setPath(final String pathOrUri) {
50+
this.path = extractPath(pathOrUri);
4551
}
4652

4753
public Optional<Long> getSize() {
4854
return size;
4955
}
5056

51-
public void setSize(final Optional<Long> size) {
57+
void setSize(final Optional<Long> size) {
5258
this.size = size;
5359
}
5460

55-
private boolean isFile() {
56-
return file;
61+
public boolean isCollection() {
62+
return collection;
5763
}
5864

59-
public void setFile(final boolean file) {
60-
this.file = file;
65+
void setCollection(final boolean collection) {
66+
this.collection = collection;
6167
}
6268

63-
public CloudItemMetadata toCloudItem() {
64-
if (isFile()) {
65-
return new CloudItemMetadata(getName(), path, FILE, lastModified, size);
66-
} else {
67-
return new CloudItemMetadata(getName(), path, FOLDER);
68-
}
69-
}
70-
71-
private String urlDecode(final String value) {
72-
return URLDecoder.decode(value, StandardCharsets.UTF_8);
73-
}
74-
75-
int getDepth() {
76-
return path.getNameCount();
69+
public long getDepth() {
70+
return Splitter.on("/").omitEmptyStrings().splitToStream(path).count();
7771
}
7872

79-
private String getName() {
80-
return path.getFileName().toString();
73+
public String getName() {
74+
return Streams.findLast(Splitter.on("/").omitEmptyStrings().splitToStream(path)).orElse("");
8175
}
82-
8376
}

src/main/java/org/cryptomator/cloudaccess/webdav/PropfindResponseParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ private void assembleEntry() {
134134
entry.setLastModified(parseDate(lastModified));
135135
entry.setSize(parseLong(contentLength));
136136
entry.setPath(href);
137-
entry.setFile(!isCollection);
137+
entry.setCollection(isCollection);
138138

139139
entries.add(entry);
140140
}

src/main/java/org/cryptomator/cloudaccess/webdav/WebDavClient.java

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import okhttp3.Response;
77
import org.cryptomator.cloudaccess.api.CloudItemList;
88
import org.cryptomator.cloudaccess.api.CloudItemMetadata;
9+
import org.cryptomator.cloudaccess.api.CloudItemType;
910
import org.cryptomator.cloudaccess.api.CloudPath;
1011
import org.cryptomator.cloudaccess.api.ProgressListener;
1112
import org.cryptomator.cloudaccess.api.exceptions.AlreadyExistsException;
@@ -27,7 +28,7 @@
2728

2829
public class WebDavClient {
2930

30-
private static final Comparator<PropfindEntryData> ASCENDING_BY_DEPTH = Comparator.comparingInt(PropfindEntryData::getDepth);
31+
private static final Comparator<PropfindEntryData> ASCENDING_BY_DEPTH = Comparator.comparingLong(PropfindEntryData::getDepth);
3132

3233
private final WebDavCompatibleHttpClient httpClient;
3334
private final URL baseUrl;
@@ -39,30 +40,30 @@ public class WebDavClient {
3940
}
4041

4142
CloudItemList list(final CloudPath folder) throws CloudProviderException {
42-
try (final var response = executePropfindRequest(folder, PROPFIND_DEPTH.ONE)) {
43+
try (final var response = executePropfindRequest(folder, PropfindDepth.ONE)) {
4344
checkExecutionSucceeded(response.code());
4445

4546
final var nodes = getEntriesFromResponse(response);
4647

47-
return processDirList(nodes);
48+
return processDirList(nodes, folder);
4849
} catch (IOException | SAXException e) {
4950
throw new CloudProviderException(e);
5051
}
5152
}
5253

5354
CloudItemMetadata itemMetadata(final CloudPath path) throws CloudProviderException {
54-
try (final var response = executePropfindRequest(path, PROPFIND_DEPTH.ZERO)) {
55+
try (final var response = executePropfindRequest(path, PropfindDepth.ZERO)) {
5556
checkExecutionSucceeded(response.code());
5657

5758
final var nodes = getEntriesFromResponse(response);
5859

59-
return processGet(nodes);
60+
return processGet(nodes, path);
6061
} catch (IOException | SAXException e) {
6162
throw new CloudProviderException(e);
6263
}
6364
}
6465

65-
private Response executePropfindRequest(final CloudPath path, final PROPFIND_DEPTH propfind_depth) throws IOException {
66+
private Response executePropfindRequest(final CloudPath path, final PropfindDepth propfindDepth) throws IOException {
6667
final var body = "<d:propfind xmlns:d=\"DAV:\">\n" //
6768
+ "<d:prop>\n" //
6869
+ "<d:resourcetype />\n" //
@@ -74,7 +75,7 @@ private Response executePropfindRequest(final CloudPath path, final PROPFIND_DEP
7475
final var builder = new Request.Builder() //
7576
.method("PROPFIND", RequestBody.create(body, MediaType.parse(body))) //
7677
.url(absoluteURLFrom(path)) //
77-
.header("DEPTH", propfind_depth.value) //
78+
.header("Depth", propfindDepth.value) //
7879
.header("Content-Type", "text/xml");
7980

8081
return httpClient.execute(builder);
@@ -86,12 +87,12 @@ private List<PropfindEntryData> getEntriesFromResponse(final Response response)
8687
}
8788
}
8889

89-
private CloudItemMetadata processGet(final List<PropfindEntryData> entryData) {
90+
private CloudItemMetadata processGet(final List<PropfindEntryData> entryData, final CloudPath path) {
9091
entryData.sort(ASCENDING_BY_DEPTH);
91-
return entryData.size() >= 1 ? entryData.get(0).toCloudItem() : null;
92+
return entryData.size() >= 1 ? toCloudItem(entryData.get(0), path) : null;
9293
}
9394

94-
private CloudItemList processDirList(final List<PropfindEntryData> entryData) {
95+
private CloudItemList processDirList(final List<PropfindEntryData> entryData, final CloudPath folder) {
9596
var result = new CloudItemList(new ArrayList<>());
9697

9798
if (entryData.isEmpty()) {
@@ -103,11 +104,19 @@ private CloudItemList processDirList(final List<PropfindEntryData> entryData) {
103104
// because it's depth is 1 smaller than the depth
104105
// ot the other entries, thus we skip the first entry
105106
for (PropfindEntryData childEntry : entryData.subList(1, entryData.size())) {
106-
result = result.add(List.of(childEntry.toCloudItem()));
107+
result = result.add(List.of(toCloudItem(childEntry, folder.resolve(childEntry.getName()))));
107108
}
108109
return result;
109110
}
110111

112+
private CloudItemMetadata toCloudItem(final PropfindEntryData data, final CloudPath path) {
113+
if (data.isCollection()) {
114+
return new CloudItemMetadata(data.getName(), path, CloudItemType.FOLDER);
115+
} else {
116+
return new CloudItemMetadata(data.getName(), path, CloudItemType.FILE, data.getLastModified(), data.getSize());
117+
}
118+
}
119+
111120
CloudPath move(final CloudPath from, final CloudPath to, boolean replace) throws CloudProviderException {
112121
final var builder = new Request.Builder() //
113122
.method("MOVE", null) //
@@ -142,7 +151,7 @@ InputStream read(final CloudPath path, final ProgressListener progressListener)
142151

143152
InputStream read(final CloudPath path, final long offset, final long count, final ProgressListener progressListener) throws CloudProviderException {
144153
final var getRequest = new Request.Builder() //
145-
.header("Range", String.format("bytes=%d-%d", offset, offset + count - 1))
154+
.header("Range", String.format("bytes=%d-%d", offset, offset + count - 1)) //
146155
.get() //
147156
.url(absoluteURLFrom(path));
148157
return read(getRequest, progressListener);
@@ -156,7 +165,7 @@ private InputStream read(final Request.Builder getRequest, final ProgressListene
156165
final var countingBody = new ProgressResponseWrapper(response.body(), progressListener);
157166

158167
final int UNSATISFIABLE_RANGE = 416;
159-
if(response.code() == UNSATISFIABLE_RANGE) {
168+
if (response.code() == UNSATISFIABLE_RANGE) {
160169
return new ByteArrayInputStream(new byte[0]);
161170
}
162171

@@ -178,8 +187,8 @@ CloudItemMetadata write(final CloudPath file, final boolean replace, final Input
178187
}
179188

180189
final var countingBody = new ProgressRequestWrapper(InputStreamRequestBody.from(data, size), progressListener);
181-
final var requestBuilder = new Request.Builder()
182-
.url(absoluteURLFrom(file))
190+
final var requestBuilder = new Request.Builder() //
191+
.url(absoluteURLFrom(file)) //
183192
.put(countingBody);
184193

185194
try (final var response = httpClient.execute(requestBuilder)) {
@@ -199,7 +208,7 @@ private boolean exists(CloudPath path) throws CloudProviderException {
199208
}
200209

201210
CloudPath createFolder(final CloudPath path) throws CloudProviderException {
202-
if(exists(path)) {
211+
if (exists(path)) {
203212
throw new AlreadyExistsException(String.format("Folder %s already exists", path.toString()));
204213
}
205214

@@ -228,8 +237,8 @@ void delete(final CloudPath path) throws CloudProviderException {
228237
}
229238

230239
void checkServerCompatibility() throws ServerNotWebdavCompatibleException {
231-
final var optionsRequest = new Request.Builder()
232-
.method("OPTIONS", null)
240+
final var optionsRequest = new Request.Builder() //
241+
.method("OPTIONS", null) //
233242
.url(baseUrl);
234243

235244
try (final var response = httpClient.execute(optionsRequest)) {
@@ -282,19 +291,20 @@ URL absoluteURLFrom(final CloudPath relativePath) {
282291
}
283292
}
284293

285-
private enum PROPFIND_DEPTH {
286-
ZERO("0"),
287-
ONE("1"),
294+
private enum PropfindDepth {
295+
ZERO("0"), //
296+
ONE("1"), //
288297
INFINITY("infinity");
289298

290299
private final String value;
291300

292-
PROPFIND_DEPTH(final String value) {
301+
PropfindDepth(final String value) {
293302
this.value = value;
294303
}
295304
}
296305

297306
static class WebDavAuthenticator {
307+
298308
static WebDavClient createAuthenticatedWebDavClient(final WebDavCredential webDavCredential) throws ServerNotWebdavCompatibleException, UnauthorizedException {
299309
final var webDavClient = new WebDavClient(new WebDavCompatibleHttpClient(webDavCredential), webDavCredential);
300310

src/test/java/org/cryptomator/cloudaccess/webdav/PropfindResponseParserTest.java

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,8 @@ public class PropfindResponseParserTest {
2828
private static final String RESPONSE_ONE_FILE_MULTI_STATUS = "file-multi-status";
2929

3030
private static final CloudItemMetadata testFolder = new CloudItemMetadata("Gelöschte Dateien", CloudPath.of("/Gelöschte Dateien"), CloudItemType.FOLDER, Optional.empty(), Optional.empty());
31-
private static final CloudItemMetadata testFile = new CloudItemMetadata(
32-
"0.txt",
33-
CloudPath.of("/0.txt"),
34-
CloudItemType.FILE,
35-
Optional.of(TestUtil.toInstant("Thu, 18 May 2017 9:49:41 GMT")),
36-
Optional.of(54175L));
37-
private final Comparator<PropfindEntryData> ASCENDING_BY_DEPTH = Comparator.comparingInt(PropfindEntryData::getDepth);
31+
private static final CloudItemMetadata testFile = new CloudItemMetadata("0.txt", CloudPath.of("/0.txt"), CloudItemType.FILE, Optional.of(TestUtil.toInstant("Thu, 18 May 2017 9:49:41 GMT")), Optional.of(54175L));
32+
private final Comparator<PropfindEntryData> ASCENDING_BY_DEPTH = Comparator.comparingLong(PropfindEntryData::getDepth);
3833
private PropfindResponseParser propfindResponseParser;
3934

4035
@BeforeEach
@@ -45,7 +40,7 @@ public void setup() {
4540
@Test
4641
public void testEmptyResponseLeadsToEmptyCloudNodeList() throws SAXException, IOException {
4742
final var propfindEntryList = propfindResponseParser.parse(load(RESPONSE_EMPTY_DIRECTORY));
48-
final var cloudNodeItemList = processDirList(propfindEntryList);
43+
final var cloudNodeItemList = processDirList(propfindEntryList, CloudPath.of("/"));
4944

5045
Assertions.assertEquals(Collections.EMPTY_LIST, cloudNodeItemList.getItems());
5146
Assertions.assertEquals(Optional.empty(), cloudNodeItemList.getNextPageToken());
@@ -54,14 +49,9 @@ public void testEmptyResponseLeadsToEmptyCloudNodeList() throws SAXException, IO
5449
@Test
5550
public void testFolderWithoutServerPartInHrefResponseLeadsToFolderInCloudNodeListWithCompleteUrl() throws SAXException, IOException {
5651
final var propfindEntryList = propfindResponseParser.parse(load(RESPONSE_ONE_FILE_NO_SERVER));
57-
final var cloudNodeItemList = processDirList(propfindEntryList);
52+
final var cloudNodeItemList = processDirList(propfindEntryList, CloudPath.of("/User7de989b/asdasdasd/d/OC/"));
5853

59-
final var resultFolder = new CloudItemMetadata(
60-
"DYNTZMMHWLW25RZHWYEDHLFWIUZZG2",
61-
CloudPath.of("/User7de989b/asdasdasd/d/OC/DYNTZMMHWLW25RZHWYEDHLFWIUZZG2"),
62-
CloudItemType.FOLDER,
63-
Optional.empty(),
64-
Optional.empty());
54+
final var resultFolder = new CloudItemMetadata("DYNTZMMHWLW25RZHWYEDHLFWIUZZG2", CloudPath.of("/User7de989b/asdasdasd/d/OC/DYNTZMMHWLW25RZHWYEDHLFWIUZZG2"), CloudItemType.FOLDER, Optional.empty(), Optional.empty());
6555

6656
Assertions.assertEquals(1, cloudNodeItemList.getItems().size());
6757
Assertions.assertEquals(List.of(resultFolder), cloudNodeItemList.getItems());
@@ -70,7 +60,7 @@ public void testFolderWithoutServerPartInHrefResponseLeadsToFolderInCloudNodeLis
7060
@Test
7161
public void testFileResponseLeadsToFileAndFoldersInCloudNodeList() throws SAXException, IOException {
7262
final var propfindEntryList = propfindResponseParser.parse(load(RESPONSE_ONE_FILE_AND_FOLDERS));
73-
final var cloudNodeItemList = processDirList(propfindEntryList);
63+
final var cloudNodeItemList = processDirList(propfindEntryList, CloudPath.of("/"));
7464

7565
Assertions.assertEquals(2, cloudNodeItemList.getItems().size());
7666
Assertions.assertEquals(List.of(testFile, testFolder), cloudNodeItemList.getItems());
@@ -79,22 +69,16 @@ public void testFileResponseLeadsToFileAndFoldersInCloudNodeList() throws SAXExc
7969
@Test
8070
public void testFileWithMalFormattedDateResponseLeadsToFileAndFoldersInCloudNodeListWithoutDate() throws SAXException, IOException {
8171
final var propfindEntryList = propfindResponseParser.parse(load(RESPONSE_MAL_FORMATTED_DATE));
82-
final var cloudNodeItemList = processDirList(propfindEntryList);
72+
final var cloudNodeItemList = processDirList(propfindEntryList, CloudPath.of("/"));
8373

8474
Assertions.assertEquals(2, cloudNodeItemList.getItems().size());
85-
Assertions.assertEquals(
86-
List.of(
87-
new CloudItemMetadata("0.txt", CloudPath.of("/0.txt"), CloudItemType.FILE, Optional.empty(), Optional.of(54175L)),
88-
new CloudItemMetadata("Gelöschte Dateien", CloudPath.of("/Gelöschte Dateien"), CloudItemType.FOLDER, Optional.empty(), Optional.empty())
89-
),
90-
cloudNodeItemList.getItems()
91-
);
75+
Assertions.assertEquals(List.of(new CloudItemMetadata("0.txt", CloudPath.of("/0.txt"), CloudItemType.FILE, Optional.empty(), Optional.of(54175L)), new CloudItemMetadata("Gelöschte Dateien", CloudPath.of("/Gelöschte Dateien"), CloudItemType.FOLDER, Optional.empty(), Optional.empty())), cloudNodeItemList.getItems());
9276
}
9377

9478
@Test
9579
public void testFileMultiStatusLeadsToFolderInCloudNodeList() throws SAXException, IOException {
9680
final var propfindEntryList = propfindResponseParser.parse(load(RESPONSE_ONE_FILE_MULTI_STATUS));
97-
final var cloudNodeItemList = processDirList(propfindEntryList);
81+
final var cloudNodeItemList = processDirList(propfindEntryList, CloudPath.of("/"));
9882

9983
Assertions.assertEquals(1, cloudNodeItemList.getItems().size());
10084
Assertions.assertEquals(List.of(testFolder), cloudNodeItemList.getItems());
@@ -103,13 +87,13 @@ public void testFileMultiStatusLeadsToFolderInCloudNodeList() throws SAXExceptio
10387
@Test
10488
public void testFileNoPathResponseLeadsToFileAndFoldersInCloudNodeListWithoutDate() throws SAXException, IOException {
10589
final var propfindEntryList = propfindResponseParser.parse(load(RESPONSE_MAL_FORMATTED_NO_PATH));
106-
final var cloudNodeItemList = processDirList(propfindEntryList);
90+
final var cloudNodeItemList = processDirList(propfindEntryList, CloudPath.of("/"));
10791

10892
Assertions.assertEquals(0, cloudNodeItemList.getItems().size());
10993
Assertions.assertEquals(Collections.EMPTY_LIST, cloudNodeItemList.getItems());
11094
}
11195

112-
private CloudItemList processDirList(final List<PropfindEntryData> entryData) {
96+
private CloudItemList processDirList(final List<PropfindEntryData> entryData, final CloudPath folder) {
11397
var result = new CloudItemList(new ArrayList<>());
11498

11599
if (entryData.isEmpty()) {
@@ -121,11 +105,19 @@ private CloudItemList processDirList(final List<PropfindEntryData> entryData) {
121105
// because it's depth is 1 smaller than the depth
122106
// ot the other entries, thus we skip the first entry
123107
for (PropfindEntryData childEntry : entryData.subList(1, entryData.size())) {
124-
result = result.add(List.of(childEntry.toCloudItem()));
108+
result = result.add(List.of(toCloudItem(childEntry, folder.resolve(childEntry.getName()))));
125109
}
126110
return result;
127111
}
128112

113+
private CloudItemMetadata toCloudItem(final PropfindEntryData data, final CloudPath path) {
114+
if (data.isCollection()) {
115+
return new CloudItemMetadata(data.getName(), path, CloudItemType.FOLDER);
116+
} else {
117+
return new CloudItemMetadata(data.getName(), path, CloudItemType.FILE, data.getLastModified(), data.getSize());
118+
}
119+
}
120+
129121
@Test
130122
public void testMallFormattedResponseLeadsToSAXException() {
131123
Assertions.assertThrows(SAXException.class, () -> propfindResponseParser.parse(load(RESPONSE_MAL_FORMATTED_XMLPULLPARSER_EXCEPTION)));

0 commit comments

Comments
 (0)