de.schlichtherle.io.Files Maven / Gradle / Ivy
Show all versions of truezip Show documentation
/*
* Copyright (C) 2007-2010 Schlichtherle IT Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.schlichtherle.io;
import de.schlichtherle.io.ArchiveController.*;
import de.schlichtherle.io.ArchiveFileSystem.*;
import de.schlichtherle.io.archive.spi.*;
import de.schlichtherle.io.util.*;
import java.io.*;
import java.util.*;
/**
* Provides static utility methods for {@link File}s.
* Note that in contrast to the {@link File} class, the methods in this
* class accept and return plain {@code java.io.File} instances.
* Full advantage is taken if a parameter is actually an instance of the
* {@code File} class in this package, however.
*
* TODO: Consider making this class public in TrueZIP 7 and remove the
* stub methods for the same purpose in {@link File}.
*
* @author Christian Schlichtherle
* @version $Id: Files.java,v 1.7 2010/09/20 22:13:11 christian_schlichtherle Exp $
* @since TrueZIP 6.5
*/
final class Files {
/**
* A lock used when copying data from one archive file to another.
* This lock must be acquired before any other locks on the controllers
* are acquired in order to prevent dead locks.
*/
private static final CopyLock copyLock = new CopyLock();
/** This class cannot get instantiated. */
protected Files() {
}
/**
* @see File#contains
*/
public static boolean contains(java.io.File a, java.io.File b) {
a = getCanOrAbsFile(a);
b = getCanOrAbsFile(b);
return contains(a.getPath(), b.getPath());
}
/**
* Returns true if and only if the {@code pathA} contains
* {@code pathB}.
*
* @param pathA A valid file path.
* @param pathB A valid file path.
* @throws NullPointerException If any parameter is {@code null}.
*/
static boolean contains(String pathA, String pathB) {
// Windows is just case preserving, all others are case sensitive.
if (File.separatorChar == '\\') {
pathA = pathA.toLowerCase();
pathB = pathB.toLowerCase();
}
if (!pathB.startsWith(pathA))
return false;
final int lengthA = pathA.length();
final int lengthB = pathB.length();
if (lengthA == lengthB)
return true;
else if (lengthA < lengthB)
return pathB.charAt(lengthA) == File.separatorChar;
return false;
}
/**
* Returns the canonical path of the given file or the normalized absolute
* path if canonicalizing the path fails due to an {@code IOException}.
*
* @return The canonical or absolute path of this file as a
* {@code java.io.File} instance.
* @throws NullPointerException If {@code file} is {@code null}.
*/
public static java.io.File getCanOrAbsFile(java.io.File file) {
try {
return file.getCanonicalFile();
} catch (IOException ex) {
final java.io.File parent = file.getParentFile();
return normalize(parent != null
? new java.io.File(getCanOrAbsFile0(parent), file.getName())
: file.getAbsoluteFile());
}
}
private static java.io.File getCanOrAbsFile0(java.io.File file) {
try {
return file.getCanonicalFile();
} catch (IOException ex) {
final java.io.File parent = file.getParentFile();
return parent != null
? new java.io.File(getCanOrAbsFile0(parent), file.getName())
: file.getAbsoluteFile();
}
}
/**
* Removes any {@code "."} and {@code ".."}
* directories from the path wherever possible.
*
* @param file The file instance which's path is to be normalized.
* @return {@code file} if it was already in normalized form.
* Otherwise, an object which's runtime class is guaranteed to
* be {@code java.io.File}.
*/
public static java.io.File normalize(final java.io.File file) {
final String path = file.getPath();
final String newPath = Paths.normalize(path, File.separatorChar);
return newPath != path // mind contract of Paths.normalize!
? new java.io.File(newPath)
: file;
}
/**
* Returns {@code true} if the given file exists or can be created
* and at least one byte can be successfully written to it - the file is
* restored to its previous state afterwards.
* This is a much stronger test than {@link File#canWrite()}.
*/
public static boolean isWritableOrCreatable(final java.io.File file) {
try {
if (!file.exists()) {
final boolean created = file.createNewFile();
boolean ok = isWritableOrCreatable(file);
if (created && !file.delete())
ok = false; // be conservative!
return ok;
} else if (file.canWrite()) {
// Some operating and file system combinations make File.canWrite()
// believe that the file is writable although it's not.
// We are not that gullible, so let's test this...
final long time = file.lastModified();
if (time < 0) {
// lastModified() may return negative values but setLastModified()
// throws an IAE for negative values, so we are conservative.
// See issue #18.
return false;
}
if (!file.setLastModified(time + 1)) {
// This may happen on Windows and normally means that
// somebody else has opened this file
// (regardless of read or write mode).
// Be conservative: We don't allow writing to this file!
return false;
}
boolean ok;
try {
// Open the file for reading and writing, requiring any
// update to its contents to be written to the filesystem
// synchronously.
// As Dr. Simon White from Catalysoft, Cambridge, UK reported,
// "rws" does NOT work on Mac OS X with Apple's Java 1.5
// Release 1 (equivalent to Sun's Java 1.5.0_02), however
// it DOES work with Apple's Java 1.5 Release 3.
// He also confirmed that "rwd" works on Apple's
// Java 1.5 Release 1, so we use this instead.
// Thank you very much for spending the time to fix this
// issue, Dr. White!
final RandomAccessFile raf = new RandomAccessFile(file, "rwd");
try {
final boolean empty;
int octet = raf.read();
if (octet == -1) {
octet = 0; // assume first byte is 0
empty = true;
} else {
empty = false;
}
// Let's test if we can overwrite the first byte.
// See issue #29.
raf.seek(0);
raf.write(octet);
try {
// Rewrite original content and check success.
raf.seek(0);
final int check = raf.read();
// This should always return true unless the storage
// device is faulty.
ok = octet == check;
} finally {
if (empty)
raf.setLength(0);
}
} finally {
raf.close();
}
} finally {
if (!file.setLastModified(time)) {
// This may happen on Windows and normally means that
// somebody else has opened this file meanwhile
// (regardless of read or write mode).
// Be conservative: We don't allow (further) writing to
// this file!
ok = false;
}
}
return ok;
} else { // if (file.exists() && !file.canWrite()) {
return false;
}
} catch (IOException ex) {
return false; // don't allow writing if anything goes wrong!
}
}
//
// Move, copy and remove methods:
//
/**
* Moves the source to the destination by recursively copying and deleting
* its files and directories.
* Hence, this file system operation works even with archive files or
* entries within archive files, but is not atomic.
*
* The name of this method is inspired by the Unix command line utility
* {@code mv} although in most cases it performs a plain rename
* operation rather than a copy-and-delete operation.
*
* @param src The source file or directory.
* This must exist.
* @param dst The destination file or directory.
* This may or may not exist.
* If it does, its contents are overwritten.
* @param detector The object used to detect any archive
* files in the path and configure their parameters.
* @return Whether the operation succeeded or not.
* If it fails, the source and destination may contain only a
* subset of the source before this operation.
* However, each file has either been completely moved or not.
* @see File#renameTo(java.io.File, ArchiveDetector)
* @see Third Party
* Access using different Archive Detectors
*/
public static final boolean mv(
final java.io.File src,
final java.io.File dst,
final ArchiveDetector detector) {
return !contains(src, dst) && mv0(src, dst, detector);
}
private static boolean mv0(
final java.io.File src,
final java.io.File dst,
final ArchiveDetector detector) {
boolean ok = true;
if (src.isDirectory()) {
final long srcLastModified = src.lastModified();
final boolean srcIsArchived = src instanceof File
&& ((File) src).getInnerArchive() != null;
final boolean dstIsArchived = dst instanceof File
&& ((File) dst).getInnerArchive() != null;
final boolean srcIsGhost = srcIsArchived
&& srcLastModified <= 0;
if (!srcIsGhost || !dstIsArchived || !File.isLenient())
dst.mkdir();
final String[] members = src.list();
if (!srcIsArchived && dstIsArchived) {
// Create sorted entries if writing a new archive file.
// This is courtesy only, so natural order is sufficient.
Arrays.sort(members);
}
for (int i = 0, l = members.length; i < l; i++) {
final String member = members[i];
ok &= mv0( detector.createFile(src, member),
detector.createFile(dst, member),
detector);
}
if (!srcIsGhost)
ok &= dst.setLastModified(srcLastModified);
} else if (src.isFile()) { // !isDirectory()
try {
cp(true, src, dst);
} catch (IOException ex) {
ok = false;
}
} else {
ok = false; // don't move special files!
}
return ok && src.delete(); // only unlink if ok!
}
/**
* The name of this method is inspired by the Unix command line utility
* {@code cp} with the {@code -r} option to operate recursively.
*
* @see File#copyAllTo(java.io.File, ArchiveDetector, ArchiveDetector)
* @see File#archiveCopyAllTo(java.io.File, ArchiveDetector, ArchiveDetector)
* @see Third Party
* Access using different Archive Detectors
*/
public static final void cp_r(
final boolean preserve,
final java.io.File src,
final java.io.File dst,
final ArchiveDetector srcDetector,
final ArchiveDetector dstDetector)
throws IOException {
if (contains(src, dst))
throw new ContainsFileException(src, dst);
cp_r0(preserve, src, dst, srcDetector, dstDetector);
}
/**
* Unchecked parameters version.
*/
private static void cp_r0(
final boolean preserve,
final java.io.File src,
final java.io.File dst,
final ArchiveDetector srcDetector,
final ArchiveDetector dstDetector)
throws IOException {
if (src.isDirectory()) {
final long srcLastModified = src.lastModified();
final boolean srcIsArchived = src instanceof File
&& ((File) src).getInnerArchive() != null;
final boolean dstIsArchived = dst instanceof File
&& ((File) dst).getInnerArchive() != null;
final boolean srcIsGhost = srcIsArchived
&& srcLastModified <= 0;
if (!srcIsGhost || !dstIsArchived || !File.isLenient())
if (!dst.mkdir() && !dst.isDirectory())
throw new IOException("destination is not a directory");
final String[] members = src.list();
if (!srcIsArchived && dstIsArchived) {
// Create sorted entries if writing a new archive.
// This is a courtesy only, so natural order is sufficient.
Arrays.sort(members);
}
for (int i = 0, l = members.length; i < l; i++) {
final String member = members[i];
cp_r0( preserve,
srcDetector.createFile(src, member),
dstDetector.createFile(dst, member),
srcDetector, dstDetector);
}
if (preserve && !srcIsGhost)
if (!dst.setLastModified(srcLastModified))
throw new IOException("cannot set last modification time");
} else if (src.isFile() && (!dst.exists() || dst.isFile())) {
cp0(preserve, src, dst);
} else {
throw new IOException("cannot copy non-existent or special files");
}
}
/**
* The name of this method is inspired by the Unix command line utility
* {@code cp}.
*
* @see File#cp(java.io.File, java.io.File)
* @see File#cp_p(java.io.File, java.io.File)
* @see Third Party
* Access using different Archive Detectors
*/
public static final void cp(
final boolean preserve,
final java.io.File src,
final java.io.File dst)
throws IOException {
if (contains(src, dst))
throw new ContainsFileException(src, dst);
cp0(preserve, src, dst);
}
/**
* Unchecked parameters version.
*/
private static void cp0(
final boolean preserve,
final java.io.File src,
final java.io.File dst)
throws IOException {
assert src != null;
assert dst != null;
try {
try {
if (src instanceof File) {
final File srcFile = (File) src;
srcFile.ensureNotVirtualRoot("cannot read");
final String srcEntryName = srcFile.getEnclEntryName();
if (srcEntryName != null) {
cp0(preserve,
srcFile.getEnclArchive().getArchiveController(),
srcEntryName, dst);
return;
}
}
} catch (RfsEntryFalsePositiveException srcIsNotArchive) {
}
// Treat the source like a regular file.
final InputStream in = new java.io.FileInputStream(src);
try {
cp0(preserve, src, in, dst);
} finally {
try {
in.close();
} catch (IOException ex) {
throw new InputIOException(ex);
}
}
} catch (FileNotFoundException ex) {
throw ex;
} catch (ArchiveBusyException ex) {
throw new FileBusyException(ex);
} catch (ArchiveFileSystemException afse) {
final FileNotFoundException fnfe
= new FileNotFoundException(afse.toString());
fnfe.initCause(afse);
throw fnfe;
} catch (IOException ex) {
dst.delete();
throw ex;
}
}
/**
* Copies a source file to a destination file, optionally preserving the
* source's last modification time.
* We already have an input stream to read the source file,
* but we know nothing about the destination file yet.
* Note that this method never closes the given input stream!
*
* @throws FileNotFoundException If either the source or the destination
* cannot get accessed.
* @throws InputIOException If copying the data fails because of an
* IOException in the source.
* @throws IOException If copying the data fails because of an
* IOException in the destination.
*/
private static void cp0(
final boolean preserve,
final java.io.File src,
final InputStream in,
final java.io.File dst)
throws IOException {
try {
if (dst instanceof File) {
final File dstFile = (File) dst;
dstFile.ensureNotVirtualRoot("cannot write");
final String dstEntryName = dstFile.getEnclEntryName();
if (dstEntryName != null) {
cp0(preserve, src, in,
dstFile.getEnclArchive().getArchiveController(),
dstEntryName);
return;
}
}
} catch (RfsEntryFalsePositiveException dstIsNotArchive) {
}
// Treat the destination like a regular file.
final OutputStream out = new java.io.FileOutputStream(dst);
try {
Streams.cat(in, out);
} finally {
out.close();
}
if (preserve && !dst.setLastModified(src.lastModified()))
throw new IOException(dst.getPath()
+ " (cannot preserve last modification time)");
}
/**
* Copies a source file to a destination file, optionally preserving the
* source's last modification time.
* We know that the source file appears to be an entry in an archive
* file, but we know nothing about the destination file yet.
*
* Note that this method synchronizes on the class object in order
* to prevent dead locks by two threads copying archive entries to the
* other's source archive concurrently!
*
* @throws FalsePositiveException If the source or the destination is a
* false positive and the exception
* cannot get resolved within this method.
* @throws InputIOException If copying the data fails because of an
* IOException in the source.
* @throws IOException If copying the data fails because of an
* IOException in the destination.
*/
private static void cp0(
final boolean preserve,
final ArchiveController srcController,
final String srcEntryName,
final java.io.File dst)
throws IOException {
// Do not assume anything about the lock status of the controller:
// This method may be called from a subclass while a lock is acquired!
//assert !srcController.readLock().isLocked();
//assert !srcController.writeLock().isLocked();
try {
try {
if (dst instanceof File) {
final File dstFile = (File) dst;
dstFile.ensureNotVirtualRoot("cannot write");
final String dstEntryName = dstFile.getEnclEntryName();
if (dstEntryName != null) {
cp0(preserve, srcController, srcEntryName,
dstFile.getEnclArchive().getArchiveController(),
dstEntryName);
return;
}
}
} catch (RfsEntryFalsePositiveException isNotArchive) {
// Both the source and/or the destination may be false positives,
// so we need to use the exception's additional information to
// find out which controller actually detected the false positive.
if (isNotArchive.getController() == srcController)
throw isNotArchive; // not my job - pass on!
}
final InputStream in;
final long time;
srcController.readLock().lock();
try {
in = srcController.createInputStream0(srcEntryName); // detects false positives!
time = srcController.lastModified(srcEntryName);
} finally {
srcController.readLock().unlock();
}
// Treat the destination like a regular file.
final OutputStream out;
try {
out = new java.io.FileOutputStream(dst);
} catch (IOException ex) {
try {
in.close();
} catch (IOException inFailure) {
throw new InputIOException(inFailure);
}
throw ex;
}
cp(in, out);
if (preserve && !dst.setLastModified(time))
throw new IOException(dst.getPath()
+ " (cannot preserve last modification time)");
} catch (ArchiveEntryFalsePositiveException ex) {
assert srcController == ex.getController();
// Reroute call to the source's enclosing archive controller.
cp0(preserve, srcController.getEnclController(),
srcController.enclEntryName(srcEntryName),
dst);
}
}
/**
* Copies a source file to a destination file, optionally preserving the
* source's last modification time.
* We know that the source and destination files both appear to be entries
* in an archive file.
*
* @throws FalsePositiveException If the source or the destination is a
* false positive and the exception for the destination
* cannot get resolved within this method.
* @throws InputIOException If copying the data fails because of an
* IOException in the source.
* @throws IOException If copying the data fails because of an
* IOException in the destination.
*/
private static void cp0(
final boolean preserve,
final ArchiveController srcController,
final String srcEntryName,
final ArchiveController dstController,
final String dstEntryName)
throws IOException {
// Do not assume anything about the lock status of the controller:
// This method may be called from a subclass while a lock is acquired!
//assert !srcController.readLock().isLocked();
//assert !srcController.writeLock().isLocked();
//assert !dstController.readLock().isLocked();
//assert !dstController.writeLock().isLocked();
try {
class IOStreamCreator implements IORunnable {
InputStream in;
OutputStream out;
public void run() throws IOException {
// Update controllers.
// This may invalidate the file system object, so it must be
// done first in case srcController and dstController are the
// same!
class SrcControllerUpdater implements IORunnable {
public void run() throws IOException {
srcController.autoUmount(srcEntryName);
srcController.readLock().lock(); // downgrade to read lock upon return
}
} // class SrcControllerUpdater
final ArchiveEntry srcEntry, dstEntry;
final Delta delta;
srcController.runWriteLocked(new SrcControllerUpdater());
try {
dstController.autoUmount(dstEntryName);
// Get source archive entry.
final ArchiveFileSystem srcFileSystem
= srcController.autoMount(false);
srcEntry = srcFileSystem.get(srcEntryName);
// Get destination archive entry.
final boolean lenient = File.isLenient();
final ArchiveFileSystem dstFileSystem
= dstController.autoMount(lenient);
delta = dstFileSystem.link(dstEntryName,
lenient, preserve ? srcEntry : null);
dstEntry = delta.getEntry();
// Create input stream.
in = srcController.createInputStream(srcEntry, dstEntry);
} finally {
srcController.readLock().unlock();
}
try {
// Create output stream.
out = dstController.createOutputStream(dstEntry, srcEntry);
try {
// Now link the destination entry into the file system.
delta.commit();
} catch (IOException ex) {
out.close();
throw ex;
}
} catch (IOException ex) {
try {
in.close();
} catch (IOException inFailure) {
throw new InputIOException(inFailure);
}
throw ex;
}
}
} // class IOStreamCreator
final IOStreamCreator streams = new IOStreamCreator();
synchronized (copyLock) {
dstController.runWriteLocked(streams);
}
// Finally copy the entry data.
cp(streams.in, streams.out);
} catch (ArchiveEntryFalsePositiveException ex) {
// Both the source and/or the destination may be false positives,
// so we need to use the exception's additional information to
// find out which controller actually detected the false positive.
if (dstController != ex.getController())
throw ex; // not my job - pass on!
// Reroute call to the destination's enclosing archive controller.
cp0(preserve, srcController, srcEntryName,
dstController.getEnclController(),
dstController.enclEntryName(dstEntryName));
}
}
/**
* Copies a source file to a destination file, optionally preserving the
* source's last modification time.
* We already have an input stream to read the source file and the
* destination appears to be an entry in an archive file.
* Note that this method never closes the given input stream!
*
* Note that this method synchronizes on the class object in order
* to prevent dead locks by two threads copying archive entries to the
* other's source archive concurrently!
*
* @throws FalsePositiveException If the destination is a
* false positive and the exception
* cannot get resolved within this method.
* @throws InputIOException If copying the data fails because of an
* IOException in the source.
* @throws IOException If copying the data fails because of an
* IOException in the destination.
*/
static final void cp0(
final boolean preserve,
final java.io.File src,
final InputStream in,
final ArchiveController dstController,
final String dstEntryName)
throws IOException {
// Do not assume anything about the lock status of the controller:
// This method may be called from a subclass while a lock is acquired!
//assert !dstController.readLock().isLocked();
//assert !dstController.writeLock().isLocked();
try {
class OStreamCreator implements IORunnable {
OutputStream out; // = null;
public void run() throws IOException {
// Update controller.
// This may invalidate the file system object, so it must be
// done first in case srcController and dstController are the
// same!
dstController.autoUmount(dstEntryName);
final boolean lenient = File.isLenient();
// Get source archive entry.
final ArchiveEntry srcEntry = new RfsEntry(src);
// Get destination archive entry.
final ArchiveFileSystem dstFileSystem
= dstController.autoMount(lenient);
final Delta delta = dstFileSystem.link(dstEntryName,
lenient, preserve ? srcEntry : null);
final ArchiveEntry dstEntry = delta.getEntry();
// Create output stream.
out = dstController.createOutputStream(dstEntry, srcEntry);
// Now link the destination entry into the file system.
delta.commit();
}
}
// Create the output stream while the destination controller is
// write locked.
final OStreamCreator stream = new OStreamCreator();
dstController.runWriteLocked(stream);
final OutputStream out = stream.out;
// Finally copy the entry data.
try {
Streams.cat(in, out);
} finally {
out.close();
}
} catch (ArchiveEntryFalsePositiveException ex) {
assert dstController == ex.getController();
// Reroute call to the destination's enclosing ArchiveController.
cp0(preserve, src, in,
dstController.getEnclController(),
dstController.enclEntryName(dstEntryName));
}
}
/**
* @see File#cp(InputStream, OutputStream)
*/
public static void cp(
final InputStream in,
final OutputStream out)
throws IOException {
try {
try {
Streams.cat(in, out);
} finally {
out.close();
}
} finally {
try {
in.close();
} catch (IOException ex) {
throw new InputIOException(ex);
}
}
}
/**
* Removes the entire directory tree represented by the parameter,
* regardless whether it's a file or directory, whether the directory
* is empty or not or whether the file or directory is actually an
* archive file, an entry in an archive file or not enclosed in an
* archive file at all.
*
* The name of this method is inspired by the Unix command line utility
* {@code rm} with the {@code -r} option to operate recursively.
*
* This file system operation is not atomic.
*
* @return Whether or not the entire directory tree was successfully
* removed.
*/
public static boolean rm_r(final java.io.File file) {
boolean ok = true;
if (file.isDirectory()) {
// If the directory is an archive file, one may be tempted to delete it
// directly (using e.g. java.io.File.delete()).
// However, this would bypass the ArchiveController's state and cause
// subsequent mayhem.
// So we play it safe despite the fact that this procedure is comparably
// much slower.
java.io.File[] members = file.listFiles();
for (int i = members.length; --i >= 0;)
ok &= rm_r(members[i]);
}
return ok && file.delete();
}
//
// Static member classes and interfaces.
//
/**
* A lock used when copying data from one archive to another.
* This lock must be acquired before any other locks on the controllers
* are acquired in order to prevent dead locks.
*/
private static class CopyLock {
}
}