/*
 * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
 * which is available at https://www.apache.org/licenses/LICENSE-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
 */

package io.vertx.core.file.impl;

import io.netty.util.internal.PlatformDependent;
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.util.Enumeration;
import java.util.function.IntPredicate;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import static io.vertx.core.net.impl.URIDecoder.decodeURIComponent;

Sometimes the file resources of an application are bundled into jars, or are somewhere on the classpath but not available on the file system, e.g. in the case of a Vert.x webapp bundled as a fat jar. In this case we want the application to access the resource from the classpath as if it was on the file system. We can do this by looking for the file on the classpath, and if found, copying it to a temporary cache directory on disk and serving it from there. There is one cache dir per Vert.x instance and they are deleted on Vert.x shutdown.
Author:Tim Fox, Rob Worsnop
/** * Sometimes the file resources of an application are bundled into jars, or are somewhere on the classpath but not * available on the file system, e.g. in the case of a Vert.x webapp bundled as a fat jar. * * In this case we want the application to access the resource from the classpath as if it was on the file system. * * We can do this by looking for the file on the classpath, and if found, copying it to a temporary cache directory * on disk and serving it from there. * * There is one cache dir per Vert.x instance and they are deleted on Vert.x shutdown. * * @author <a href="http://tfox.org">Tim Fox</a> * @author <a href="https://github.com/rworsnop/">Rob Worsnop</a> */
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 final File cwd; private final boolean enableCaching; private final boolean closeCache; private final FileCache cache; public FileResolver() { this(new FileSystemOptions()); } public FileResolver(FileSystemOptions fileSystemOptions) { this( fileSystemOptions.isFileCachingEnabled(), fileSystemOptions.isClassPathResolvingEnabled() ? FileCache.setupCache(fileSystemOptions.getFileCacheDir()) : null, fileSystemOptions.isClassPathResolvingEnabled()); } public FileResolver(boolean enableCaching, FileCache cache, boolean closeCache) { this.enableCaching = enableCaching; this.cache = cache; this.closeCache = closeCache; String cwdOverride = System.getProperty("vertx.cwd"); if (cwdOverride != null) { cwd = new File(cwdOverride).getAbsoluteFile(); } else { cwd = null; } }
Close this file resolver, this is a blocking operation.
/** * Close this file resolver, this is a blocking operation. */
public void close() throws IOException { if (closeCache) { cache.close(); } } public File resolveFile(String fileName) { // First look for file with that name on disk File file = new File(fileName); if (cwd != null && !file.isAbsolute()) { file = new File(cwd, fileName); } if (this.cache == null) { return file; } // We need to synchronized here to avoid 2 different threads to copy the file to the cache directory and so // corrupting the content. synchronized (cache) { if (!file.exists()) { // Look for it in local file cache File cacheFile = cache.getFile(fileName); if (this.enableCaching && cacheFile.exists()) { return cacheFile; } // Look for file on classpath ClassLoader cl = getClassLoader(); //https://github.com/eclipse/vert.x/issues/2126 //Cache all elements in the parent directory if it exists //this is so that listing the directory after an individual file has //been read works. String parentFileName = file.getParent(); if (parentFileName != null) { if (NON_UNIX_FILE_SEP) { parentFileName = parentFileName.replace(FILE_SEP, "/"); } URL directoryContents = getValidClassLoaderResource(cl, parentFileName); if (directoryContents != null) { unpackUrlResource(directoryContents, parentFileName, cl, true); } } if (NON_UNIX_FILE_SEP) { fileName = fileName.replace(FILE_SEP, "/"); } URL url = getValidClassLoaderResource(cl, fileName); if (url != null) { return unpackUrlResource(url, fileName, cl, false); } } } return file; } private static boolean isValidWindowsCachePath(char c) { if (c < 32) { return false; } switch (c) { case '"': case '*': case ':': case '<': case '>': case '?': case '|': return false; default: return true; } } private static boolean isValidCachePath(String fileName) { if (PlatformDependent.isWindows()) { int len = fileName.length(); for (int i = 0;i < len;i++) { char c = fileName.charAt(i); if (!isValidWindowsCachePath(c)) { return false; } // Space only valid when it's not ending a name if (c == ' ' && (i + 1 == len || fileName.charAt(i + 1) == '/')) { return false; } } return true; } else { return fileName.indexOf('\u0000') == -1; } }
Get a class loader resource that can unpack to a valid cache path. Some valid entries are avoided purposely when we cannot create the corresponding file in the file cache.
/** * Get a class loader resource that can unpack to a valid cache path. * * Some valid entries are avoided purposely when we cannot create the corresponding file in the file cache. */
private static URL getValidClassLoaderResource(ClassLoader cl, String fileName) { URL resource = cl.getResource(fileName); if (resource != null && !isValidCachePath(fileName)) { return null; } return resource; } 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": // Apache Felix, Knopflerfish case "bundleentry": // Equinox case "bundleresource": // Equinox case "resource": // substratevm (graal native image) return unpackFromBundleURL(url, isDir); default: throw new IllegalStateException("Invalid url protocol: " + prot); } } private File unpackFromFileURL(URL url, String fileName, ClassLoader cl) { final File resource = new File(decodeURIComponent(url.getPath(), false)); boolean isDirectory = resource.isDirectory(); File cacheFile; try { cacheFile = cache.cache(fileName, resource, !enableCaching); } catch (IOException e) { throw new VertxException(e); } if (isDirectory) { String[] listing = resource.list(); if (listing != null) { for (String file: listing) { String subResource = fileName + "/" + file; URL url2 = getValidClassLoaderResource(cl, subResource); if (url2 == null) { throw new VertxException("Invalid resource: " + subResource); } unpackFromFileURL(url2, subResource, cl); } } } return cacheFile; } private 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); } Enumeration<? extends ZipEntry> entries = zip.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); String name = entry.getName(); if (name.startsWith(fileName)) { if (name.endsWith("/")) { // Directory cache.cacheDir(name); } else { try (InputStream is = zip.getInputStream(entry)) { cache.cacheFile(name, is, !enableCaching); } } } } } catch (IOException e) { throw new VertxException(e); } finally { closeQuietly(zip); } return cache.getFile(fileName); } private void closeQuietly(Closeable zip) { if (zip != null) { try { zip.close(); } catch (IOException e) { // Ignored. } } }
It is possible to determine if a resource from a bundle is a directory based on whether or not the ClassLoader returns null for a path (which does not already contain a trailing '/') *and* that path with an added trailing '/'
Params:
  • url – the url
Returns:if the bundle resource represented by the bundle URL is a directory
/** * It is possible to determine if a resource from a bundle is a directory based on whether or not the ClassLoader * returns null for a path (which does not already contain a trailing '/') *and* that path with an added trailing '/' * * @param url the url * @return if the bundle resource represented by the bundle URL is a directory */
private boolean isBundleUrlDirectory(URL url) { return url.toExternalForm().endsWith("/") || getValidClassLoaderResource(getClassLoader(), url.getPath().substring(1) + "/") != null; }
bundle:// urls are used by OSGi implementations to refer to a file contained in a bundle, or in a fragment. There is not much we can do to get the file from it, except reading it from the url. This method copies the files by reading it from the url.
Params:
  • url – the url
Returns:the extracted file
/** * bundle:// urls are used by OSGi implementations to refer to a file contained in a bundle, or in a fragment. There * is not much we can do to get the file from it, except reading it from the url. This method copies the files by * reading it from the url. * * @param url the url * @return the extracted file */
private File unpackFromBundleURL(URL url, boolean isDir) { String file = url.getHost() + File.separator + url.getFile(); try { if ((getClassLoader() != null && isBundleUrlDirectory(url)) || isDir) { // Directory cache.cacheDir(file); } else { try (InputStream is = url.openStream()) { cache.cacheFile(file, is, !enableCaching); } } } catch (IOException e) { throw new VertxException(e); } return cache.getFile(file); } private ClassLoader getClassLoader() { ClassLoader cl = Thread.currentThread().getContextClassLoader(); if (cl == null) { cl = getClass().getClassLoader(); } // when running on substratevm (graal) the access to class loaders // is very limited and might be only available from compile time // known classes. (Object is always known, so we do a final attempt // to get it here). if (cl == null) { cl = Object.class.getClassLoader(); } return cl; } }