package io.undertow.server.handlers.resource;
import io.undertow.UndertowLogger;
import io.undertow.io.IoCallback;
import io.undertow.io.Sender;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.DateUtils;
import io.undertow.util.ETag;
import io.undertow.util.MimeMappings;
import io.undertow.util.StatusCodes;
import org.xnio.IoUtils;
import io.undertow.connector.PooledByteBuffer;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class PathResource implements RangeAwareResource {
private final Path file;
private final String path;
private final ETag eTag;
private final PathResourceManager manager;
public PathResource(final Path file, final PathResourceManager manager, String path, ETag eTag) {
this.file = file;
this.path = path;
this.manager = manager;
this.eTag = eTag;
}
public PathResource(final Path file, final PathResourceManager manager, String path) {
this(file, manager, path, null);
}
@Override
public String getPath() {
return path;
}
@Override
public Date getLastModified() {
try {
return new Date(Files.getLastModifiedTime(file).toMillis());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public String getLastModifiedString() {
return DateUtils.toDateString(getLastModified());
}
@Override
public ETag getETag() {
return eTag;
}
@Override
public String getName() {
return file.getFileName().toString();
}
@Override
public boolean isDirectory() {
return Files.isDirectory(file);
}
@Override
public List<Resource> list() {
final List<Resource> resources = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(file)) {
for (Path child : stream) {
resources.add(new PathResource(child, manager, path));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return resources;
}
@Override
public String getContentType(final MimeMappings mimeMappings) {
final String fileName = file.getFileName().toString();
int index = fileName.lastIndexOf('.');
if (index != -1 && index != fileName.length() - 1) {
return mimeMappings.getMimeType(fileName.substring(index + 1));
}
return null;
}
@Override
public void serve(final Sender sender, final HttpServerExchange exchange, final IoCallback callback) {
serveImpl(sender, exchange, -1, -1, callback, false);
}
@Override
public void serveRange(final Sender sender, final HttpServerExchange exchange, final long start, final long end, final IoCallback callback) {
serveImpl(sender, exchange, start, end, callback, true);
}
private void serveImpl(final Sender sender, final HttpServerExchange exchange, final long start, final long end, final IoCallback callback, final boolean range) {
abstract class BaseFileTask implements Runnable {
protected volatile FileChannel fileChannel;
protected boolean openFile() {
try {
fileChannel = FileChannel.open(file, StandardOpenOption.READ);
if(range) {
fileChannel.position(start);
}
} catch (NoSuchFileException e) {
exchange.setStatusCode(StatusCodes.NOT_FOUND);
callback.onException(exchange, sender, e);
return false;
} catch (IOException e) {
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
callback.onException(exchange, sender, e);
return false;
}
return true;
}
}
class ServerTask extends BaseFileTask implements IoCallback {
private PooledByteBuffer pooled;
long remaining = end - start + 1;
@Override
public void run() {
if(range && remaining == 0) {
pooled.close();
pooled = null;
IoUtils.safeClose(fileChannel);
callback.onComplete(exchange, sender);
return;
}
if (fileChannel == null) {
if (!openFile()) {
return;
}
pooled = exchange.getConnection().getByteBufferPool().allocate();
}
if (pooled != null) {
ByteBuffer buffer = pooled.getBuffer();
try {
buffer.clear();
int res = fileChannel.read(buffer);
if (res == -1) {
pooled.close();
IoUtils.safeClose(fileChannel);
callback.onComplete(exchange, sender);
return;
}
buffer.flip();
if(range) {
if(buffer.remaining() > remaining) {
buffer.limit((int) (buffer.position() + remaining));
}
remaining -= buffer.remaining();
}
sender.send(buffer, this);
} catch (IOException e) {
onException(exchange, sender, e);
}
}
}
@Override
public void onComplete(final HttpServerExchange exchange, final Sender sender) {
if (exchange.isInIoThread()) {
exchange.dispatch(this);
} else {
run();
}
}
@Override
public void onException(final HttpServerExchange exchange, final Sender sender, final IOException exception) {
UndertowLogger.REQUEST_IO_LOGGER.ioException(exception);
if (pooled != null) {
pooled.close();
pooled = null;
}
IoUtils.safeClose(fileChannel);
if (!exchange.isResponseStarted()) {
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
}
callback.onException(exchange, sender, exception);
}
}
class TransferTask extends BaseFileTask {
@Override
public void run() {
if (!openFile()) {
return;
}
sender.transferFrom(fileChannel, new IoCallback() {
@Override
public void onComplete(HttpServerExchange exchange, Sender sender) {
try {
IoUtils.safeClose(fileChannel);
} finally {
callback.onComplete(exchange, sender);
}
}
@Override
public void onException(HttpServerExchange exchange, Sender sender, IOException exception) {
try {
IoUtils.safeClose(fileChannel);
} finally {
callback.onException(exchange, sender, exception);
}
}
});
}
}
BaseFileTask task;
try {
task = manager.getTransferMinSize() > Files.size(file) || range ? new ServerTask() : new TransferTask();
} catch (IOException e) {
throw new RuntimeException(e);
}
if (exchange.isInIoThread()) {
exchange.dispatch(task);
} else {
task.run();
}
}
@Override
public Long getContentLength() {
try {
return Files.size(file);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public String getCacheKey() {
return file.toString();
}
@Override
public File getFile() {
return file.toFile();
}
@Override
public Path getFilePath() {
return file;
}
@Override
public File getResourceManagerRoot() {
return manager.getBasePath().toFile();
}
@Override
public Path getResourceManagerRootPath() {
return manager.getBasePath();
}
@Override
public URL getUrl() {
try {
return file.toUri().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isRangeSupported() {
return true;
}
}