/*
* Copyright 2016 The Simple File Server Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sfs.nodes;
import com.google.common.base.Optional;
import com.google.common.escape.Escaper;
import com.google.common.net.HostAndPort;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.streams.ReadStream;
import org.sfs.Server;
import org.sfs.VertxContext;
import org.sfs.filesystem.volume.DigestBlob;
import org.sfs.filesystem.volume.HeaderBlob;
import org.sfs.filesystem.volume.ReadStreamBlob;
import org.sfs.io.BufferEndableWriteStream;
import org.sfs.io.HttpClientRequestEndableWriteStream;
import org.sfs.rx.BufferToJsonObject;
import org.sfs.rx.Defer;
import org.sfs.rx.HttpClientKeepAliveResponseBodyBuffer;
import org.sfs.rx.HttpClientResponseBodyBuffer;
import org.sfs.rx.ObservableFuture;
import org.sfs.rx.RxHelper;
import org.sfs.util.HttpClientRequestAndResponse;
import org.sfs.util.MessageDigestFactory;
import org.sfs.vo.TransientServiceDef;
import rx.Observable;
import java.util.Collection;
import static com.google.common.base.Optional.absent;
import static com.google.common.base.Optional.of;
import static com.google.common.io.BaseEncoding.base64;
import static com.google.common.net.UrlEscapers.urlFragmentEscaper;
import static io.vertx.core.http.HttpHeaders.CONTENT_LENGTH;
import static io.vertx.core.logging.LoggerFactory.getLogger;
import static java.lang.String.format;
import static java.lang.String.valueOf;
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
import static java.net.HttpURLConnection.HTTP_OK;
import static org.sfs.io.AsyncIO.pump;
import static org.sfs.rx.RxHelper.combineSinglesDelayError;
import static org.sfs.util.SfsHttpHeaders.X_SFS_REMOTE_NODE_TOKEN;
import static org.sfs.util.SfsHttpQueryParams.KEEP_ALIVE_TIMEOUT;
import static org.sfs.util.SfsHttpQueryParams.LENGTH;
import static org.sfs.util.SfsHttpQueryParams.OFFSET;
import static org.sfs.util.SfsHttpQueryParams.POSITION;
import static org.sfs.util.SfsHttpQueryParams.VOLUME;
import static org.sfs.util.SfsHttpQueryParams.X_CONTENT_COMPUTED_DIGEST_PREFIX;
import static rx.Observable.just;
public class RemoteNode extends AbstractNode {
private static final Logger LOGGER = getLogger(RemoteNode.class);
private final Vertx vertx;
private final Collection<HostAndPort> hostAndPorts;
private final int responseTimeout;
private final String remoteNodeSecret;
private final HttpClient httpClient;
private final Nodes nodes;
public RemoteNode(VertxContext<Server> vertxContext, int responseTimeout, Collection<HostAndPort> hostAndPorts) {
this.hostAndPorts = hostAndPorts;
this.responseTimeout = responseTimeout;
this.remoteNodeSecret = base64().encode(vertxContext.verticle().getRemoteNodeSecret());
this.httpClient = vertxContext.verticle().httpClient(false);
this.nodes = vertxContext.verticle().nodes();
this.vertx = vertxContext.vertx();
}
@Override
public Observable<Optional<TransientServiceDef>> getNodeStats() {
return Defer.aVoid()
.flatMap(aVoid ->
nodes.connectFirstAvailable(
vertx,
hostAndPorts,
hostAndPort -> {
String url =
format("http://%s/_internal_node/stats", hostAndPort.toString());
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("getNodeStats " + url);
}
ObservableFuture<HttpClientResponse> handler = RxHelper.observableFuture();
HttpClientRequest httpClientRequest =
httpClient
.getAbs(url, httpClientResponse -> {
httpClientResponse.pause();
handler.complete(httpClientResponse);
})
.exceptionHandler(handler::fail)
.putHeader(X_SFS_REMOTE_NODE_TOKEN, remoteNodeSecret)
.setTimeout(responseTimeout);
httpClientRequest.end();
return handler.map(httpClientResponse -> new HttpClientRequestAndResponse(httpClientRequest, httpClientResponse));
}))
.map(HttpClientRequestAndResponse::getResponse)
.flatMap(httpClientResponse ->
Defer.just(httpClientResponse)
.flatMap(new HttpClientResponseBodyBuffer(HTTP_OK))
.map(new BufferToJsonObject())
.filter(entries -> !entries.isEmpty())
.map(jsonObject -> {
TransientServiceDef transientServiceDef = new TransientServiceDef();
transientServiceDef.merge(jsonObject);
return transientServiceDef;
})
.map(Optional::of)
)
.singleOrDefault(Optional.absent());
}
@Override
public Observable<Optional<DigestBlob>> checksum(String volumeId, long position, Optional<Long> oOffset, Optional<Long> oLength, MessageDigestFactory... messageDigestFactories) {
return Defer.aVoid()
.flatMap(aVoid ->
nodes.connectFirstAvailable(
vertx,
hostAndPorts,
hostAndPort -> {
Escaper escaper = urlFragmentEscaper();
StringBuilder urlBuilder =
new StringBuilder("http://")
.append(hostAndPort.toString());
urlBuilder = urlBuilder.append("/_internal_node_data/blob/checksum?");
urlBuilder = urlBuilder.append(KEEP_ALIVE_TIMEOUT);
urlBuilder = urlBuilder.append('=');
urlBuilder = urlBuilder.append(responseTimeout / 2);
urlBuilder = urlBuilder.append('&');
urlBuilder = urlBuilder.append(VOLUME);
urlBuilder = urlBuilder.append('=');
urlBuilder = urlBuilder.append(escaper.escape(volumeId));
urlBuilder = urlBuilder.append('&');
urlBuilder = urlBuilder.append(escaper.escape(POSITION));
urlBuilder = urlBuilder.append('=');
urlBuilder = urlBuilder.append(position);
if (messageDigestFactories.length > 0) {
for (MessageDigestFactory instance : messageDigestFactories) {
urlBuilder = urlBuilder.append('&');
urlBuilder =
urlBuilder
.append(escaper.escape(format("%s%s", X_CONTENT_COMPUTED_DIGEST_PREFIX, instance.getValue())))
.append('=')
.append("true");
}
}
final String url = urlBuilder.toString();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("get " + url);
}
ObservableFuture<HttpClientResponse> handler = RxHelper.observableFuture();
HttpClientRequest httpClientRequest =
httpClient
.getAbs(url, httpClientResponse -> {
httpClientResponse.pause();
handler.complete(httpClientResponse);
})
.exceptionHandler(handler::fail)
.putHeader(X_SFS_REMOTE_NODE_TOKEN, remoteNodeSecret)
.setTimeout(responseTimeout);
httpClientRequest.end();
return handler.map(httpClientResponse -> new HttpClientRequestAndResponse(httpClientRequest, httpClientResponse));
}))
.map(HttpClientRequestAndResponse::getResponse)
.flatMap(httpClientResponse ->
just(httpClientResponse)
.flatMap(new HttpClientKeepAliveResponseBodyBuffer())
.map(buffer -> {
if (HTTP_OK != httpClientResponse.statusCode()) {
throw new HttpClientResponseException(httpClientResponse, buffer);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Buffer is " + buffer.toString());
}
return buffer;
})
.map(new BufferToJsonObject())
.map(jsonObject -> {
Integer code = jsonObject.getInteger("code");
if (code != null) {
if (HTTP_OK == code) {
return of(jsonObject);
} else if (HTTP_NOT_FOUND == code) {
return Optional.<JsonObject>absent();
}
}
throw new HttpClientResponseException(httpClientResponse, jsonObject);
})
.filter(Optional::isPresent)
.map(Optional::get)
.map(jsonObject -> {
JsonObject blob = jsonObject.getJsonObject("blob");
return of(new DigestBlob(blob));
}))
.singleOrDefault(absent());
}
@Override
public Observable<Optional<HeaderBlob>> delete(String volumeId, final long position) {
return Defer.aVoid()
.flatMap(aVoid ->
nodes.connectFirstAvailable(
vertx,
hostAndPorts,
hostAndPort -> {
Escaper escaper = urlFragmentEscaper();
String url =
format("http://%s/_internal_node_data/blob?%s=%s&%s=%d",
hostAndPort.toString(),
escaper.escape(VOLUME),
escaper.escape(volumeId),
escaper.escape(POSITION),
position);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("delete " + url);
}
ObservableFuture<HttpClientResponse> handler = RxHelper.observableFuture();
HttpClientRequest httpClientRequest =
httpClient
.deleteAbs(url, httpClientResponse -> {
httpClientResponse.pause();
handler.complete(httpClientResponse);
})
.exceptionHandler(handler::fail)
.putHeader(X_SFS_REMOTE_NODE_TOKEN, remoteNodeSecret)
.setTimeout(responseTimeout);
httpClientRequest.end();
return handler.map(httpClientResponse -> new HttpClientRequestAndResponse(httpClientRequest, httpClientResponse));
}))
.map(HttpClientRequestAndResponse::getResponse)
.flatMap(httpClientResponse ->
Defer.just(httpClientResponse)
.flatMap(new HttpClientResponseBodyBuffer())
.map(buffer -> {
int status = httpClientResponse.statusCode();
if (HTTP_NOT_MODIFIED == status) {
return absent();
} else if (HTTP_NO_CONTENT == status) {
return of(new HeaderBlob(httpClientResponse));
} else {
throw new HttpClientResponseException(httpClientResponse, buffer);
}
}));
}
@Override
public Observable<Optional<HeaderBlob>> acknowledge(String volumeId, final long position) {
return Defer.aVoid()
.flatMap(aVoid ->
nodes.connectFirstAvailable(
vertx,
hostAndPorts,
hostAndPort -> {
Escaper escaper = urlFragmentEscaper();
final String url =
format("http://%s/_internal_node_data/blob/ack?%s=%s&%s=%d",
hostAndPort.toString(),
escaper.escape(VOLUME),
escaper.escape(volumeId),
escaper.escape(POSITION),
position);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("acknowledge " + url);
}
ObservableFuture<HttpClientResponse> handler = RxHelper.observableFuture();
HttpClientRequest httpClientRequest =
httpClient
.putAbs(url, httpClientResponse -> {
httpClientResponse.pause();
handler.complete(httpClientResponse);
})
.exceptionHandler(handler::fail)
.putHeader(X_SFS_REMOTE_NODE_TOKEN, remoteNodeSecret)
.setTimeout(responseTimeout);
httpClientRequest.end();
return handler.map(httpClientResponse -> new HttpClientRequestAndResponse(httpClientRequest, httpClientResponse));
}))
.map(HttpClientRequestAndResponse::getResponse)
.flatMap(httpClientResponse ->
Defer.just(httpClientResponse)
.flatMap(new HttpClientResponseBodyBuffer())
.map(buffer -> {
int status = httpClientResponse.statusCode();
if (HTTP_NOT_MODIFIED == status) {
return absent();
} else if (HTTP_NO_CONTENT == status) {
return of(new HeaderBlob(httpClientResponse));
} else {
throw new HttpClientResponseException(httpClientResponse, buffer);
}
}));
}
@Override
public Observable<Optional<ReadStreamBlob>> createReadStream(String volumeId, final long position, final Optional<Long> oOffset, final Optional<Long> oLength) {
return Defer.aVoid()
.flatMap(aVoid ->
nodes.connectFirstAvailable(
vertx,
hostAndPorts,
hostAndPort -> {
Escaper escaper = urlFragmentEscaper();
StringBuilder urlBuilder =
new StringBuilder("http://")
.append(hostAndPort.toString())
.append("/_internal_node_data/blob")
.append('?')
.append(escaper.escape(VOLUME))
.append('=')
.append(escaper.escape(volumeId))
.append('&')
.append(escaper.escape(POSITION))
.append('=')
.append(position);
if (oOffset.isPresent()) {
long offset = oOffset.get();
urlBuilder =
urlBuilder.append('&')
.append(escaper.escape(OFFSET))
.append('=')
.append(offset);
}
if (oLength.isPresent()) {
long length = oLength.get();
urlBuilder =
urlBuilder.append('&')
.append(escaper.escape(LENGTH))
.append('=')
.append(length);
}
String url = urlBuilder.toString();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("createReadStream " + url);
}
ObservableFuture<HttpClientResponse> handler = RxHelper.observableFuture();
HttpClientRequest httpClientRequest =
httpClient
.getAbs(url, httpClientResponse -> {
httpClientResponse.pause();
handler.complete(httpClientResponse);
})
.exceptionHandler(handler::fail)
.putHeader(X_SFS_REMOTE_NODE_TOKEN, remoteNodeSecret)
.setTimeout(responseTimeout);
httpClientRequest.end();
return handler.map(httpClientResponse -> new HttpClientRequestAndResponse(httpClientRequest, httpClientResponse));
}))
.map(HttpClientRequestAndResponse::getResponse)
.flatMap(httpClientResponse -> {
int status = httpClientResponse.statusCode();
if (HTTP_OK == status) {
ReadStreamBlob readStreamBlob = new ReadStreamBlob(httpClientResponse) {
@Override
public Observable<Void> produce(BufferEndableWriteStream endableWriteStream) {
return pump(httpClientResponse, endableWriteStream);
}
};
return just(of(readStreamBlob));
} else if (HTTP_NOT_FOUND == status) {
return just(httpClientResponse)
.flatMap(new HttpClientResponseBodyBuffer())
.map(buffer -> absent());
} else {
return just(httpClientResponse)
.flatMap(new HttpClientResponseBodyBuffer())
.map(buffer -> {
throw new HttpClientResponseException(httpClientResponse, buffer);
});
}
});
}
@Override
public Observable<Boolean> canReadVolume(String volumeId) {
return Defer.aVoid()
.flatMap(aVoid ->
nodes.connectFirstAvailable(
vertx,
hostAndPorts,
hostAndPort -> {
Escaper escaper = urlFragmentEscaper();
String url = format("http://%s/_internal_node_data/blob/canread?%s=%s",
hostAndPort.toString(),
escaper.escape(VOLUME),
escaper.escape(escaper.escape(volumeId)));
ObservableFuture<HttpClientResponse> handler = RxHelper.observableFuture();
HttpClientRequest httpClientRequest =
httpClient.getAbs(url,
httpClientResponse -> {
httpClientResponse.pause();
handler.complete(httpClientResponse);
})
.exceptionHandler(handler::fail)
.putHeader(X_SFS_REMOTE_NODE_TOKEN, remoteNodeSecret)
.setTimeout(responseTimeout);
httpClientRequest.end();
return handler.map(httpClientResponse -> new HttpClientRequestAndResponse(httpClientRequest, httpClientResponse));
}))
.map(HttpClientRequestAndResponse::getResponse)
.flatMap(httpClientResponse ->
just(httpClientResponse)
.flatMap(new HttpClientResponseBodyBuffer())
.map(buffer -> {
int status = httpClientResponse.statusCode();
if (status >= 400) {
throw new HttpClientResponseException(httpClientResponse, buffer);
}
return true;
}));
}
@Override
public Observable<Boolean> canWriteVolume(String volumeId) {
return Defer.aVoid()
.flatMap(aVoid ->
nodes.connectFirstAvailable(
vertx,
hostAndPorts,
hostAndPort -> {
Escaper escaper = urlFragmentEscaper();
String url = format("http://%s/_internal_node_data/blob/canwrite?%s=%s",
hostAndPort.toString(),
escaper.escape(VOLUME),
escaper.escape(escaper.escape(volumeId)));
ObservableFuture<HttpClientResponse> handler = RxHelper.observableFuture();
HttpClientRequest httpClientRequest =
httpClient.getAbs(url,
httpClientResponse -> {
httpClientResponse.pause();
handler.complete(httpClientResponse);
})
.exceptionHandler(handler::fail)
.putHeader(X_SFS_REMOTE_NODE_TOKEN, remoteNodeSecret)
.setTimeout(responseTimeout);
httpClientRequest.end();
return handler.map(httpClientResponse -> new HttpClientRequestAndResponse(httpClientRequest, httpClientResponse));
}))
.map(HttpClientRequestAndResponse::getResponse)
.flatMap(httpClientResponse ->
just(httpClientResponse)
.flatMap(new HttpClientResponseBodyBuffer())
.map(buffer -> {
int status = httpClientResponse.statusCode();
if (status >= 400) {
throw new HttpClientResponseException(httpClientResponse, buffer);
}
return true;
}));
}
@Override
public Observable<NodeWriteStreamBlob> createWriteStream(final String volumeId, final long length, final MessageDigestFactory... messageDigestFactories) {
final XNode _this = this;
return Defer.aVoid()
.flatMap(aVoid ->
nodes.connectFirstAvailable(
vertx,
hostAndPorts,
hostAndPort -> {
Escaper escaper = urlFragmentEscaper();
StringBuilder urlBuilder =
new StringBuilder("http://")
.append(hostAndPort.toString());
urlBuilder = urlBuilder.append("/_internal_node_data/blob?");
urlBuilder = urlBuilder.append(KEEP_ALIVE_TIMEOUT);
urlBuilder = urlBuilder.append('=');
urlBuilder = urlBuilder.append(responseTimeout / 2);
urlBuilder = urlBuilder.append('&');
urlBuilder = urlBuilder.append(VOLUME);
urlBuilder = urlBuilder.append('=');
urlBuilder = urlBuilder.append(escaper.escape(volumeId));
if (messageDigestFactories.length > 0) {
for (MessageDigestFactory instance : messageDigestFactories) {
urlBuilder = urlBuilder.append('&');
urlBuilder =
urlBuilder
.append(escaper.escape(format("%s%s", X_CONTENT_COMPUTED_DIGEST_PREFIX, instance.getValue())))
.append('=')
.append("true");
}
}
final String url = urlBuilder.toString();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("createWriteStream " + url);
}
ObservableFuture<HttpClientResponse> handler = RxHelper.observableFuture();
HttpClientRequest httpClientRequest =
httpClient
.putAbs(url, httpClientResponse -> {
httpClientResponse.pause();
handler.complete(httpClientResponse);
})
.exceptionHandler(handler::fail)
.putHeader(X_SFS_REMOTE_NODE_TOKEN, remoteNodeSecret)
.putHeader(CONTENT_LENGTH, valueOf(length))
.setTimeout(responseTimeout);
httpClientRequest.sendHead();
return handler.map(httpClientResponse -> new HttpClientRequestAndResponse(httpClientRequest, httpClientResponse));
}))
.map(httpClientRequestAndResponse -> {
HttpClientRequest httpClientRequest = httpClientRequestAndResponse.getRequest();
HttpClientResponse httpClientResponse = httpClientRequestAndResponse.getResponse();
if (HTTP_OK != httpClientResponse.statusCode()) {
httpClientResponse.resume();
throw new HttpClientResponseException(httpClientResponse, Buffer.buffer());
}
Observable<DigestBlob> oResponse =
Defer.just(httpClientResponse)
.flatMap(new HttpClientKeepAliveResponseBodyBuffer())
.map(new BufferToJsonObject())
.map(jsonObject -> {
Integer code = jsonObject.getInteger("code");
if (code == null || HTTP_OK != code) {
throw new HttpClientResponseException(httpClientResponse, jsonObject);
}
return jsonObject;
})
.map(jsonObject -> {
JsonObject blob = jsonObject.getJsonObject("blob");
return new DigestBlob(blob);
});
NodeWriteStreamBlob writeStreamBlob = new NodeWriteStreamBlob(_this) {
@Override
public Observable<DigestBlob> consume(ReadStream<Buffer> src) {
return combineSinglesDelayError(
pump(src, new HttpClientRequestEndableWriteStream(httpClientRequest)),
oResponse,
(aVoid1, digestBlob) -> digestBlob);
}
};
return writeStreamBlob;
});
}
@Override
public String toString() {
return "RemoteNode{" +
"hostAndPorts=" + hostAndPorts +
'}';
}
}