package org.yamcs.web.rest.archive; import java.io.IOException; import java.util.HashSet; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yamcs.YamcsException; import org.yamcs.YamcsServer; import org.yamcs.api.MediaType; import org.yamcs.archive.IndexRequestListener; import org.yamcs.archive.IndexServer; import org.yamcs.protobuf.SchemaYamcs; import org.yamcs.protobuf.Yamcs.ArchiveRecord; import org.yamcs.protobuf.Yamcs.IndexRequest; import org.yamcs.protobuf.Yamcs.IndexResult; import org.yamcs.protobuf.Yamcs.NamedObjectId; import org.yamcs.web.BadRequestException; import org.yamcs.web.HttpException; import org.yamcs.web.HttpRequestHandler; import org.yamcs.web.HttpRequestHandler.ChunkedTransferStats; import org.yamcs.web.rest.RestHandler; import org.yamcs.web.rest.RestRequest; import org.yamcs.web.rest.RestRequest.IntervalResult; import org.yamcs.web.rest.Route; import com.fasterxml.jackson.core.JsonGenerator; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.handler.codec.http.LastHttpContent; import io.protostuff.JsonIOUtil; /** * Serves archive indexes through a web api. * * <p>These responses use chunked encoding with an unspecified content length, which enables * us to send large dumps without needing to determine a content length on the server. */ public class ArchiveIndexRestHandler extends RestHandler { private static final Logger log = LoggerFactory.getLogger(ArchiveIndexRestHandler.class); /** * indexes a combination of multiple indexes. If nothing is specified, sends all available */ @Route(path = "/api/archive/:instance/indexes", method = "GET") public void downloadIndexes(RestRequest req) throws HttpException { String instance = verifyInstance(req, req.getRouteParam("instance")); IndexServer indexServer = verifyIndexServer(req, instance); IndexRequest.Builder requestb = IndexRequest.newBuilder(); requestb.setInstance(instance); IntervalResult ir = req.scanForInterval(); if (ir.hasStart()) { requestb.setStart(ir.getStart()); } if (ir.hasStop()) { requestb.setStop(ir.getStop()); } if (req.hasQueryParameter("packetname")) { for (String names : req.getQueryParameterList("packetname")) { for (String name : names.split(",")) { requestb.addTmPacket(NamedObjectId.newBuilder().setName(name.trim())); } } } Set<String> filter = new HashSet<>(); if (req.hasQueryParameter("filter")) { for (String names : req.getQueryParameterList("filter")) { for (String name : names.split(",")) { filter.add(name.toLowerCase().trim()); } } } if (filter.isEmpty() && requestb.getTmPacketCount() == 0) { requestb.setSendAllTm(true); requestb.setSendAllPp(true); requestb.setSendAllCmd(true); requestb.setSendAllEvent(true); requestb.setSendCompletenessIndex(true); } else { requestb.setSendAllTm(filter.contains("tm") && requestb.getTmPacketCount() == 0); requestb.setSendAllPp(filter.contains("pp")); requestb.setSendAllCmd(filter.contains("commands")); requestb.setSendAllEvent(filter.contains("events")); requestb.setSendCompletenessIndex(filter.contains("completeness")); } try { indexServer.submitIndexRequest(requestb.build(), new ChunkedIndexResultProtobufEncoder(req, false)); } catch (YamcsException e) { log.error("Error while processing index request", e); } } @Route(path = "/api/archive/:instance/indexes/packets", method = "GET") public void downloadPacketIndex(RestRequest req) throws HttpException { String instance = verifyInstance(req, req.getRouteParam("instance")); IndexServer indexServer = verifyIndexServer(req, instance); IndexRequest.Builder requestb = IndexRequest.newBuilder(); requestb.setInstance(instance); IntervalResult ir = req.scanForInterval(); if (ir.hasStart()) { requestb.setStart(ir.getStart()); } if (ir.hasStop()) { requestb.setStop(ir.getStop()); } if (req.hasQueryParameter("name")) { for (String names : req.getQueryParameterList("name")) { for (String name : names.split(",")) { requestb.addTmPacket(NamedObjectId.newBuilder().setName(name.trim())); } } } if (requestb.getTmPacketCount() == 0) { requestb.setSendAllTm(true); } try { indexServer.submitIndexRequest(requestb.build(), new ChunkedIndexResultProtobufEncoder(req, true)); } catch (YamcsException e) { log.error("Error while processing index request", e); } } @Route(path = "/api/archive/:instance/indexes/pp", method = "GET") public void downloadPpIndex(RestRequest req) throws HttpException { String instance = verifyInstance(req, req.getRouteParam("instance")); IndexServer indexServer = verifyIndexServer(req, instance); IndexRequest.Builder requestb = IndexRequest.newBuilder(); requestb.setInstance(instance); IntervalResult ir = req.scanForInterval(); if (ir.hasStart()) { requestb.setStart(ir.getStart()); } if (ir.hasStop()) { requestb.setStop(ir.getStop()); } requestb.setSendAllPp(true); try { indexServer.submitIndexRequest(requestb.build(), new ChunkedIndexResultProtobufEncoder(req, true)); } catch (YamcsException e) { log.error("Error while processing index request", e); } } @Route(path = "/api/archive/:instance/indexes/commands", method = "GET") public void downloadCommandHistoryIndex(RestRequest req) throws HttpException { String instance = verifyInstance(req, req.getRouteParam("instance")); IndexServer indexServer = verifyIndexServer(req, instance); IndexRequest.Builder requestb = IndexRequest.newBuilder(); requestb.setInstance(instance); IntervalResult ir = req.scanForInterval(); if (ir.hasStart()) { requestb.setStart(ir.getStart()); } if (ir.hasStop()) { requestb.setStop(ir.getStop()); } requestb.setSendAllCmd(true); try { indexServer.submitIndexRequest(requestb.build(), new ChunkedIndexResultProtobufEncoder(req, true)); } catch (YamcsException e) { log.error("Error while processing index request", e); } } @Route(path = "/api/archive/:instance/indexes/events", method = "GET") public void downloadEventIndex(RestRequest req) throws HttpException { String instance = verifyInstance(req, req.getRouteParam("instance")); IndexServer indexServer = verifyIndexServer(req, instance); IndexRequest.Builder requestb = IndexRequest.newBuilder(); requestb.setInstance(instance); IntervalResult ir = req.scanForInterval(); if (ir.hasStart()) { requestb.setStart(ir.getStart()); } if (ir.hasStop()) { requestb.setStop(ir.getStop()); } requestb.setSendAllEvent(true); try { indexServer.submitIndexRequest(requestb.build(), new ChunkedIndexResultProtobufEncoder(req, true)); } catch (YamcsException e) { log.error("Error while processing index request", e); } } @Route(path = "/api/archive/:instance/indexes/completeness", method = "GET") public void downloadCompletenessIndex(RestRequest req) throws HttpException { String instance = verifyInstance(req, req.getRouteParam("instance")); IndexServer indexServer = verifyIndexServer(req, instance); IndexRequest.Builder requestb = IndexRequest.newBuilder(); requestb.setInstance(instance); IntervalResult ir = req.scanForInterval(); if (ir.hasStart()) { requestb.setStart(ir.getStart()); } if (ir.hasStop()) { requestb.setStop(ir.getStop()); } requestb.setSendCompletenessIndex(true); try { indexServer.submitIndexRequest(requestb.build(), new ChunkedIndexResultProtobufEncoder(req, true)); } catch (YamcsException e) { log.error("Error while processing index request", e); } } private static class ChunkedIndexResultProtobufEncoder implements IndexRequestListener { private static final Logger log = LoggerFactory.getLogger(ChunkedIndexResultProtobufEncoder.class); private static final int CHUNK_TRESHOLD = 8096; private final RestRequest req; private final MediaType contentType; private final boolean unpack; private ByteBuf buf; private ByteBufOutputStream bufOut; private ChannelFuture lastChannelFuture; private ChunkedTransferStats stats; private boolean first; // If unpack, the result will be a stream of Archive Records, otherwise IndexResult public ChunkedIndexResultProtobufEncoder(RestRequest req, boolean unpack) { this.req = req; this.unpack = unpack; contentType = req.deriveTargetContentType(); resetBuffer(); first = true; } private void resetBuffer() { buf = req.getChannelHandlerContext().alloc().buffer(); bufOut = new ByteBufOutputStream(buf); } @Override public void processData(IndexResult indexResult) throws Exception { if (first) { lastChannelFuture = HttpRequestHandler.startChunkedTransfer(req.getChannelHandlerContext(), req.getHttpRequest(), contentType, null); stats = req.getChannelHandlerContext().attr(HttpRequestHandler.CTX_CHUNK_STATS).get(); first = false; } if (unpack) { for (ArchiveRecord rec : indexResult.getRecordsList()) { bufferArchiveRecord(rec); } } else { bufferIndexResult(indexResult); } if (buf.readableBytes() >= CHUNK_TRESHOLD) { bufOut.close(); writeChunk(); resetBuffer(); } } private void bufferArchiveRecord(ArchiveRecord msg) throws IOException { if (MediaType.PROTOBUF.equals(contentType)) { msg.writeDelimitedTo(bufOut); } else { JsonGenerator generator = req.createJsonGenerator(bufOut); JsonIOUtil.writeTo(generator, msg, SchemaYamcs.ArchiveRecord.WRITE, false); generator.close(); } } private void bufferIndexResult(IndexResult msg) throws IOException { if (MediaType.PROTOBUF.equals(contentType)) { msg.writeDelimitedTo(bufOut); } else { JsonGenerator generator = req.createJsonGenerator(bufOut); JsonIOUtil.writeTo(generator, msg, SchemaYamcs.IndexResult.WRITE, false); generator.close(); } } @Override public void finished(boolean success) { if (first) { //empty result RestHandler.completeOK(req); } else { try { bufOut.close(); if (buf.readableBytes() > 0) { writeChunk(); } req.getChannelHandlerContext().writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) .addListener(ChannelFutureListener.CLOSE) .addListener(l-> req.getCompletableFuture().complete(null)); } catch (IOException e) { log.error("Could not write final chunk of data", e); req.getChannelHandlerContext().close(); } } } private void writeChunk() throws IOException { int txSize = buf.readableBytes(); req.addTransferredSize(txSize); stats.totalBytes += buf.readableBytes(); stats.chunkCount++; lastChannelFuture = HttpRequestHandler.writeChunk(req.getChannelHandlerContext(), buf); } } private IndexServer verifyIndexServer(RestRequest req, String instance) throws HttpException { verifyInstance(req, instance); IndexServer indexServer = YamcsServer.getService(instance, IndexServer.class); if (indexServer == null) { throw new BadRequestException("Index service not enabled for instance '" + instance + "'"); } else { return indexServer; } } }