All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.java.truevfs.kernel.impl.TargetArchiveController Maven / Gradle / Ivy

/*
 * Copyright © 2005 - 2021 Schlichtherle IT Services.
 * All rights reserved. Use is subject to license terms.
 */
package net.java.truevfs.kernel.impl;

import bali.Cache;
import bali.Lookup;
import lombok.val;
import net.java.truecommons.cio.*;
import net.java.truecommons.io.ClosedInputException;
import net.java.truecommons.io.ClosedOutputException;
import net.java.truecommons.shed.BitField;
import net.java.truecommons.shed.ControlFlowException;
import net.java.truevfs.kernel.spec.*;

import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.NoSuchFileException;
import java.util.Collections;
import java.util.Iterator;
import java.util.Optional;
import java.util.function.Supplier;

import static bali.CachingStrategy.NOT_THREAD_SAFE;
import static net.java.truecommons.cio.Entry.ALL_SIZES;
import static net.java.truecommons.cio.Entry.Access.READ;
import static net.java.truecommons.cio.Entry.Access.WRITE;
import static net.java.truecommons.cio.Entry.Size.DATA;
import static net.java.truecommons.cio.Entry.Type.DIRECTORY;
import static net.java.truecommons.cio.Entry.Type.SPECIAL;
import static net.java.truecommons.cio.Entry.UNKNOWN;
import static net.java.truevfs.kernel.spec.FsAccessOption.CACHE;
import static net.java.truevfs.kernel.spec.FsAccessOption.GROW;
import static net.java.truevfs.kernel.spec.FsAccessOptions.ACCESS_PREFERENCES_MASK;
import static net.java.truevfs.kernel.spec.FsSyncOption.ABORT_CHANGES;

/**
 * Manages I/O to the entry which represents the target archive file in its parent file system, detects archive entry
 * collisions and implements a sync of the target archive file.
 * 

* This controller is an emitter of {@link net.java.truecommons.shed.ControlFlowException}s, for example when * {@linkplain net.java.truevfs.kernel.impl.FalsePositiveArchiveException detecting a false positive archive file}, or * {@linkplain net.java.truevfs.kernel.impl.NeedsSyncException requiring a sync}. * * @param the type of the archive entries. * @author Christian Schlichtherle */ @NotThreadSafe abstract class TargetArchiveController extends FileSystemArchiveController { private static final BitField MOUNT_OPTIONS = BitField.of(CACHE); private static final BitField WRITE_ACCESS = BitField.of(WRITE); /** * The (possibly cached) {@link InputArchive} which is used to mount the (virtual) archive file system and read the * entries from the target archive file. */ private Optional> _inputArchive = Optional.empty(); /** * The (possibly cached) {@link OutputArchive} which is used to write the entries to the target archive file. */ private Optional> _outputArchive = Optional.empty(); private boolean invariants() { assert getModel().getParent() == getParent().getModel(); val fs = getFileSystem(); assert !_inputArchive.isPresent() || fs.isPresent(); assert !_outputArchive.isPresent() || fs.isPresent(); assert !fs.isPresent() || _inputArchive.isPresent() || _outputArchive.isPresent(); return true; } @Lookup(param = "driver") @Override public abstract FsArchiveDriver getDriver(); @Lookup(param = "model") abstract FsModel getUnderlyingModel(); @Cache(NOT_THREAD_SAFE) @Override public ArchiveModel getModel() { return new TargetArchiveModel(getDriver(), getUnderlyingModel()); } /** * The entry name of the target archive file in the parent file system. */ @Cache(NOT_THREAD_SAFE) FsNodeName getName() { val path = getMountPoint().getPath(); assert null != path; return path.getNodeName(); } private Optional> getInputArchive() { if (_inputArchive.isPresent() && !_inputArchive.get().isOpen()) { throw NeedsSyncException.apply(); } return _inputArchive; } private void setInputArchive(final Optional> ia) { assert !ia.isPresent() || !_inputArchive.isPresent(); ia.ifPresent(a -> setMounted(true)); _inputArchive = ia; } private Optional> getOutputArchive() { if (_outputArchive.isPresent() && !_outputArchive.get().isOpen()) { throw NeedsSyncException.apply(); } return _outputArchive; } private void setOutputArchive(final Optional> oa) { assert !oa.isPresent() || !_outputArchive.isPresent(); oa.ifPresent(a -> setMounted(true)); _outputArchive = oa; } @Override void mount(final BitField options, final boolean autoCreate) throws IOException { try { mount0(options, autoCreate); } finally { assert invariants(); } } private void mount0(final BitField options, final boolean autoCreate) throws IOException { // HC SVNT DRACONES! // Check parent file system node. final FsNode pn; try { pn = getParent().node(options, getName()); } catch (FalsePositiveArchiveException e) { throw new AssertionError(e); } catch (IOException e) { if (autoCreate) { throw e; } throw new FalsePositiveArchiveException(e); } // Obtain file system by creating or loading it from the parent node. final ArchiveFileSystem fs; if (null == pn) { if (autoCreate) { // This may fail e.g. if the container file is a RAES encrypted ZIP file and the user cancels password // prompting: outputArchive(options); fs = ArchiveFileSystem.apply(getModel()); } else { throw new FalsePositiveArchiveException(new NoSuchFileException(getName().toString())); } } else { // ro must be init first because the parent filesystem controller could be a // net.java.truevfs.driver.file.FileController and then on Windoze this property changes to `TRUE` once the // file is opened for reading! // FIXME: Produce a new exception on each call! val ro = checkReadOnly().map(e -> (Supplier) () -> e); final InputService is; try { is = getDriver().newInput(getModel(), MOUNT_OPTIONS, getParent(), getName()); } catch (FalsePositiveArchiveException e) { throw new AssertionError(e); } catch (IOException e) { if (pn.isType(SPECIAL)) { throw new FalsePositiveArchiveException(e); } else { throw new PersistentFalsePositiveArchiveException(e); } } fs = ArchiveFileSystem.apply(getModel(), is, pn, ro); setInputArchive(Optional.of(new InputArchive<>(is))); assert isMounted(); } setFileSystem(Optional.of(fs)); } private Optional checkReadOnly() { try { getParent().checkAccess(MOUNT_OPTIONS, getName(), WRITE_ACCESS); return Optional.empty(); } catch (FalsePositiveArchiveException e) { throw new AssertionError(e); } catch (IOException e) { return Optional.of(e); } } /** * Ensures that `outputArchive` is not empty. * * @return The output archive. */ private OutputArchive outputArchive(final BitField options) throws IOException { if (getOutputArchive().isPresent()) { assert isMounted(); return getOutputArchive().get(); } val is = getInputArchive().map(InputArchive::getDriverProduct).orElse(null); final OutputService os; try { os = getDriver().newOutput(getModel(), options.and(ACCESS_PREFERENCES_MASK).set(CACHE), getParent(), getName(), is); } catch (FalsePositiveArchiveException e) { throw new AssertionError(e); } catch (final ControlFlowException e) { assert e instanceof NeedsLockRetryException : e; throw e; } val oa = new OutputArchive<>(os); setOutputArchive(Optional.of(oa)); assert isMounted(); return oa; } @Override InputSocket input(String name) { return new AbstractInputSocket() { InputSocket socket; InputSocket socket() { val s = socket; return null != s ? s : (this.socket = getInputArchive().get().input(name)); } @Override public E target() throws IOException { return socket().target(); } @Override public InputStream stream(OutputSocket peer) throws IOException { return syncOn(ClosedInputException.class, new Op() { @Override public InputStream call() throws IOException { return socket().stream(peer); } }); } @Override public SeekableByteChannel channel(OutputSocket peer) throws IOException { return syncOn(ClosedInputException.class, new Op() { @Override public SeekableByteChannel call() throws IOException { return socket().channel(peer); } }); } }; } @Override OutputSocket output(BitField options, E entry) { return new AbstractOutputSocket() { OutputSocket socket; OutputSocket socket() throws IOException { val s = socket; return null != s ? s : (this.socket = outputArchive(options).output(entry)); } @Override public E target() { return entry; } @Override public OutputStream stream(InputSocket peer) throws IOException { return syncOn(ClosedOutputException.class, new Op() { @Override public OutputStream call() throws IOException { return socket().stream(peer); } }); } @Override public SeekableByteChannel channel(InputSocket peer) throws IOException { return syncOn(ClosedOutputException.class, new Op() { @Override public SeekableByteChannel call() throws IOException { return socket().channel(peer); } }); } }; } private static A syncOn(final Class klass, final Op op) throws X { try { return op.call(); } catch (IOException e) { if (klass.isInstance(e)) { throw NeedsSyncException.apply(); } else { throw e; } } } @Override public void sync(final BitField options) throws FsSyncException { try { val builder = new FsSyncExceptionBuilder(); if (!options.get(ABORT_CHANGES)) { copy(builder); } close(options, builder); builder.check(); } finally { assert invariants(); } } /** * Synchronizes all entries in the (virtual) archive file system with the (temporary) output archive file. * * @param handler the strategy for assembling sync exceptions. */ private void copy(final FsSyncExceptionBuilder handler) throws FsSyncException { // Skip (In|Out)putArchive for better performance. // This is safe because the ResourceController has already shut down all concurrent access by closing the // respective resources (streams, channels etc). // The Disconnecting(In|Out)putService should not get skipped however: // If these would throw an (In|Out)putClosedException, then this would be an artifact of a bug. val ois = _inputArchive .map(InputArchive::clutch) .filter(DisconnectingInputService::isOpen); final InputService is; if (ois.isPresent()) { is = ois.get(); } else { is = new DummyInputService<>(); } val oos = _outputArchive .map(OutputArchive::clutch) .filter(DisconnectingOutputService::isOpen); final OutputService os; if (oos.isPresent()) { os = oos.get(); } else { return; } assert getFileSystem().isPresent(); for (val cn : getFileSystem().get()) { for (val ae : cn.getEntries()) { val aen = ae.getName(); if (null == os.entry(aen)) { try { if (DIRECTORY == ae.getType()) { if (!cn.isRoot()) { // never output the root directory! if (UNKNOWN != ae.getTime(WRITE)) { // never output a ghost directory! os.output(ae).stream(null).close(); } } } else if (null != is.entry(aen)) { IoSockets.copy(is.input(aen), os.output(ae)); } else { // The file system entry is a newly created // non-directory entry which hasn't received any // content yet, e.g. as a result of make() // => output an empty file system entry. for (val size : ALL_SIZES) { ae.setSize(size, UNKNOWN); } ae.setSize(DATA, 0); os.output(ae).stream(null).close(); } } catch (IOException e) { throw handler.fail(new FsSyncException(getMountPoint(), e)); } } } } } /** * Discards the file system, closes the input archive and finally the output archive. * Note that this order is critical: The parent file system controller is expected to replace the entry for the * target archive file with the output archive when it gets closed, so this must be done last. * Using a finally block ensures that this is done even in the unlikely event of an exception when closing the input * archive. * Note that in this case closing the output archive is likely to fail and override the IOException thrown by this * method, too. * * @param handler the strategy for assembling sync exceptions. */ private void close(final BitField options, final FsSyncExceptionBuilder handler) { // HC SVNT DRACONES! if (_inputArchive.isPresent()) { val ia = _inputArchive.get(); try { ia.close(); } catch (final ControlFlowException e) { assert e instanceof NeedsLockRetryException : e; throw e; } catch (IOException e) { handler.warn(new FsSyncWarningException(getMountPoint(), e)); } setInputArchive(Optional.empty()); } if (_outputArchive.isPresent()) { val oa = _outputArchive.get(); try { oa.close(); } catch (final ControlFlowException e) { assert e instanceof NeedsLockRetryException : e; throw e; } catch (IOException e) { handler.warn(new FsSyncException(getMountPoint(), e)); } setOutputArchive(Optional.empty()); } setFileSystem(Optional.empty()); if (options.get(ABORT_CHANGES)) { setMounted(false); } } @Override void checkSync(final BitField options, final FsNodeName name, final Entry.Access intention) throws NeedsSyncException { // HC SVNT DRACONES! // If no file system exists then pass the test. if (!getFileSystem().isPresent()) { return; } val fs = getFileSystem().get(); // If GROWing and the driver supports the respective access method, then pass the test. if (options.get(GROW)) { switch (intention) { case READ: break; case WRITE: if (getDriver().getRedundantContentSupport()) { getOutputArchive(); // side-effect! return; } break; default: if (getDriver().getRedundantMetaDataSupport()) { return; } } } // If the file system does not contain an entry with the given name, then pass the test. val optNode = fs.node(options, name); if (!optNode.isPresent()) { return; } val node = optNode.get(); assert null != node.getEntry(); val aen = node.getEntry().getName(); // If the entry name addresses the file system root, then pass the test because the root entry cannot get input // or output anyway. if (name.isRoot()) { return; } // Check if the entry is already written to the output archive. if (getOutputArchive().isPresent()) { val oa = getOutputArchive().get(); if (null != oa.entry(aen)) { throw NeedsSyncException.apply(); } } // If our intention is reading the entry then check if it's present in the input archive. if (intention == READ) { if (getInputArchive().isPresent()) { val ia = getInputArchive().get(); if (null == ia.entry(aen)) { throw NeedsSyncException.apply(); } } else { throw NeedsSyncException.apply(); } } } private final class TargetArchiveModel extends ArchiveModel { TargetArchiveModel(FsArchiveDriver driver, FsModel model) { super(driver, model); } @Override void touch(BitField options) throws IOException { outputArchive(options); } } private static final class InputArchive extends LockInputService { final InputService driverProduct; InputArchive(InputService driverProduct) { super(new DisconnectingInputService<>(driverProduct)); this.driverProduct = driverProduct; } InputService getDriverProduct() { return driverProduct; } boolean isOpen() { return clutch().isOpen(); } DisconnectingInputService clutch() { assert null != container; return (DisconnectingInputService) container; } } private static final class OutputArchive extends LockOutputService { final OutputService driverProduct; OutputArchive(OutputService driverProduct) { super(new DisconnectingOutputService<>(driverProduct)); this.driverProduct = driverProduct; } OutputService getDriverProduct() { return driverProduct; } boolean isOpen() { return clutch().isOpen(); } DisconnectingOutputService clutch() { assert null != container; return (DisconnectingOutputService) container; } } private static final class DummyInputService implements InputService { @Override public int size() { return 0; } @Override public Iterator iterator() { return Collections.emptyIterator(); } @Nullable @Override public E entry(String name) { return null; } @Override public InputSocket input(String name) { throw new AssertionError(); } @Override public void close() throws IOException { throw new AssertionError(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy