io.micronaut.http.server.netty.body.SystemFileBodyWriter Maven / Gradle / Ivy
/*
* Copyright 2017-2023 original 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
*
* https://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 io.micronaut.http.server.netty.body;
import io.micronaut.buffer.netty.NettyByteBufferFactory;
import io.micronaut.core.annotation.Experimental;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.io.buffer.ByteBufferFactory;
import io.micronaut.core.type.Argument;
import io.micronaut.core.type.MutableHeaders;
import io.micronaut.http.ByteBodyHttpResponse;
import io.micronaut.http.ByteBodyHttpResponseWrapper;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpMethod;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.body.ResponseBodyWriter;
import io.micronaut.http.body.stream.InputStreamByteBody;
import io.micronaut.http.codec.CodecException;
import io.micronaut.http.exceptions.MessageBodyException;
import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration;
import io.micronaut.http.server.types.files.SystemFile;
import io.micronaut.scheduling.TaskExecutors;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.OptionalLong;
import java.util.concurrent.ExecutorService;
import static io.micronaut.http.HttpHeaders.CONTENT_RANGE;
/**
* Body writer for {@link SystemFile}s.
*
* @author Graeme Rocher
* @since 4.0.0
*/
@Singleton
@Experimental
@Internal
public final class SystemFileBodyWriter extends AbstractFileBodyWriter implements ResponseBodyWriter {
private static final String UNIT_BYTES = "bytes";
private final ExecutorService ioExecutor;
public SystemFileBodyWriter(NettyHttpServerConfiguration.FileTypeHandlerConfiguration configuration, @Named(TaskExecutors.BLOCKING) ExecutorService ioExecutor) {
super(configuration);
this.ioExecutor = ioExecutor;
}
@Override
public void writeTo(Argument type, MediaType mediaType, SystemFile file, MutableHeaders outgoingHeaders, OutputStream outputStream) throws CodecException {
throw new UnsupportedOperationException("Can only be used in a Netty context");
}
@Override
public ByteBodyHttpResponse> write(ByteBufferFactory, ?> bufferFactory, HttpRequest> request, @NonNull MutableHttpResponse httpResponse, @NonNull Argument type, @NonNull MediaType mediaType, SystemFile object) throws CodecException {
return write(request, httpResponse, object);
}
public ByteBodyHttpResponse> write(HttpRequest> request, MutableHttpResponse response, SystemFile systemFile) throws CodecException {
if (!systemFile.getFile().canRead()) {
throw new MessageBodyException("Could not find file");
}
if (handleIfModifiedAndHeaders(request, response, systemFile, response)) {
return notModified(response);
} else {
// Parse the range headers (if any), and determine the position and content length
// Only `bytes` ranges are supported. Only single ranges are supported. Invalid ranges fall back to returning the full response.
// See https://httpwg.org/specs/rfc9110.html#field.range
long fileLength = systemFile.getLength();
long position = 0;
long contentLength = fileLength;
if (fileLength > -1) {
String rangeHeader = request.getHeaders().get(HttpHeaders.RANGE);
if (rangeHeader != null
&& request.getMethod() == HttpMethod.GET // A server MUST ignore a Range header field received with a request method that is unrecognized or for which range handling is not defined.
&& rangeHeader.startsWith(UNIT_BYTES) // An origin server MUST ignore a Range header field that contains a range unit it does not understand.
&& response.status() == HttpStatus.OK // The Range header field is evaluated after evaluating the precondition header fields defined in Section 13.1, and only if the result in absence of the Range header field would be a 200 (OK) response.
) {
IntRange range = parseRangeHeader(rangeHeader, fileLength);
if (range != null // A server that supports range requests MAY ignore or reject a Range header field that contains an invalid ranges-specifier (Section 14.1.1)
&& range.firstPos < range.lastPos // A server that supports range requests MAY ignore a Range header field when the selected representation has no content (i.e., the selected representation's data is of zero length).
&& range.firstPos < fileLength
&& range.lastPos < fileLength
) {
position = range.firstPos;
contentLength = range.lastPos + 1 - range.firstPos;
response.status(HttpStatus.PARTIAL_CONTENT);
response.header(CONTENT_RANGE, "%s %d-%d/%d".formatted(UNIT_BYTES, range.firstPos, range.lastPos, fileLength));
}
}
response.header(HttpHeaders.ACCEPT_RANGES, UNIT_BYTES);
response.header(HttpHeaders.CONTENT_LENGTH, Long.toString(contentLength));
} else {
response.header(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
}
File file = systemFile.getFile();
InputStream is;
try {
is = new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new MessageBodyException("Could not find file", e);
}
@NonNull InputStream stream = new RangeInputStream(is, position, contentLength);
return ByteBodyHttpResponseWrapper.wrap(response, InputStreamByteBody.create(stream, OptionalLong.of(contentLength), ioExecutor, NettyByteBufferFactory.DEFAULT));
}
}
@Nullable
private static IntRange parseRangeHeader(String value, long contentLength) {
int equalsIdx = value.indexOf('=');
if (equalsIdx < 0 || equalsIdx == value.length() - 1) {
return null; // Malformed range
}
int minusIdx = value.indexOf('-', equalsIdx + 1);
if (minusIdx < 0) {
return null; // Malformed range
}
String from = value.substring(equalsIdx + 1, minusIdx).trim();
String to = value.substring(minusIdx + 1).trim();
try {
long fromPosition = from.isEmpty() ? 0 : Long.parseLong(from);
long toPosition = to.isEmpty() ? contentLength - 1 : Long.parseLong(to);
return new IntRange(fromPosition, toPosition);
} catch (NumberFormatException e) {
return null; // Malformed range
}
}
// See https://httpwg.org/specs/rfc9110.html#rule.int-range
private static class IntRange {
private final long firstPos;
private final long lastPos;
IntRange(long firstPos, long lastPos) {
this.firstPos = firstPos;
this.lastPos = lastPos;
}
}
private static final class RangeInputStream extends InputStream {
private final InputStream delegate;
private final long toSkip;
private long remainingLength;
private boolean skipped = false;
private boolean skipSuccess = false;
private RangeInputStream(InputStream delegate, long toSkip, long length) {
this.delegate = delegate;
this.toSkip = toSkip;
this.remainingLength = length;
if (toSkip == 0) {
skipped = true;
skipSuccess = true;
}
}
private boolean doSkip() throws IOException {
if (!skipped) {
skipped = true;
try {
delegate.skipNBytes(toSkip);
skipSuccess = true;
} catch (EOFException ignored) {
}
}
return skipSuccess;
}
@Override
public int read() throws IOException {
if (!doSkip()) {
return -1;
}
if (remainingLength <= 0) {
return -1;
}
int read = delegate.read();
if (read != -1) {
remainingLength--;
}
return read;
}
@Override
public int read(@NonNull byte[] b, int off, int len) throws IOException {
if (!doSkip()) {
return -1;
}
if (remainingLength <= 0) {
return -1;
}
if (len > remainingLength) {
len = (int) remainingLength;
}
int n = delegate.read(b, off, len);
if (n != -1) {
remainingLength -= n;
}
return n;
}
@Override
public void close() throws IOException {
delegate.close();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy