diff --git a/li/cil/oc/server/fs/FileSystem.scala b/li/cil/oc/server/fs/FileSystem.scala index 95853d1b5..2e8e25fb9 100644 --- a/li/cil/oc/server/fs/FileSystem.scala +++ b/li/cil/oc/server/fs/FileSystem.scala @@ -2,7 +2,7 @@ package li.cil.oc.server.fs import dan200.computer.api.{IWritableMount, IMount} import java.io -import java.util.zip.ZipFile +import java.net.URL import li.cil.oc.api.fs.Label import li.cil.oc.server.component import li.cil.oc.{Settings, api} @@ -10,33 +10,45 @@ import net.minecraftforge.common.DimensionManager object FileSystem extends api.detail.FileSystemAPI { override def fromClass(clazz: Class[_], domain: String, root: String): api.fs.FileSystem = { - val codeSource = clazz.getProtectionDomain.getCodeSource - if (codeSource == null) return null - val file = new io.File(codeSource.getLocation.toURI) - if (!file.exists || file.isDirectory) return null - val path = ("/assets/" + domain + "/" + (root.trim + "/")).replace("//", "/") - if (file.getName.endsWith(".class")) - new io.File(new io.File(file.getParent), path) match { + val innerPath = ("/assets/" + domain + "/" + (root.trim + "/")).replace("//", "/") + + val codeSource = clazz.getProtectionDomain.getCodeSource.getLocation.getPath + val (codeUrl, isArchive) = + if (codeSource.contains(".zip!") || codeSource.contains(".jar!")) + (codeSource.substring(0, codeSource.lastIndexOf('!')), true) + else + (codeSource, false) + + val file = try { + val url = new URL(codeUrl) + try { + new io.File(url.toURI) + } + catch { + case _: Throwable => new io.File(url.getPath) + } + } catch { + case _: Throwable => new io.File(codeSource) + } + + if (isArchive) { + ZipFileInputStreamFileSystem.fromFile(file, innerPath.substring(1)) + } + else { + if (!file.exists || file.isDirectory) return null + new io.File(new io.File(file.getParent), innerPath) match { case fsp if fsp.exists() && fsp.isDirectory => new ReadOnlyFileSystem(fsp) case _ => System.getProperty("java.class.path").split(System.getProperty("path.separator")). find(cp => { - val fsp = new io.File(new io.File(cp), path) + val fsp = new io.File(new io.File(cp), innerPath) fsp.exists() && fsp.isDirectory }) match { case None => null - case Some(dir) => new ReadOnlyFileSystem(new io.File(new io.File(dir), path)) + case Some(dir) => new ReadOnlyFileSystem(new io.File(new io.File(dir), innerPath)) } } - else { - val zip = new ZipFile(file) - val entry = zip.getEntry(path) - if (entry == null || !entry.isDirectory) { - zip.close() - return null - } - new ZipFileInputStreamFileSystem(zip, path) } } diff --git a/li/cil/oc/server/fs/ZipFileInputStreamFileSystem.scala b/li/cil/oc/server/fs/ZipFileInputStreamFileSystem.scala index dfe40cdf6..b21475878 100644 --- a/li/cil/oc/server/fs/ZipFileInputStreamFileSystem.scala +++ b/li/cil/oc/server/fs/ZipFileInputStreamFileSystem.scala @@ -1,24 +1,28 @@ package li.cil.oc.server.fs +import com.google.common.cache.CacheBuilder +import java.io +import java.io.ByteArrayInputStream +import java.util.concurrent.Callable +import java.util.logging.Level import java.util.zip.{ZipEntry, ZipFile} +import li.cil.oc.OpenComputers +import li.cil.oc.server.fs.ZipFileInputStreamFileSystem.{ArchiveFile, ArchiveDirectory} import scala.collection.mutable +import scala.language.postfixOps -// TODO we may want to read in the complete zip file (and keep a cache in the -// factory) to avoid a ton of open real file handles. -class ZipFileInputStreamFileSystem(val zip: ZipFile, val root: String) extends InputStreamFileSystem { - private val directories = mutable.Map.empty[ZipEntry, Array[String]] - - // ----------------------------------------------------------------------- // +class ZipFileInputStreamFileSystem(private val archive: ArchiveDirectory) extends InputStreamFileSystem { def spaceTotal = spaceUsed def spaceUsed = spaceUsed_ private lazy val spaceUsed_ = { - var size = 0L - val enum = zip.entries() - while (enum.hasMoreElements) size += enum.nextElement.getSize - size + def recurse(d: ArchiveDirectory): Long = d.children.foldLeft(0L)((acc, c) => acc + (c match { + case directory: ArchiveDirectory => recurse(directory) + case file: ArchiveFile => file.size + })) + recurse(archive) } // ----------------------------------------------------------------------- // @@ -26,48 +30,144 @@ class ZipFileInputStreamFileSystem(val zip: ZipFile, val root: String) extends I override def exists(path: String) = entry(path).isDefined override def size(path: String) = entry(path) match { - case Some(file) if !file.isDirectory => file.getSize + case Some(file) if !file.isDirectory => file.size case _ => 0L } override def isDirectory(path: String) = entry(path).exists(_.isDirectory) def lastModified(path: String) = entry(path) match { - case Some(file) => file.getTime + case Some(file) => file.lastModified case _ => 0L } override def list(path: String) = entry(path) match { - case Some(entry) if entry.isDirectory => entries(entry) + case Some(entry) if entry.isDirectory => entry.list() case _ => null } - override def close() { - super.close() - zip.close() - directories.clear() - } + // ----------------------------------------------------------------------- // + + override protected def openInputStream(path: String) = entry(path).map(_.openStream()) // ----------------------------------------------------------------------- // - override protected def openInputStream(path: String) = entry(path).map(entry => zip.getInputStream(entry)) - - // ----------------------------------------------------------------------- // - - private def entry(path: String) = Option(zip.getEntry((root + path.replace("\\", "/")).replace("//", "/"))) - - private def entries(path: ZipEntry) = directories.get(path).getOrElse { - val pathName = root + path.getName - val list = mutable.ArrayBuffer.empty[String] - val iterator = zip.entries - while (iterator.hasMoreElements) { - val child = iterator.nextElement - if (child.getName.startsWith(pathName)) { - list += child.getName.substring(pathName.length) - } - } - val children = list.toArray - directories += path -> children - children + private def entry(path: String) = { + val cleanPath = "/" + path.replace("\\", "/").replace("//", "/").stripPrefix("/").stripSuffix("/") + if (cleanPath == "/") Some(archive) + else archive.find(cleanPath.split("/")) } } + +object ZipFileInputStreamFileSystem { + private val cache = com.google.common.cache.CacheBuilder.newBuilder(). + weakValues(). + asInstanceOf[CacheBuilder[String, ArchiveDirectory]]. + build[String, ArchiveDirectory]() + + def fromFile(file: io.File, innerPath: String) = this.synchronized { + Option(cache.get(file.getPath, new Callable[ArchiveDirectory] { + def call = try { + val zip = new ZipFile(file.getPath) + val cleanedPath = innerPath.stripPrefix("/").stripSuffix("/") + "/" + val rootEntry = zip.getEntry(cleanedPath) + val result = if (rootEntry != null && rootEntry.isDirectory) { + val directories = mutable.Set.empty[ArchiveDirectory] + val files = mutable.Set.empty[ArchiveFile] + val iterator = zip.entries() + while (iterator.hasMoreElements) { + val entry = iterator.nextElement() + if (entry.getName.startsWith(cleanedPath)) { + if (entry.isDirectory) directories += new ArchiveDirectory(entry, cleanedPath) + else files += new ArchiveFile(zip, entry, cleanedPath) + } + } + var root: ArchiveDirectory = null + for (entry <- directories ++ files) { + if (entry.path.length > 0) { + val parent = entry.path.substring(0, entry.path.lastIndexOf('/') max 0) + directories.find(d => d.path == parent) match { + case Some(directory) => directory.children += entry + case _ => + } + } + else { + assert(entry.isInstanceOf[ArchiveDirectory]) + root = entry.asInstanceOf[ArchiveDirectory] + } + } + root + } + else null + zip.close() + result + } + catch { + case e: Throwable => + OpenComputers.log.log(Level.WARNING, "Failed creating ZIP file system.", e) + null + } + })) match { + case Some(archive) => new ZipFileInputStreamFileSystem(archive) + case _ => null + } + } + + abstract class Archive(entry: ZipEntry, root: String) { + val path = entry.getName.stripPrefix(root).stripSuffix("/") + + val name = path.substring(path.lastIndexOf('/') + 1) + + val lastModified = entry.getTime + + val isDirectory = entry.isDirectory + + def size: Int + + def list(): Array[String] + + def openStream(): io.InputStream + + def find(path: Iterable[String]): Option[Archive] + } + + private class ArchiveFile(zip: ZipFile, entry: ZipEntry, root: String) extends Archive(entry, root) { + val data = { + val in = zip.getInputStream(entry) + Iterator.continually(in.read).takeWhile(-1 !=).map(_.toByte).toArray + } + + val size = data.length + + def list() = null + + def openStream() = new ByteArrayInputStream(data) + + def find(path: Iterable[String]) = + if (path.size == 1 && path.head == name) Some(this) + else None + } + + private class ArchiveDirectory(entry: ZipEntry, root: String) extends Archive(entry, root) { + val children = mutable.Set.empty[Archive] + + val size = 0 + + def list() = children.map(c => c.name + (if (c.isDirectory) "/" else "")).toArray + + def openStream() = null + + def find(path: Iterable[String]) = + if (path.head == name) { + if (path.size == 1) Some(this) + else { + val subPath = path.drop(1) + children.map(_.find(subPath)).collectFirst { + case Some(a) => a + } + } + } + else None + } + +} \ No newline at end of file