package io.vertx.core.file.impl;
import io.vertx.core.VertxException;
import io.vertx.core.file.FileSystemOptions;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Enumeration;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import static io.vertx.core.net.impl.URIDecoder.*;
public class FileResolver {
public static final String DISABLE_FILE_CACHING_PROP_NAME = "vertx.disableFileCaching";
public static final String DISABLE_CP_RESOLVING_PROP_NAME = "vertx.disableFileCPResolving";
public static final String CACHE_DIR_BASE_PROP_NAME = "vertx.cacheDirBase";
private static final String FILE_SEP = System.getProperty("file.separator");
private static final boolean NON_UNIX_FILE_SEP = !FILE_SEP.equals("/");
private static final String JAR_URL_SEP = "!/";
private static final Pattern JAR_URL_SEP_PATTERN = Pattern.compile(JAR_URL_SEP);
private final File cwd;
private File cacheDir;
private Thread shutdownHook;
private final boolean enableCaching;
private final boolean enableCpResolving;
private final String fileCacheDir;
public FileResolver() {
this(new FileSystemOptions());
}
public FileResolver(boolean enableCaching) {
this(new FileSystemOptions().setFileCachingEnabled(enableCaching));
}
public FileResolver(FileSystemOptions fileSystemOptions) {
this.enableCaching = fileSystemOptions.isFileCachingEnabled();
this.enableCpResolving = fileSystemOptions.isClassPathResolvingEnabled();
this.fileCacheDir = fileSystemOptions.getFileCacheDir();
String cwdOverride = System.getProperty("vertx.cwd");
if (cwdOverride != null) {
cwd = new File(cwdOverride).getAbsoluteFile();
} else {
cwd = null;
}
if (this.enableCpResolving) {
setupCacheDir();
}
}
public void close() throws IOException {
synchronized (this) {
if (shutdownHook != null) {
try {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
} catch (IllegalStateException ignore) {
}
}
}
deleteCacheDir();
}
public File resolveFile(String fileName) {
File file = new File(fileName);
if (cwd != null && !file.isAbsolute()) {
file = new File(cwd, fileName);
}
if (!this.enableCpResolving) {
return file;
}
synchronized (this) {
if (!file.exists()) {
File cacheFile = new File(cacheDir, fileName);
if (this.enableCaching && cacheFile.exists()) {
return cacheFile;
}
ClassLoader cl = getClassLoader();
if (NON_UNIX_FILE_SEP) {
fileName = fileName.replace(FILE_SEP, "/");
}
String parentFileName = file.getParent();
if (parentFileName != null) {
URL directoryContents = cl.getResource(parentFileName);
if (directoryContents != null) {
unpackUrlResource(directoryContents, parentFileName, cl, true);
}
}
URL url = cl.getResource(fileName);
if (url != null) {
return unpackUrlResource(url, fileName, cl, false);
}
}
}
return file;
}
private File unpackUrlResource(URL url, String fileName, ClassLoader cl, boolean isDir) {
String prot = url.getProtocol();
switch (prot) {
case "file":
return unpackFromFileURL(url, fileName, cl);
case "jar":
return unpackFromJarURL(url, fileName, cl);
case "bundle":
case "bundleentry":
case "bundleresource":
case "resource":
return unpackFromBundleURL(url, isDir);
default:
throw new IllegalStateException("Invalid url protocol: " + prot);
}
}
private synchronized File unpackFromFileURL(URL url, String fileName, ClassLoader cl) {
final File resource = new File(decodeURIComponent(url.getPath(), false));
boolean isDirectory = resource.isDirectory();
File cacheFile = new File(cacheDir, fileName);
if (!isDirectory) {
cacheFile.getParentFile().mkdirs();
try {
if (this.enableCaching) {
Files.copy(resource.toPath(), cacheFile.toPath());
} else {
Files.copy(resource.toPath(), cacheFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
} catch (FileAlreadyExistsException ignore) {
} catch (IOException e) {
throw new VertxException(e);
}
} else {
cacheFile.mkdirs();
String[] listing = resource.list();
for (String file: listing) {
String subResource = fileName + "/" + file;
URL url2 = cl.getResource(subResource);
unpackFromFileURL(url2, subResource, cl);
}
}
return cacheFile;
}
private synchronized File unpackFromJarURL(URL url, String fileName, ClassLoader cl) {
ZipFile zip = null;
try {
String path = url.getPath();
int idx1 = path.lastIndexOf(".jar!");
if (idx1 == -1) {
idx1 = path.lastIndexOf(".zip!");
}
int idx2 = path.lastIndexOf(".jar!", idx1 - 1);
if (idx2 == -1) {
idx2 = path.lastIndexOf(".zip!", idx1 - 1);
}
if (idx2 == -1) {
File file = new File(decodeURIComponent(path.substring(5, idx1 + 4), false));
zip = new ZipFile(file);
} else {
String s = path.substring(idx2 + 6, idx1 + 4);
File file = resolveFile(s);
zip = new ZipFile(file);
}
String inJarPath = path.substring(idx1 + 6);
String[] parts = JAR_URL_SEP_PATTERN.split(inJarPath);
StringBuilder prefixBuilder = new StringBuilder();
for (int i = 0; i < parts.length - 1; i++) {
prefixBuilder.append(parts[i]).append("/");
}
String prefix = prefixBuilder.toString();
Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
if (name.startsWith(prefix.isEmpty() ? fileName : prefix + fileName)) {
File file = new File(cacheDir, prefix.isEmpty() ? name : name.substring(prefix.length()));
if (name.endsWith("/")) {
file.mkdirs();
} else {
file.getParentFile().mkdirs();
try (InputStream is = zip.getInputStream(entry)) {
if (this.enableCaching) {
Files.copy(is, file.toPath());
} else {
Files.copy(is, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
} catch (FileAlreadyExistsException ignore) {
}
}
}
}
} catch (IOException e) {
throw new VertxException(e);
} finally {
closeQuietly(zip);
}
return new File(cacheDir, fileName);
}
private void closeQuietly(Closeable zip) {
if (zip != null) {
try {
zip.close();
} catch (IOException e) {
}
}
}
private boolean isBundleUrlDirectory(URL url) {
return url.toExternalForm().endsWith("/") ||
getClassLoader().getResource(url.getPath().substring(1) + "/") != null;
}
private synchronized File unpackFromBundleURL(URL url, boolean isDir) {
try {
File file = new File(cacheDir, url.getHost() + File.separator + url.getFile());
file.getParentFile().mkdirs();
if ((getClassLoader() != null && isBundleUrlDirectory(url)) || isDir) {
file.mkdirs();
} else {
file.getParentFile().mkdirs();
try (InputStream is = url.openStream()) {
if (this.enableCaching) {
Files.copy(is, file.toPath());
} else {
Files.copy(is, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
} catch (FileAlreadyExistsException ignore) {
}
}
} catch (IOException e) {
throw new VertxException(e);
}
return new File(cacheDir, url.getHost() + File.separator + url.getFile());
}
private ClassLoader getClassLoader() {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
if (cl == null) {
cl = getClass().getClassLoader();
}
if (cl == null) {
cl = Object.class.getClassLoader();
}
return cl;
}
private void setupCacheDir() {
String cacheDirName = fileCacheDir + "/file-cache-" + UUID.randomUUID().toString();
cacheDir = new File(cacheDirName);
if (!cacheDir.mkdirs()) {
throw new IllegalStateException("Failed to create cache dir");
}
synchronized (this) {
shutdownHook = new Thread(() -> {
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
try {
deleteCacheDir();
} catch (IOException ignore) {
}
latch.countDown();
}).run();
try {
latch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
}
private void deleteCacheDir() throws IOException {
Path path;
synchronized (this) {
if (cacheDir == null || !cacheDir.exists()) {
return;
}
path = cacheDir.toPath();
cacheDir = null;
}
FileSystemImpl.delete(path, true);
}
}