package org.webpieces.router.impl.compression;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.inject.Inject;
import org.webpieces.router.api.RouterConfig;
import org.webpieces.router.impl.StaticRoute;
import org.webpieces.router.impl.compression.MimeTypes.MimeTypeResult;
import org.webpieces.util.logging.Logger;
import org.webpieces.util.logging.LoggerFactory;
import org.webpieces.util.security.Security;
public class ProdCompressionCacheSetup implements CompressionCacheSetup {
private static final Logger log = LoggerFactory.getLogger(ProdCompressionCacheSetup.class);
private CompressionLookup lookup;
private RouterConfig config;
private MimeTypes mimeTypes;
private List<String> encodings = new ArrayList<>();
private FileUtil fileUtil;
private Map<String, FileMeta> pathToFileMeta = new HashMap<>();
@Inject
public ProdCompressionCacheSetup(CompressionLookup lookup, RouterConfig config, MimeTypes mimeTypes, FileUtil fileUtil) {
this.lookup = lookup;
this.config = config;
this.mimeTypes = mimeTypes;
encodings.add(config.getStartupCompression());
this.fileUtil = fileUtil;
}
public void setupCache(List<StaticRoute> staticRoutes) {
if(config.getCachedCompressedDirectory() == null) {
log.info("NOT setting up compressed cached directory so performance will not be as good");
return;
}
log.info("setting up compressed cache directories");
for(StaticRoute route : staticRoutes) {
createCache(route);
}
log.info("all cached directories setup");
}
private void createCache(StaticRoute route) {
File routeCache = route.getTargetCacheLocation();
createDirectory(routeCache);
File metaFile = new File(routeCache, "webpiecesMeta.properties");
Properties p = load(metaFile);
if(route.isFile()) {
File file = new File(route.getFileSystemPath());
log.info("setting up cache for file="+file);
File destination = new File(routeCache, file.getName()+".gz");
maybeAddFileToCache(p, file, destination, route.getFullPath());
} else {
File directory = new File(route.getFileSystemPath());
log.info("setting up cache for directory="+directory);
String urlPrefix = route.getFullPath();
transferAndCompress(p, directory, routeCache, urlPrefix);
}
route.setHashMeta(p);
store(metaFile, p);
}
private void store(File metaFile, Properties p) {
try {
FileOutputStream out = new FileOutputStream(metaFile);
p.store(out, "file hashes for next time. Single file format(key:urlPathOnly, value:hash), dir(key:urlPath+relativeFilePath, value:hash)");
} catch(IOException e) {
throw new RuntimeException(e);
}
}
private Properties load(File metaFile) {
try {
Properties p = new Properties();
if(!metaFile.exists())
return p;
p.load(new FileInputStream(metaFile));
return p;
} catch(IOException e) {
throw new RuntimeException(e);
}
}
private void transferAndCompress(Properties p, File directory, File destination, String urlPath) {
File[] files = directory.listFiles();
for(File f : files) {
if(f.isDirectory()) {
File newTarget = new File(destination, f.getName());
createDirectory(newTarget);
transferAndCompress(p, f, newTarget, urlPath+f.getName()+"/");
} else {
File newTarget = new File(destination, f.getName()+".gz");
String path = urlPath+f.getName();
maybeAddFileToCache(p, f, newTarget, path);
}
}
}
private void maybeAddFileToCache(Properties properties, File src, File destination, String urlPath) {
String name = src.getName();
int indexOf = name.lastIndexOf(".");
if(indexOf < 0) {
pathToFileMeta.put(urlPath, new FileMeta());
return; //do nothing
}
String extension = name.substring(indexOf+1);
MimeTypeResult mimeType = mimeTypes.extensionToContentType(extension, "application/octet-stream");
Compression compression = lookup.createCompressionStream(encodings, extension, mimeType);
if(compression == null) {
pathToFileMeta.put(urlPath, new FileMeta());
return;
}
//before we do the below, do a quick timestamp check to avoid reading in the files when not necessary
long lastModifiedSrc = src.lastModified();
long lastModified = destination.lastModified();
//if hash is not there, the user may have changed the url so need to recalculate new hashes for new keys
//There is a test for this...
String previousHash = properties.getProperty(urlPath);
if(lastModified > lastModifiedSrc && previousHash != null) {
log.info("timestamp later than src so skipping writing to="+destination);
pathToFileMeta.put(urlPath, new FileMeta(previousHash));
return; //no need to check anything as destination was written after this source file
}
try {
byte[] allData = fileUtil.readFileContents(urlPath, src);
String hash = Security.hash(allData);
if(previousHash != null) {
if(hash.equals(previousHash)) {
if(!destination.exists())
throw new IllegalStateException("Previously existing file is missing="+destination+" Your file cache was "
+ "corrupted. You will need to delete the whole cache directory");
log.info("Previous file is the same, no need to compress to="+destination+" hash="+hash);
pathToFileMeta.put(urlPath, new FileMeta(previousHash));
return;
}
}
//open, write, and close file with new data
writeFile(destination, compression, allData, urlPath, src);
//if file writing succeeded, set the hash
properties.setProperty(urlPath, hash);
FileMeta existing = pathToFileMeta.get(urlPath);
if(existing != null)
throw new IllegalStateException("this urlpath="+urlPath+" is referencing two files. hash1="+existing.getHash()+" hash2="+hash
+" You should search your logs for this hash");
pathToFileMeta.put(urlPath, new FileMeta(hash));
log.info("compressed "+src.length()+" bytes to="+destination.length()+" to file="+destination+" hash="+hash);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void writeFile(File destination, Compression compression, byte[] allData, String urlPath, File src) throws FileNotFoundException, IOException {
FileOutputStream out = new FileOutputStream(destination);
try(OutputStream compressionOut = compression.createCompressionStream(out))
{
fileUtil.writeFile(compressionOut, allData, urlPath, src);
}
}
private void createDirectory(File directoryToCreate) {
if(directoryToCreate.exists()) {
if(!directoryToCreate.isDirectory())
throw new RuntimeException("File="+directoryToCreate+" is NOT a directory and we need a directory there...perhaps"
+ " delete the cache and restart the server as something is corrupt");
return;
}
boolean success = directoryToCreate.mkdirs();
if(!success)
throw new RuntimeException("Could not create cache directory="+directoryToCreate);
}
@Override
public FileMeta relativeUrlToHash(String path) {
return pathToFileMeta.get(path);
}
}