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

Commit 9390614

Browse files
committed
Implement WebDAV Provider
1 parent 84ff68a commit 9390614

15 files changed

+1177
-8
lines changed

pom.xml

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,16 @@
1313

1414
<properties>
1515
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
16-
1716
<guava.version>29.0-jre</guava.version>
17+
18+
<okhttp.version>4.7.2</okhttp.version>
19+
<okhttp-digest.version>2.3</okhttp-digest.version>
20+
<okhttp.mockwebserver.version>4.7.2</okhttp.mockwebserver.version>
21+
<kxml2.version>2.3.0</kxml2.version>
22+
23+
<slf4j.version>1.7.28</slf4j.version>
24+
<logback.version>1.2.3</logback.version>
25+
1826
<junit.jupiter.version>5.6.2</junit.jupiter.version>
1927
<mockito.version>3.3.3</mockito.version>
2028
<hamcrest.version>2.2</hamcrest.version>
@@ -49,7 +57,36 @@
4957
<artifactId>guava</artifactId>
5058
<version>${guava.version}</version>
5159
</dependency>
52-
60+
61+
<!-- Logging -->
62+
<dependency>
63+
<groupId>org.slf4j</groupId>
64+
<artifactId>slf4j-api</artifactId>
65+
<version>${slf4j.version}</version>
66+
</dependency>
67+
<dependency>
68+
<groupId>org.slf4j</groupId>
69+
<artifactId>slf4j-simple</artifactId>
70+
<version>${slf4j.version}</version>
71+
</dependency>
72+
73+
<!-- WebDAV -->
74+
<dependency>
75+
<groupId>com.squareup.okhttp3</groupId>
76+
<artifactId>okhttp</artifactId>
77+
<version>${okhttp.version}</version>
78+
</dependency>
79+
<dependency>
80+
<groupId>com.burgstaller</groupId>
81+
<artifactId>okhttp-digest</artifactId>
82+
<version>${okhttp-digest.version}</version>
83+
</dependency>
84+
<dependency>
85+
<groupId>net.sf.kxml</groupId>
86+
<artifactId>kxml2</artifactId>
87+
<version>${kxml2.version}</version>
88+
</dependency>
89+
5390
<!-- Test -->
5491
<dependency>
5592
<groupId>org.junit.jupiter</groupId>
@@ -69,6 +106,12 @@
69106
<version>${hamcrest.version}</version>
70107
<scope>test</scope>
71108
</dependency>
109+
<dependency>
110+
<groupId>com.squareup.okhttp3</groupId>
111+
<artifactId>mockwebserver</artifactId>
112+
<version>${okhttp.mockwebserver.version}</version>
113+
<scope>test</scope>
114+
</dependency>
72115
</dependencies>
73116

74117
<build>

src/main/java/module-info.java

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.cryptomator.cloudaccess.webdav;
2+
3+
import java.util.HashSet;
4+
import java.util.Set;
5+
6+
class HeaderNames {
7+
8+
private final Set<String> lowercaseNames = new HashSet<>();
9+
10+
public HeaderNames(final String... headerNames) {
11+
for (final var headerName : headerNames) {
12+
lowercaseNames.add(headerName.toLowerCase());
13+
}
14+
}
15+
16+
public boolean contains(final String headerName) {
17+
return lowercaseNames.contains(headerName.toLowerCase());
18+
}
19+
20+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package org.cryptomator.cloudaccess.webdav;
2+
3+
import okhttp3.Headers;
4+
import okhttp3.Interceptor;
5+
import okhttp3.Protocol;
6+
import okhttp3.Request;
7+
import okhttp3.Response;
8+
9+
import java.io.IOException;
10+
11+
import static java.lang.String.format;
12+
import static java.util.concurrent.TimeUnit.NANOSECONDS;
13+
14+
public final class HttpLoggingInterceptor implements Interceptor {
15+
16+
private static final HeaderNames EXCLUDED_HEADERS = new HeaderNames(//
17+
// headers excluded because they are logged separately:
18+
"Content-Type", "Content-Length",
19+
// headers excluded because they contain sensitive information:
20+
"Authorization", //
21+
"WWW-Authenticate", //
22+
"Cookie", //
23+
"Set-Cookie" //
24+
);
25+
26+
public interface Logger {
27+
void log(String message);
28+
}
29+
30+
public HttpLoggingInterceptor(final Logger logger) {
31+
this.logger = logger;
32+
}
33+
34+
private final Logger logger;
35+
36+
@Override
37+
public Response intercept(final Chain chain) throws IOException {
38+
return proceedWithLogging(chain);
39+
}
40+
41+
private Response proceedWithLogging(final Chain chain) throws IOException {
42+
final var request = chain.request();
43+
logRequest(request, chain);
44+
return getAndLogResponse(request, chain);
45+
}
46+
47+
private void logRequest(final Request request, final Chain chain) throws IOException {
48+
logRequestStart(request, chain);
49+
logContentTypeAndLength(request);
50+
logHeaders(request.headers());
51+
logRequestEnd(request);
52+
}
53+
54+
private Response getAndLogResponse(final Request request, final Chain chain) throws IOException {
55+
final var startOfRequestMs = System.nanoTime();
56+
final var response = getResponseLoggingExceptions(request, chain);
57+
final var requestDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startOfRequestMs);
58+
logResponse(response, requestDurationMs);
59+
return response;
60+
}
61+
62+
private Response getResponseLoggingExceptions(final Request request, final Chain chain) throws IOException {
63+
try {
64+
return chain.proceed(request);
65+
} catch (Exception e) {
66+
logger.log("<-- HTTP FAILED: " + e);
67+
throw e;
68+
}
69+
}
70+
71+
private void logResponse(final Response response, final long requestDurationMs) {
72+
logResponseStart(response, requestDurationMs);
73+
logHeaders(response.headers());
74+
logger.log("<-- END HTTP");
75+
}
76+
77+
private void logRequestStart(final Request request, final Chain chain) throws IOException {
78+
final var connection = chain.connection();
79+
final var protocol = connection != null ? connection.protocol() : Protocol.HTTP_1_1;
80+
final var bodyLength = hasBody(request) ? request.body().contentLength() + "-byte body" : "unknown length";
81+
82+
logger.log(format("--> %s %s %s (%s)", //
83+
request.method(), //
84+
request.url(), //
85+
protocol, //
86+
bodyLength //
87+
));
88+
}
89+
90+
private void logContentTypeAndLength(final Request request) throws IOException {
91+
// Request body headers are only present when installed as a network interceptor. Force
92+
// them to be included (when available) so there values are known.
93+
if (hasBody(request)) {
94+
final var body = request.body();
95+
if (body.contentType() != null) {
96+
logger.log("Content-Type: " + body.contentType());
97+
}
98+
if (body.contentLength() != -1) {
99+
logger.log("Content-Length: " + body.contentLength());
100+
}
101+
}
102+
}
103+
104+
private void logRequestEnd(final Request request) throws IOException {
105+
logger.log("--> END " + request.method());
106+
}
107+
108+
private void logResponseStart(final Response response, final long requestDurationMs) {
109+
logger.log("<-- " + response.code() + ' ' + response.message() + ' ' + response.request().url() + " (" + requestDurationMs + "ms" + ')');
110+
}
111+
112+
private boolean hasBody(final Request request) {
113+
return request.body() != null;
114+
}
115+
116+
private void logHeaders(final Headers headers) {
117+
for (int i = 0, count = headers.size(); i < count; i++) {
118+
final var name = headers.name(i);
119+
if (isExcludedHeader(name)) {
120+
continue;
121+
}
122+
logger.log(name + ": " + headers.value(i));
123+
}
124+
}
125+
126+
private boolean isExcludedHeader(final String name) {
127+
return EXCLUDED_HEADERS.contains(name);
128+
}
129+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.cryptomator.cloudaccess.webdav;
2+
3+
import okhttp3.MediaType;
4+
import okhttp3.RequestBody;
5+
import okio.BufferedSink;
6+
import okio.Okio;
7+
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
11+
public class InputStreamRequestBody extends RequestBody {
12+
private final InputStream inputStream;
13+
14+
public static RequestBody from(final InputStream inputStream) {
15+
return new InputStreamRequestBody(inputStream);
16+
}
17+
18+
private InputStreamRequestBody(final InputStream inputStream) {
19+
if (inputStream == null) throw new NullPointerException("inputStream == null");
20+
this.inputStream = inputStream;
21+
}
22+
23+
@Override
24+
public MediaType contentType() {
25+
return MediaType.parse("application/octet-stream");
26+
}
27+
28+
@Override
29+
public long contentLength() throws IOException {
30+
return inputStream.available() == 0 ? -1 : inputStream.available();
31+
}
32+
33+
@Override
34+
public void writeTo(final BufferedSink sink) throws IOException {
35+
try(final var source = Okio.source(inputStream)) {
36+
sink.writeAll(source);
37+
}
38+
}
39+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.cryptomator.cloudaccess.webdav;
2+
3+
import okhttp3.MediaType;
4+
import okhttp3.RequestBody;
5+
import okio.*;
6+
import org.cryptomator.cloudaccess.api.ProgressListener;
7+
8+
import java.io.IOException;
9+
10+
public class ProgressRequestWrapper extends RequestBody {
11+
12+
protected RequestBody delegate;
13+
protected ProgressListener listener;
14+
15+
public ProgressRequestWrapper(final RequestBody delegate, final ProgressListener listener) {
16+
this.delegate = delegate;
17+
this.listener = listener;
18+
}
19+
20+
@Override
21+
public MediaType contentType() {
22+
return delegate.contentType();
23+
}
24+
25+
@Override
26+
public long contentLength() throws IOException {
27+
return delegate.contentLength();
28+
}
29+
30+
@Override
31+
public void writeTo(final BufferedSink sink) throws IOException {
32+
final var countingSink = new CountingSink(sink);
33+
34+
final var bufferedSink = Okio.buffer(countingSink);
35+
36+
delegate.writeTo(bufferedSink);
37+
38+
bufferedSink.flush();
39+
}
40+
41+
protected final class CountingSink extends ForwardingSink {
42+
43+
private long bytesWritten = 0;
44+
45+
public CountingSink(Sink delegate) {
46+
super(delegate);
47+
}
48+
49+
@Override
50+
public void write(final Buffer source, final long byteCount) throws IOException {
51+
super.write(source, byteCount);
52+
53+
bytesWritten += byteCount;
54+
listener.onProgress(bytesWritten);
55+
}
56+
57+
}
58+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.cryptomator.cloudaccess.webdav;
2+
3+
import okhttp3.MediaType;
4+
import okhttp3.ResponseBody;
5+
import okio.*;
6+
import org.cryptomator.cloudaccess.api.ProgressListener;
7+
8+
import java.io.IOException;
9+
10+
class ProgressResponseWrapper extends ResponseBody {
11+
12+
private final ResponseBody delegate;
13+
private final ProgressListener progressListener;
14+
private final int EOF = -1;
15+
private BufferedSource bufferedSource;
16+
17+
ProgressResponseWrapper(final ResponseBody delegate, final ProgressListener progressListener) {
18+
this.delegate = delegate;
19+
this.progressListener = progressListener;
20+
}
21+
22+
@Override public MediaType contentType() {
23+
return delegate.contentType();
24+
}
25+
26+
@Override public long contentLength() {
27+
return delegate.contentLength();
28+
}
29+
30+
@Override public BufferedSource source() {
31+
if (bufferedSource == null) {
32+
bufferedSource = Okio.buffer(source(delegate.source()));
33+
}
34+
return bufferedSource;
35+
}
36+
37+
private Source source(final Source source) {
38+
return new ForwardingSource(source) {
39+
long totalBytesRead = 0L;
40+
41+
@Override public long read(final Buffer sink, final long byteCount) throws IOException {
42+
long bytesRead = super.read(sink, byteCount);
43+
totalBytesRead += bytesRead != EOF ? bytesRead : 0;
44+
if(bytesRead != EOF) {
45+
progressListener.onProgress(totalBytesRead);
46+
}
47+
return bytesRead;
48+
}
49+
};
50+
}
51+
}

0 commit comments

Comments
 (0)