-
-
Save liluxdev/02dee4eae6630eb7c877eb9bb503222b to your computer and use it in GitHub Desktop.
Simple static fileserver for vert.x - uses cached SHA1 hash etags for client caching to ensure that cached files remain correct after redeploys and between servers.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.rc; | |
import java.nio.file.FileSystems; | |
import java.nio.file.Path; | |
import java.util.Map; | |
import java.util.concurrent.ConcurrentHashMap; | |
import org.vertx.java.core.AsyncResult; | |
import org.vertx.java.core.Handler; | |
import org.vertx.java.core.Vertx; | |
import org.vertx.java.core.file.AsyncFile; | |
import org.vertx.java.core.file.FileProps; | |
import org.vertx.java.core.file.FileSystem; | |
import org.vertx.java.core.http.HttpServerRequest; | |
import org.vertx.java.core.http.impl.MimeMapping; | |
import org.vertx.java.core.logging.Logger; | |
/** | |
* | |
* We use a different sort of caching from most other web servers - the sha1 hash of the file contents is used as an ETAG. Most other | |
* servers will use a hash of the last modified time and file size. This is a problem in a multi server environment as files on different | |
* servers may have different modified dates based on server deployment times, etc. By using the sha1 we ensure that files with the same | |
* ETAG will be cached across all servers. | |
* | |
* Calculating the SHA1 is expensive, so we do it in-line with transmitting the file, and we cache the result and only re-check it when the | |
* file modification date we have cached changes. | |
* | |
*/ | |
public class StaticFileHandler { | |
private static final String DEFAULT_FILE = "index.html"; | |
final Path staticPath; | |
final String staticPathStr; | |
final Vertx vertx; | |
final Logger logger; | |
private static final Map<String, FileCacheInfo> cacheMap = new ConcurrentHashMap<>(); | |
public StaticFileHandler(Vertx vertx, Logger logger, String staticPathStr) { | |
this.vertx = vertx; | |
this.staticPathStr = staticPathStr; | |
this.logger = logger; | |
staticPath = FileSystems.getDefault().getPath(staticPathStr).normalize(); | |
} | |
public void handle(final HttpServerRequest request) { | |
if (!"GET".equals(request.method())) { | |
sendNotFound(request); | |
return; | |
} | |
Path requestPath = FileSystems.getDefault().getPath(staticPathStr, request.path()).normalize(); | |
// Ensure path request is inside statics path | |
if (!requestPath.startsWith(staticPath)) { | |
logger.info("Attempt to access outside of path"); | |
sendNotFound(request); | |
return; | |
} | |
handleRequestString(request, requestPath.toString()); | |
} | |
private void handleRequestString(final HttpServerRequest request, final String requestStr) { | |
final FileSystem fileSystem = vertx.fileSystem(); | |
fileSystem.exists(requestStr, new Handler<AsyncResult<Boolean>>() { | |
@Override | |
public void handle(AsyncResult<Boolean> exists) { | |
if (!exists.result()) { | |
sendNotFound(request); | |
return; | |
} | |
fileSystem.props(requestStr, new Handler<AsyncResult<FileProps>>() { | |
@Override | |
public void handle(AsyncResult<FileProps> event) { | |
FileProps props = event.result(); | |
testFileAndSend(request, requestStr, props); | |
} | |
}); | |
} | |
}); | |
} | |
private void testFileAndSend(final HttpServerRequest request, final String requestStr, FileProps props) { | |
if (props.isDirectory()) { | |
handleRequestString(request, requestStr + "/" + DEFAULT_FILE); | |
return; | |
} | |
if (!props.isRegularFile()) { | |
sendNotFound(request); | |
return; | |
} | |
FileCacheInfo cacheInfo = cacheMap.get(requestStr); | |
String etag = request.headers().get("If-None-Match"); | |
if (cacheInfo != null && cacheInfo.lastModifiedTime == props.lastModifiedTime().getTime()) { | |
// Last modified time has not changed for this file, we don't need to recalculate the sha1 of the contents | |
if (etag != null && etag.equals(cacheInfo.etagsha1)) { | |
sendNotChanged(request); | |
} else { | |
sendFile(request, requestStr, cacheInfo); | |
} | |
} else { | |
// Last modified time has changed - we need to send the file and also calculate sha1 of the contents | |
sendFileAndCache(request, requestStr, props); | |
} | |
} | |
private void sendFileAndCache(final HttpServerRequest request, final String requestStr, final FileProps props) { | |
request.response().putHeader("Content-Length", Long.toString(props.size())); | |
int li = requestStr.lastIndexOf('.'); | |
if (li != -1 && li != requestStr.length() - 1) { | |
String ext = requestStr.substring(li + 1, requestStr.length()); | |
String contentType = MimeMapping.getMimeTypeForExtension(ext); | |
if (contentType != null) { | |
request.response().putHeader("Content-Type", contentType); | |
} | |
} | |
vertx.fileSystem().open(requestStr, null, true, false, false, new Handler<AsyncResult<AsyncFile>>() { | |
@Override | |
public void handle(AsyncResult<AsyncFile> event) { | |
final AsyncFile asyncFile = event.result(); | |
final Sha1PumpToHttp pump = new Sha1PumpToHttp(asyncFile, request.response()); | |
asyncFile.endHandler(new Handler<Void>() { | |
@Override | |
public void handle(Void event) { | |
FileCacheInfo fileCacheInfo = new FileCacheInfo(props.lastModifiedTime().getTime(), pump.getSHA1Hash()); | |
cacheMap.put(requestStr, fileCacheInfo); | |
asyncFile.close(); | |
// Unfortunately we can't send the new ETAG to this request as the ETAG must be sent in the header, but the next request will get it. | |
request.response().end(); | |
} | |
}); | |
pump.start(); | |
} | |
}); | |
} | |
private void sendFile(HttpServerRequest request, String requestStr, FileCacheInfo cacheInfo) { | |
request.response().putHeader("ETag", cacheInfo.etagsha1); | |
request.response().sendFile(requestStr); | |
} | |
private void sendNotFound(HttpServerRequest request) { | |
request.response().setStatusCode(404).end("Not found"); | |
} | |
private void sendNotChanged(HttpServerRequest request) { | |
request.response().setStatusCode(304).end(); | |
} | |
private static class FileCacheInfo { | |
final long lastModifiedTime; | |
final String etagsha1; | |
public FileCacheInfo(long lastModifiedTime, String etagsha1) { | |
this.lastModifiedTime = lastModifiedTime; | |
this.etagsha1 = etagsha1; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.rc; | |
import java.security.MessageDigest; | |
import java.security.NoSuchAlgorithmException; | |
import org.vertx.java.core.Handler; | |
import org.vertx.java.core.buffer.Buffer; | |
import org.vertx.java.core.streams.ReadStream; | |
import org.vertx.java.core.streams.WriteStream; | |
/** | |
* A copy of Pump that also creates an SHA1 hash of the stream as it passes through. Can be fetched once all data has been pushed through | |
* with getSHA1Hash() | |
*/ | |
public class Sha1PumpToHttp { | |
private final ReadStream<?> readStream; | |
private final WriteStream<?> writeStream; | |
private int pumped; | |
private MessageDigest md; | |
/** | |
* Start the Pump. The Pump can be started and stopped multiple times. | |
*/ | |
public Sha1PumpToHttp start() { | |
readStream.dataHandler(dataHandler); | |
return this; | |
} | |
/** | |
* Stop the Pump. The Pump can be started and stopped multiple times. | |
*/ | |
public Sha1PumpToHttp stop() { | |
writeStream.drainHandler(null); | |
readStream.dataHandler(null); | |
return this; | |
} | |
/** | |
* Return the total number of bytes pumped by this pump. | |
*/ | |
public int bytesPumped() { | |
return pumped; | |
} | |
/** | |
* Return a hex string of the sha1 hash of the data that passed through | |
*/ | |
public String getSHA1Hash() { | |
return convertToHex(md.digest()); | |
} | |
private String convertToHex(byte[] data) { | |
// Create Hex String | |
StringBuffer hexString = new StringBuffer(); | |
for (int i = 0; i < data.length; i++) { | |
String h = Integer.toHexString(0xFF & data[i]); | |
while (h.length() < 2) | |
h = "0" + h; | |
hexString.append(h); | |
} | |
return hexString.toString(); | |
} | |
private final Handler<Void> drainHandler = new Handler<Void>() { | |
@Override | |
public void handle(Void v) { | |
readStream.resume(); | |
} | |
}; | |
private final Handler<Buffer> dataHandler = new Handler<Buffer>() { | |
@Override | |
public void handle(Buffer buffer) { | |
md.update(buffer.getBytes()); | |
writeStream.write(buffer); | |
pumped += buffer.length(); | |
if (writeStream.writeQueueFull()) { | |
readStream.pause(); | |
writeStream.drainHandler(drainHandler); | |
} | |
} | |
}; | |
public Sha1PumpToHttp(ReadStream<?> rs, WriteStream<?> ws) { | |
readStream = rs; | |
writeStream = ws; | |
try { | |
md = MessageDigest.getInstance("SHA-1"); | |
} catch (NoSuchAlgorithmException e) { | |
throw new RuntimeException(e); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment