net.java.truevfs.kernel.impl.ArchiveFileSystem.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of truevfs-kernel-impl Show documentation
Show all versions of truevfs-kernel-impl Show documentation
Implements the API for accessing the federated virtual file system space.
This reference implementation can get overridden by providing another
file system manager factory implementation with a higher priority on
the class path.
/*
* Copyright (C) 2005-2012 Schlichtherle IT Services.
* All rights reserved. Use is subject to license terms.
*/
package net.java.truevfs.kernel.impl
import net.java.truecommons.shed._
import net.java.truecommons.shed.HashMaps._
import net.java.truecommons.shed.Paths._
import java.nio.file._
import java.util.Locale
import javax.annotation.concurrent._
import net.java.truevfs.kernel.spec._
import net.java.truevfs.kernel.spec.FsAccessOption._
import net.java.truevfs.kernel.spec.FsAccessOptions._
import net.java.truevfs.kernel.spec.FsNodeName._
import net.java.truevfs.kernel.spec.cio._
import net.java.truevfs.kernel.spec.cio.Entry._
import net.java.truevfs.kernel.spec.cio.Entry.Access._
import net.java.truevfs.kernel.spec.cio.Entry.Type._
import scala.annotation._
import ArchiveFileSystem._
/** A read/write virtual file system for archive entries.
*
* @tparam E the type of the archive entries.
* @author Christian Schlichtherle
*/
@NotThreadSafe
private class ArchiveFileSystem[E <: FsArchiveEntry] private(
driver: FsArchiveDriver[E],
master: EntryTable[E])
extends Iterable[FsCovariantNode[E]] {
private val splitter = new Splitter
/** Whether or not this file system has been modified. */
private var touched: Boolean = _
private var _touchListener: Option[TouchListener] = None
def this(driver: FsArchiveDriver[E]) {
this(driver, new EntryTable(OVERHEAD_SIZE))
val root = newEntry(RootPath, DIRECTORY, None)
val time = System.currentTimeMillis()
for (access <- ALL_ACCESS)
root.setTime(access, time)
master.add(RootPath, root)
touched = true
}
def this(driver: FsArchiveDriver[E], archive: Container[E], rootTemplate: Option[Entry]) {
// Allocate some extra capacity to create missing parent directories.
this(driver, new EntryTable(archive.size + OVERHEAD_SIZE))
// Load entries from source archive.
var paths = List[String]()
val normalizer = new PathNormalizer(SEPARATOR_CHAR)
for (ae <- archive) {
val path = cutTrailingSeparators(
normalizer.normalize(
ae.getName.replace('\\', SEPARATOR_CHAR)), // fix illegal Windoze file name separators
SEPARATOR_CHAR)
master.add(path, ae)
if (!path.startsWith(SEPARATOR)
&& !(".." + SEPARATOR).startsWith(path.substring(0, math.min(3, path.length))))
paths ::= path
}
// Setup root file system entry, potentially replacing its previous
// mapping from the source archive.
master.add(RootPath, newEntry(RootPath, DIRECTORY, rootTemplate))
// Now perform a file system check to create missing parent directories
// and populate directories with their members - this must be done
// separately!
for (path <- paths) fix(path)
}
/** Called from a constructor in order to fix the parent directories of the
* file system entry identified by `name`, ensuring that all parent
* directories of the file system entry exist and that they contain the
* respective member entry.
* If a parent directory does not exist, it is created using an unkown time
* as the last modification time - this is defined to be a
* ''ghost directory''.
* If a parent directory does exist, the respective member entry is added
*
* @param name the entry name.
*/
@tailrec
private def fix(name: String) {
// When recursing into this method, it may be called with the root
// directory as its parameter, so we may NOT skip the following test.
if (!isRoot(name)) {
splitter.split(name)
val pp = splitter.getParentPath
val mn = splitter.getMemberName
val pce = master.get(pp) match {
case Some(pce) if pce.isType(DIRECTORY) => pce
case _ => master.add(pp, newEntry(pp, DIRECTORY, None))
}
pce.add(mn)
fix(pp)
}
}
override def size = master.size
def iterator = master.iterator
/** Returns a covariant file system entry or `None` if no file system
* entry exists for the given name.
* Modifying the returned object graph is either not supported (i.e. throws
* an [[java.lang.UnsupportedOperationException]] or does not show any effect
* on this file system.
*
* @param name the name of the file system entry to look up.
* @return A covariant file system entry or `None` if no file system
* entry exists for the given name.
*/
def node(options: AccessOptions, name: FsNodeName) = {
master.get(name.getPath) match {
case Some(ce) => Some(ce.clone(driver))
case None => None
}
}
def checkAccess(options: AccessOptions, name: FsNodeName, types: BitField[Access]) {
if (master.get(name.getPath).isEmpty)
throw new NoSuchFileException(name.toString)
}
def setReadOnly(name: FsNodeName) {
throw new FileSystemException(name.toString, null,
"Cannot set read-only state!")
}
def setTime(options: AccessOptions, name: FsNodeName, times: Map[Access, Long]) = {
val ce = master.get(name.getPath) match {
case Some(ce) => ce
case _ => throw new NoSuchFileException(name.toString)
}
// HC SVNT DRACONES!
touch(options)
val ae = ce.getEntry
var ok = true
for ((access, value) <- times)
ok &= 0 <= value && ae.setTime(access, value)
ok
}
def setTime(options: AccessOptions, name: FsNodeName, types: BitField[Access], value: Long) = {
if (0 > value)
throw new IllegalArgumentException(name.toString
+ " (negative access time)")
val ce = master.get(name.getPath) match {
case Some(ce) => ce
case _ => throw new NoSuchFileException(name.toString)
}
// HC SVNT DRACONES!
touch(options)
val ae = ce.getEntry
var ok = true
for (tµpe <- types)
ok &= ae.setTime(tµpe, value)
ok
}
/** Begins a ''transaction'' to create or replace and finally link a
* chain of one or more archive entries for the given `name` into
* this archive file system.
*
* To commit the transaction, you need to call
* `commit` on the returned
* [[net.java.truevfs.kernel.impl.ArchiveFileSystem.Make]] object,
* which will mark this archive file system as touched and set the last
* modification time of the created and linked archive file system entries
* to the system's current time at the moment of the call to this method.
*
* @param name the archive file system entry name.
* @param tµpe the type of the archive file system entry to create.
* @param options if `CREATE_PARENTS` is set, any missing parent
* directories will be created and linked into this file
* system with its last modification time set to the system's
* current time.
* @param template if not `None`, then the archive file system entry
* at the end of the chain shall inherit as much properties from
* this entry as possible - with the exception of its name and type.
* @throws IOException on any I/O error.
* @return A new archive file system operation on a chain of one or more
* archive file system entries for the given path name which will
* be linked into this archive file system upon a call to the
* `commit` method of the returned
* [[net.java.truevfs.kernel.impl.ArchiveFileSystem.Make]]
* object.
*/
def make(options: AccessOptions, name: FsNodeName, tµpe: Type, template: Option[Entry]) = {
require(null ne tµpe)
if (FILE.ne(tµpe) && DIRECTORY.ne(tµpe)) // TODO: Add support for other types.
throw new FileSystemException(name.toString, null,
"Can only create file or directory entries, but not a " + typeName(tµpe) + " entry!")
val path = name.getPath
master.get(path).foreach { ce =>
if (!ce.isType(FILE))
throw new FileAlreadyExistsException(name.toString, null,
"Cannot replace a " + typeName(ce) + " entry!")
if (FILE ne tµpe)
throw new FileAlreadyExistsException(name.toString, null,
"Can only replace a file entry with a file entry, but not a " + typeName(tµpe) + " entry!")
if (options.get(EXCLUSIVE))
throw new FileAlreadyExistsException(name.toString)
}
val t = template match {
case Some(ce: FsCovariantNode[_]) => Some(ce.get(tµpe))
case x => x
}
new Make(options, path, tµpe, t)
}
/** Represents a `make` transaction.
* The transaction get committed by calling `commit`.
* The state of the archive file system will not change until this method
* gets called.
* The head of the chain of covariant file system entries to commit can get
* obtained by calling `head`.
*
* TODO: The current implementation yields a potential issue: The state of
* the file system may get altered between the construction of this
* transaction and the call to its `commit` method.
* However, the change may render this operation illegal and so the file
* system may get corrupted upon a call to `commit`.
* To avoid this, the caller must not allow concurrent changes to this
* archive file system.
*/
final class Make(options: AccessOptions, path: String, tµpe: Type, template: Option[Entry]) {
private var time: Long = UNKNOWN
private val segments = newSegments(path, tµpe, template)
private def newSegments(path: String, tµpe: Type, template: Option[Entry]): List[Segment[E]] = {
splitter.split(path)
val pp = splitter.getParentPath // may equal ROOT_PATH
val mn = splitter.getMemberName
// Lookup parent entry, creating it if necessary and allowed.
master.get(pp) match {
case Some(pce) =>
if (!pce.isType(DIRECTORY))
throw new NotDirectoryException(path)
var segments = List[Segment[E]]()
segments ::= Segment(None, pce)
val mce = new FsCovariantNode[E](path)
mce.put(tµpe, newEntry(options, path, tµpe, template))
segments ::= Segment(Some(mn), mce)
segments
case _ =>
if (options.get(CREATE_PARENTS)) {
var segments = newSegments(pp, DIRECTORY, None)
val mce = new FsCovariantNode[E](path)
mce.put(tµpe, newEntry(options, path, tµpe, template))
segments ::= Segment(Some(mn), mce)
segments
} else {
throw new NoSuchFileException(path, null,
"Missing parent directory entry!")
}
}
}
def commit() {
touch(options)
val size = commit(segments)
assert(2 <= size)
val mae = segments.head.entry.getEntry
if (UNKNOWN == mae.getTime(WRITE))
mae.setTime(WRITE, getTimeMillis)
}
private def commit(segments: List[Segment[E]]): Int = {
segments match {
case Segment(mn, mce) :: parentSegments =>
val parentSize = commit(parentSegments)
if (0 < parentSize) {
val pce = parentSegments.head.entry
val pae = pce.get(DIRECTORY)
val mae = mce.getEntry
master.add(mce.getName, mae)
if (master.get(pce.getName).get.add(mn.get)
&& UNKNOWN != pae.getTime(WRITE)) // never touch ghost directories!
pae.setTime(WRITE, getTimeMillis)
}
1 + parentSize
case _ =>
0
}
}
private def getTimeMillis = {
if (UNKNOWN == time) time = System.currentTimeMillis
time
}
def head = segments.head.entry
} // Make
/** Tests the named file system entry and then - unless its the file system
* root - notifies the listener and deletes the entry.
* For the file system root, only the tests are performed but the listener
* does not get notified and the entry does not get deleted.
* For the tests to succeed, the named file system entry must exist and
* directory entries (including the file system root) must be empty.
*
* @param name the archive file system entry name.
* @throws IOException on any I/O error.
*/
def unlink(options: AccessOptions, name: FsNodeName) {
// Test.
val path = name.getPath
val mce = master.get(path) match {
case Some(mce) => mce
case _ => throw new NoSuchFileException(name.toString)
}
if (mce.isType(DIRECTORY)) {
val size = mce.getMembers.size
if (0 != size)
throw new DirectoryNotEmptyException(name.toString)
}
if (name.isRoot) {
// Removing the root entry MUST get silently ignored in order to
// make the controller logic work.
return
}
// Notify listener and modify.
touch(options)
master.remove(path);
{
// See http://java.net/jira/browse/TRUEZIP-144 :
// This is used to signal to the driver that the entry should not
// be included in the central directory even if the entry is
// already physically present in the archive file (ZIP).
// This signal will be ignored by drivers which do no support a
// central directory (TAR).
val mae = mce.getEntry
for (tµpe <- ALL_SIZES)
mae.setSize(tµpe, UNKNOWN)
for (tµpe <- ALL_ACCESS)
mae.setTime(tµpe, UNKNOWN)
}
splitter.split(path)
val pp = splitter.getParentPath
val pce = master.get(pp).get
val ok = pce.remove(splitter.getMemberName)
assert(ok, "The parent directory of \"" + name.toString
+ "\" does not contain this entry - archive file system is corrupted!")
val pae = pce.get(DIRECTORY)
if (UNKNOWN != pae.getTime(WRITE)) // never touch ghost directories!
pae.setTime(WRITE, System.currentTimeMillis)
}
/** Returns a new archive entry.
*
* Note that this is just a factory method and the returned file system entry
* is not (yet) linked into this (virtual) archive file system.
*
* @param name the entry name.
* @param tµpe the entry type.
* @param template if not `None`, then the new entry shall inherit
* as much properties from this entry as possible - with the
* exception of its name and type.
* @return A new entry for the given name.
*/
private def newEntry(name: String, tµpe: Type, template: Option[Entry]) = {
assert(!isRoot(name) || DIRECTORY.eq(tµpe))
driver.newEntry(NONE, name, tµpe, template.orNull)
}
/** Returns a new archive entry.
* This version checks that the given entry name can get encoded by the
* driver's character set.
*
* Note that this is just a factory method and the returned file system entry
* is not (yet) linked into this (virtual) archive file system.
*
* @param name the entry name.
* @param options a bit field of access options.
* @param tµpe the entry type.
* @param template if not `None`, then the new entry shall inherit
* as much properties from this entry as possible - with the
* exception of its name and type.
* @return A new entry for the given name.
* @throws CharConversionException If the entry name contains characters
* which cannot get encoded.
* @see #make
*/
private def newEntry(options: AccessOptions, name: String, tµpe: Type, template: Option[Entry]) = {
assert(!isRoot(name))
driver.checkEncodable(name)
driver.newEntry(options, name, tµpe, template.orNull)
}
/** Marks this (virtual) archive file system as touched and notifies the
* listener if and only if the touch status is changing.
*
* @throws IOException If the listener's preTouch implementation vetoed
* the operation for any reason.
*/
private def touch(options: AccessOptions) {
if (!touched) {
_touchListener.foreach(_.preTouch(options))
touched = true
}
}
/** Gets the archive file system touch listener. */
final def touchListener = _touchListener
/** Sets the archive file system touch listener.
*
* @param listener the touch listener.
* @throws IllegalStateException if `listener` is not null and the
* touch listener has already been set.
*/
final def touchListener_=(listener: Option[TouchListener]) {
if (listener.isDefined && _touchListener.isDefined)
throw new IllegalStateException("The touch listener has already been set!")
_touchListener = listener
}
} // ArchiveFileSystem
private object ArchiveFileSystem {
private val RootPath = ROOT.getPath
/** Returns a new empty archive file system and ensures its integrity.
* Only the root directory is created with its last modification time set
* to the system's current time.
* The file system is modifiable and marked as touched!
*
* @tparam E The type of the archive entries.
* @param driver the archive driver to use.
* @return A new archive file system.
*/
def apply[E <: FsArchiveEntry](driver: FsArchiveDriver[E]) =
new ArchiveFileSystem(driver)
/** Returns a new archive file system which populates its entries from
* the given `archive` and ensures its integrity.
*
* First, the entries from the archive are loaded into the file system.
*
* Second, a root directory with the given last modification time is
* created and linked into the filesystem (so it's never loaded from the
* archive).
*
* Finally, the file system integrity is checked and fixed: Any missing
* parent directories are created using the system's current time as their
* last modification time - existing directories will never be replaced.
*
* Note that the entries in the file system are shared with the given
* `archive`.
*
* @tparam E The type of the archive entries.
* @param driver the archive driver to use.
* @param archive The archive entry container to read the entries for
* the population of the archive file system.
* @param rootTemplate The optional template to use for the root entry of
* the returned archive file system.
* @param readOnly If and only if `true`, any subsequent
* modifying operation on the file system will result in a
* [[net.java.truevfs.kernel.impl.FsReadOnlyFileSystemException]].
*@return A new archive file system.
*/
def apply[E <: FsArchiveEntry](driver: FsArchiveDriver[E], archive: Container[E], rootTemplate: Option[Entry], readOnly: Boolean) = {
if (readOnly) new ReadOnlyArchiveFileSystem(driver, archive, rootTemplate)
else new ArchiveFileSystem(driver, archive, rootTemplate)
}
private def typeName(entry: FsCovariantNode[_ <: Entry]): String = {
val types = entry.getTypes
if (1 == types.cardinality) typeName(types.iterator.next)
else types.toString.toLowerCase(Locale.ROOT)
}
private def typeName(tµpe: Type) = tµpe.toString.toLowerCase(Locale.ROOT)
/** The master archive entry table.
*
* @tparam E The type of the archive entries.
*/
final class EntryTable[E <: FsArchiveEntry](_initialSize: Int)
extends Iterable[FsCovariantNode[E]] {
/** The map of covariant file system entries.
*
* Note that the archive entries in the covariant file system entries
* in this map are shared with the constructor parameter `archive` of
* the archive file system object.
*/
private[this] val map = new collection.mutable.LinkedHashMap[String, FsCovariantNode[E]] {
// See https://issues.scala-lang.org/browse/SI-5804 .
table = new Array(initialCapacity(_initialSize))
threshold = (table.size * 3L / 4).toInt
}
override def size = map.size
override def iterator = map.values.iterator
def add(name: String, ae: E) = {
val ce = map.get(name) match {
case Some(ce) => ce
case _ =>
val ce = new FsCovariantNode[E](name)
map.put(name, ce)
ce
}
ce.put(ae.getType, ae)
ce
}
def get(name: String) = map.get(name)
def remove(name: String) = map.remove(name)
} // EntryTable
private final class Splitter extends PathSplitter(SEPARATOR_CHAR, false) {
override def getParentPath = {
val path = super.getParentPath
if (null ne path) path else RootPath
}
} // Splitter
/** Used to notify implementations of an event in this file system. */
trait TouchListener {
/** Called immediately before the source archive file system is going to
* get modified (touched) for the first time.
* If this method throws an [[java.io.IOException]], then the modification
* is effectively vetoed.
*
* @throws IOException at the discretion of the implementation.
*/
def preTouch(options: AccessOptions)
} // TouchListener
/** A case class which represents a path segment for use by
* [[net.java.truevfs.kernel.impl.ArchiveFileSystem.Make]].
*
* @param name the nullable member name for the covariant file system entry.
* @param entry the covariant file system entry for the nullable member name.
*/
private final case class Segment[E <: FsArchiveEntry](
name: Option[String],
entry: FsCovariantNode[E])
} // ArchiveFileSystem
© 2015 - 2024 Weber Informatics LLC | Privacy Policy