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

Commit f8c4e06

Browse files
committed
Handle HTTP status code in each WebDAV-method separately
1 parent c27c782 commit f8c4e06

File tree

5 files changed

+128
-70
lines changed

5 files changed

+128
-70
lines changed

src/main/java/org/cryptomator/cloudaccess/api/CloudProvider.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ default CompletionStage<InputStream> read(CloudPath file, ProgressListener progr
130130
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.NotFoundException} If the parent directory of this file doesn't exist</li>
131131
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.TypeMismatchException} If the path points to a node that isn't a file</li>
132132
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.AlreadyExistsException} If a node with the given path already exists and <code>replace</code> is false</li>
133+
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.ParentFolderDoesNotExistException} If the parent folder of a node doesn't exists</li>
133134
* <li>{@link CloudProviderException} in case of generic I/O errors</li>
134135
* </ul>
135136
*
@@ -200,6 +201,7 @@ default CompletionStage<CloudPath> createFolderIfNonExisting(CloudPath folder) {
200201
* <ul>
201202
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.NotFoundException} If no item exists for the given source path</li>
202203
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.AlreadyExistsException} If a node with the given target path already exists and <code>replace</code> is false</li>
204+
* <li>{@link org.cryptomator.cloudaccess.api.exceptions.ParentFolderDoesNotExistException} If the parent folder of a node doesn't exists</li>
203205
* <li>{@link CloudProviderException} in case of generic I/O errors</li>
204206
* </ul>
205207
*
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package org.cryptomator.cloudaccess.api.exceptions;
2+
3+
public class ParentFolderDoesNotExistException extends CloudProviderException {
4+
}

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

Lines changed: 121 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import org.cryptomator.cloudaccess.api.exceptions.CloudProviderException;
1515
import org.cryptomator.cloudaccess.api.exceptions.InsufficientStorageException;
1616
import org.cryptomator.cloudaccess.api.exceptions.NotFoundException;
17+
import org.cryptomator.cloudaccess.api.exceptions.ParentFolderDoesNotExistException;
18+
import org.cryptomator.cloudaccess.api.exceptions.TypeMismatchException;
1719
import org.slf4j.Logger;
1820
import org.slf4j.LoggerFactory;
1921
import org.xml.sax.SAXException;
@@ -48,19 +50,17 @@ public class WebDavClient {
4850
CloudItemMetadata itemMetadata(final CloudPath path) throws CloudProviderException {
4951
LOG.trace("itemMetadata {}", path);
5052
try (final var response = executePropfindRequest(path, PropfindDepth.ZERO)) {
51-
checkExecutionSucceeded(response.code());
53+
checkPropfindExecutionSucceeded(response.code());
5254

53-
final var nodes = getEntriesFromResponse(response);
54-
55-
return processGet(nodes, path);
55+
return processGet(getEntriesFromResponse(response), path);
5656
} catch (IOException | SAXException e) {
5757
throw new CloudProviderException(e);
5858
}
5959
}
6060

6161
Quota quota(final CloudPath folder) throws CloudProviderException {
6262
LOG.trace("quota {}", folder);
63-
final var body = "<?xml version=\"1.0\" ?>\n" //
63+
final var body = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" //
6464
+ "<d:propfind xmlns:d=\"DAV:\">\n" //
6565
+ "<d:prop>\n" //
6666
+ "<d:quota-available-bytes/>\n" //
@@ -75,7 +75,7 @@ Quota quota(final CloudPath folder) throws CloudProviderException {
7575
.header("Content-Type", "text/xml");
7676

7777
try (final var response = httpClient.execute(builder)) {
78-
checkExecutionSucceeded(response.code());
78+
checkPropfindExecutionSucceeded(response.code());
7979

8080
try (final var responseBody = response.body()) {
8181
return new PropfindResponseParser().parseQuta(responseBody.byteStream());
@@ -88,7 +88,7 @@ Quota quota(final CloudPath folder) throws CloudProviderException {
8888
CloudItemList list(final CloudPath folder) throws CloudProviderException {
8989
LOG.trace("list {}", folder);
9090
try (final var response = executePropfindRequest(folder, PropfindDepth.ONE)) {
91-
checkExecutionSucceeded(response.code());
91+
checkPropfindExecutionSucceeded(response.code());
9292

9393
final var nodes = getEntriesFromResponse(response);
9494

@@ -98,8 +98,24 @@ CloudItemList list(final CloudPath folder) throws CloudProviderException {
9898
}
9999
}
100100

101+
private void checkPropfindExecutionSucceeded(int responseCode) {
102+
switch (responseCode) {
103+
case HttpURLConnection.HTTP_UNAUTHORIZED:
104+
throw new UnauthorizedException();
105+
case HttpURLConnection.HTTP_FORBIDDEN:
106+
throw new ForbiddenException();
107+
case HttpURLConnection.HTTP_NOT_FOUND:
108+
throw new NotFoundException();
109+
}
110+
111+
if (responseCode < 199 || responseCode > 300) {
112+
throw new CloudProviderException("Response code isn't between 200 and 300: " + responseCode);
113+
}
114+
}
115+
101116
private Response executePropfindRequest(final CloudPath path, final PropfindDepth propfindDepth) throws IOException {
102-
final var body = "<d:propfind xmlns:d=\"DAV:\">\n" //
117+
final var body = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" //
118+
+ "<d:propfind xmlns:d=\"DAV:\">\n" //
103119
+ "<d:prop>\n" //
104120
+ "<d:resourcetype />\n" //
105121
+ "<d:getcontentlength />\n" //
@@ -166,13 +182,26 @@ CloudPath move(final CloudPath from, final CloudPath to, boolean replace) throws
166182
}
167183

168184
try (final var response = httpClient.execute(builder)) {
169-
if (response.code() == HttpURLConnection.HTTP_PRECON_FAILED) {
170-
throw new AlreadyExistsException(absoluteURLFrom(to).toExternalForm());
185+
if (response.isSuccessful()) {
186+
return to;
187+
} else {
188+
switch (response.code()) {
189+
case HttpURLConnection.HTTP_UNAUTHORIZED:
190+
throw new UnauthorizedException();
191+
case HttpURLConnection.HTTP_FORBIDDEN:
192+
throw new ForbiddenException();
193+
case HttpURLConnection.HTTP_NOT_FOUND:
194+
throw new NotFoundException();
195+
case HttpURLConnection.HTTP_CONFLICT:
196+
throw new ParentFolderDoesNotExistException();
197+
case HttpURLConnection.HTTP_PRECON_FAILED:
198+
throw new AlreadyExistsException(absoluteURLFrom(to).toExternalForm());
199+
case HTTP_INSUFFICIENT_STORAGE:
200+
throw new InsufficientStorageException();
201+
default:
202+
throw new CloudProviderException("Response code isn't between 200 and 300: " + response.code());
203+
}
171204
}
172-
173-
checkExecutionSucceeded(response.code());
174-
175-
return to;
176205
} catch (IOException e) {
177206
throw new CloudProviderException(e);
178207
}
@@ -201,15 +230,23 @@ private InputStream read(final Request.Builder getRequest, final ProgressListene
201230
try {
202231
response = httpClient.execute(getRequest);
203232
final var countingBody = new ProgressResponseWrapper(response.body(), progressListener);
204-
205-
final int UNSATISFIABLE_RANGE = 416;
206-
if (response.code() == UNSATISFIABLE_RANGE) {
207-
return new ByteArrayInputStream(new byte[0]);
233+
if (response.isSuccessful()) {
234+
success = true;
235+
return countingBody.byteStream();
236+
} else {
237+
switch (response.code()) {
238+
case HttpURLConnection.HTTP_UNAUTHORIZED:
239+
throw new UnauthorizedException();
240+
case HttpURLConnection.HTTP_FORBIDDEN:
241+
throw new ForbiddenException();
242+
case HttpURLConnection.HTTP_NOT_FOUND:
243+
throw new NotFoundException();
244+
case 416: // UNSATISFIABLE_RANGE
245+
return new ByteArrayInputStream(new byte[0]);
246+
default:
247+
throw new CloudProviderException("Response code isn't between 200 and 300: " + response.code());
248+
}
208249
}
209-
210-
checkExecutionSucceeded(response.code());
211-
success = true;
212-
return countingBody.byteStream();
213250
} catch (IOException e) {
214251
throw new CloudProviderException(e);
215252
} finally {
@@ -233,7 +270,22 @@ void write(final CloudPath file, final boolean replace, final InputStream data,
233270
lastModified.ifPresent(instant -> requestBuilder.addHeader("X-OC-Mtime", String.valueOf(instant.getEpochSecond())));
234271

235272
try (final var response = httpClient.execute(requestBuilder)) {
236-
checkExecutionSucceeded(response.code());
273+
if (!response.isSuccessful()) {
274+
switch (response.code()) {
275+
case HttpURLConnection.HTTP_UNAUTHORIZED:
276+
throw new UnauthorizedException();
277+
case HttpURLConnection.HTTP_FORBIDDEN:
278+
throw new ForbiddenException();
279+
case HttpURLConnection.HTTP_BAD_METHOD:
280+
throw new TypeMismatchException();
281+
case HttpURLConnection.HTTP_CONFLICT:
282+
throw new ParentFolderDoesNotExistException();
283+
case HTTP_INSUFFICIENT_STORAGE:
284+
throw new InsufficientStorageException();
285+
default:
286+
throw new CloudProviderException("Response code isn't between 200 and 300: " + response.code());
287+
}
288+
}
237289
} catch (IOException e) {
238290
throw new CloudProviderException(e);
239291
}
@@ -249,17 +301,29 @@ private boolean exists(CloudPath path) throws CloudProviderException {
249301

250302
CloudPath createFolder(final CloudPath path) throws CloudProviderException {
251303
LOG.trace("createFolder {}", path);
252-
if (exists(path)) {
253-
throw new AlreadyExistsException(String.format("Folder %s already exists", path.toString()));
254-
}
255-
256304
final var builder = new Request.Builder() //
257305
.method("MKCOL", null) //
258306
.url(absoluteURLFrom(path));
259307

260308
try (final var response = httpClient.execute(builder)) {
261-
checkExecutionSucceeded(response.code());
262-
return path;
309+
if (response.isSuccessful()) {
310+
return path;
311+
} else {
312+
switch (response.code()) {
313+
case HttpURLConnection.HTTP_UNAUTHORIZED:
314+
throw new UnauthorizedException();
315+
case HttpURLConnection.HTTP_FORBIDDEN:
316+
throw new ForbiddenException();
317+
case HttpURLConnection.HTTP_BAD_METHOD:
318+
throw new AlreadyExistsException(String.format("Folder %s already exists", path.toString()));
319+
case HttpURLConnection.HTTP_CONFLICT:
320+
throw new ParentFolderDoesNotExistException();
321+
case HTTP_INSUFFICIENT_STORAGE:
322+
throw new InsufficientStorageException();
323+
default:
324+
throw new CloudProviderException("Response code isn't between 200 and 300: " + response.code());
325+
}
326+
}
263327
} catch (IOException e) {
264328
throw new CloudProviderException(e);
265329
}
@@ -272,7 +336,18 @@ void delete(final CloudPath path) throws CloudProviderException {
272336
.url(absoluteURLFrom(path));
273337

274338
try (final var response = httpClient.execute(builder)) {
275-
checkExecutionSucceeded(response.code());
339+
if (!response.isSuccessful()) {
340+
switch (response.code()) {
341+
case HttpURLConnection.HTTP_UNAUTHORIZED:
342+
throw new UnauthorizedException();
343+
case HttpURLConnection.HTTP_FORBIDDEN:
344+
throw new ForbiddenException();
345+
case HttpURLConnection.HTTP_NOT_FOUND:
346+
throw new NotFoundException(String.format("Node %s doesn't exists", path.toString()));
347+
default:
348+
throw new CloudProviderException("Response code isn't between 200 and 300: " + response.code());
349+
}
350+
}
276351
} catch (IOException e) {
277352
throw new CloudProviderException(e);
278353
}
@@ -285,10 +360,20 @@ void checkServerCompatibility() throws ServerNotWebdavCompatibleException {
285360
.url(baseUrl);
286361

287362
try (final var response = httpClient.execute(optionsRequest)) {
288-
checkExecutionSucceeded(response.code());
289-
final var containsDavHeader = response.headers().names().contains("DAV");
290-
if (!containsDavHeader) {
291-
throw new ServerNotWebdavCompatibleException();
363+
if (response.isSuccessful()) {
364+
final var containsDavHeader = response.headers().names().contains("DAV");
365+
if (!containsDavHeader) {
366+
throw new ServerNotWebdavCompatibleException();
367+
}
368+
} else {
369+
switch (response.code()) {
370+
case HttpURLConnection.HTTP_UNAUTHORIZED:
371+
throw new UnauthorizedException();
372+
case HttpURLConnection.HTTP_FORBIDDEN:
373+
throw new ForbiddenException();
374+
default:
375+
throw new CloudProviderException("Response code isn't between 200 and 300: " + response.code());
376+
}
292377
}
293378
} catch (IOException e) {
294379
throw new CloudProviderException(e);
@@ -297,37 +382,13 @@ void checkServerCompatibility() throws ServerNotWebdavCompatibleException {
297382

298383
void tryAuthenticatedRequest() throws UnauthorizedException {
299384
LOG.trace("tryAuthenticatedRequest");
300-
try {
301-
itemMetadata(CloudPath.of("/"));
302-
} catch (Exception e) {
303-
if (e instanceof UnauthorizedException) {
304-
throw e;
305-
}
306-
}
307-
}
308-
309-
private void checkExecutionSucceeded(final int status) throws CloudProviderException {
310-
switch (status) {
311-
case HttpURLConnection.HTTP_UNAUTHORIZED:
312-
throw new UnauthorizedException();
313-
case HttpURLConnection.HTTP_FORBIDDEN:
314-
throw new ForbiddenException();
315-
case HttpURLConnection.HTTP_NOT_FOUND: // fall through
316-
case HttpURLConnection.HTTP_CONFLICT: //
317-
throw new NotFoundException();
318-
case HTTP_INSUFFICIENT_STORAGE:
319-
throw new InsufficientStorageException();
320-
}
321-
322-
if (status < 199 || status > 300) {
323-
throw new CloudProviderException("Response code isn't between 200 and 300: " + status);
324-
}
385+
itemMetadata(CloudPath.of("/"));
325386
}
326387

327388
// visible for testing
328389
URL absoluteURLFrom(final CloudPath relativePath) {
329390
var basePath = CloudPath.of(baseUrl.getPath()).toAbsolutePath();
330-
var fullPath = IntStream.range(0, relativePath.getNameCount()).mapToObj(i -> relativePath.getName(i)).reduce(basePath, CloudPath::resolve);
391+
var fullPath = IntStream.range(0, relativePath.getNameCount()).mapToObj(relativePath::getName).reduce(basePath, CloudPath::resolve);
331392
try {
332393
return new URL(baseUrl, fullPath.toString());
333394
} catch (MalformedURLException e) {

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,6 @@ public void testWriteToAndReplaceExistingFile() throws IOException {
189189
.thenReturn(getInterceptedResponse(baseUrl, "item-write-response.xml"))
190190
.thenReturn(getInterceptedResponse(baseUrl, "item-write-response.xml"));
191191

192-
final var writtenItemMetadata = new CloudItemMetadata("foo.txt", CloudPath.of("/foo.txt"), CloudItemType.FILE, Optional.of(TestUtil.toInstant("Thu, 07 Jul 2020 16:55:50 GMT")), Optional.of(8193L));
193-
194192
InputStream inputStream = getClass().getResourceAsStream("/progress-request-text.txt");
195193
webDavClient.write(CloudPath.of("/foo.txt"), true, inputStream, inputStream.available(), Optional.empty(), ProgressListener.NO_PROGRESS_AWARE);
196194
}
@@ -199,7 +197,6 @@ public void testWriteToAndReplaceExistingFile() throws IOException {
199197
@DisplayName("create /foo")
200198
public void testCreateFolder() throws IOException {
201199
Mockito.when(webDavCompatibleHttpClient.execute(ArgumentMatchers.any()))
202-
.thenReturn(getInterceptedResponse(baseUrl, 404, ""))
203200
.thenReturn(getInterceptedResponse(baseUrl));
204201

205202
final var path = webDavClient.createFolder(CloudPath.of("/foo"));

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,8 @@ public class WebDavCloudProviderTestIT {
3636
private final CloudItemMetadata testFilePng = new CloudItemMetadata("Nextcloud.png", CloudPath.of("/Nextcloud.png"), CloudItemType.FILE, Optional.of(TestUtil.toInstant("Thu, 19 Feb 2020 10:24:12 GMT")), Optional.of(37042L));
3737
private final CloudItemMetadata testFolderPhotos = new CloudItemMetadata("Photos", CloudPath.of("/Photos"), CloudItemType.FOLDER, Optional.empty(), Optional.empty());
3838

39-
private final String webDavRequestBody = "<d:propfind xmlns:d=\"DAV:\">\n<d:prop>\n<d:resourcetype />\n<d:getcontentlength />\n<d:getlastmodified />\n</d:prop>\n</d:propfind>";
39+
private final String webDavRequestBody = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<d:propfind xmlns:d=\"DAV:\">\n<d:prop>\n<d:resourcetype />\n<d:getcontentlength />\n<d:getlastmodified />\n</d:prop>\n</d:propfind>";
4040

41-
//TODO: add timeout to webserver if it fails to start/handle the requests
4241
public WebDavCloudProviderTestIT() throws IOException, InterruptedException {
4342
server = new MockWebServer();
4443
server.start();
@@ -222,18 +221,13 @@ public void testWriteAndTrySetModDate() throws InterruptedException, IOException
222221
@Test
223222
@DisplayName("create /foo")
224223
public void testCreateFolder() throws InterruptedException {
225-
server.enqueue(getInterceptedResponse(404, ""));
226224
server.enqueue(getInterceptedResponse());
227225

228226
final var path = Assertions.assertTimeoutPreemptively(timeout, () -> provider.createFolder(CloudPath.of("/foo")).toCompletableFuture().join());
229227

230228
Assertions.assertEquals(path, CloudPath.of("/foo"));
231229

232230
var rq = Assertions.assertTimeoutPreemptively(timeout, () -> server.takeRequest());
233-
Assertions.assertEquals("PROPFIND", rq.getMethod());
234-
Assertions.assertEquals("/cloud/remote.php/webdav/foo", rq.getPath());
235-
236-
rq = Assertions.assertTimeoutPreemptively(timeout, () -> server.takeRequest());
237231
Assertions.assertEquals("MKCOL", rq.getMethod());
238232
Assertions.assertEquals("/cloud/remote.php/webdav/foo", rq.getPath());
239233
}

0 commit comments

Comments
 (0)