package com.laytonsmith.PureUtilities;
import com.laytonsmith.PureUtilities.Common.FileUtil;
import com.laytonsmith.PureUtilities.Common.StreamUtils;
import com.laytonsmith.PureUtilities.Common.StringUtils;
import com.laytonsmith.PureUtilities.Common.ArrayUtils;
import java.io.*;
import java.net.URL;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
/**
* Allows read operations to happen transparently on a zip file, as if it were a
* folder. Nested zips are also supported. All operations are read only.
* Operations on a ZipReader with a path in an actual zip are expensive, so it's
* good to keep in mind this when using the reader, you'll have to balance
* between memory usage (caching) or CPU use (re-reading as needed).
*
* @author Layton Smith
*/
public class ZipReader {
/**
* The top level zip file, which represents the actual file on the file system.
*/
private final File topZip;
/**
* The chain of Files that this file represents.
*/
private final Deque<File> chainedPath;
/**
* The actual file object.
*/
private final File file;
/**
* Whether or not we have to dig down into the zip, or if
* we can use trivial file operations.
*/
private final boolean isZipped;
/**
* A list of zip entries, which is cached, so we don't need to re-read
* the zip file each time we want to do enumerative stuff.
*/
private List<File> zipEntries = null;
/**
* The ZipEntry contains the information of whether or not the listed file is
* a directory, but since we discard that information, we cache the list of directories
* here.
*/
private List<File> zipDirectories = new ArrayList<File>();
/**
* Convenience constructor, which allows for a URL to be passed in instead of a file,
* which may be useful when working with resources.
* @param url
*/
public ZipReader(URL url){
this(new File(url.getFile()));
}
/**
* Creates a new ZipReader object, which can be used to read from a zip
* file, as if the zip files were simple directories. All files are checked
* to see if they are a zip.
*
* <p>{@code new ZipReader(new File("path/to/container.zip/with/nested.zip/file.txt"));}</p>
*
*
* @param file The path to the internal file. This needn't exist, according
* to File, as the zip file won't appear as a directory to other classes.
* This constructor will however throw a FileNotFoundException if it
* determines that the file doesn't exist.
*/
public ZipReader(File file){
chainedPath = new LinkedList<File>();
//We need to remove jar style or uri style things from the file, so do that here
if(file.getPath().startsWith("file:")){
String newFile = file.getPath().substring(5);
//Replace all \ with /, to simply processing, but also replace ! with /, since jar addresses
//use that to denote the jar. We don't care, it's just a folder, so replace that with a slash.
newFile = newFile.replace("\\", "/").replace("!", "/");
while(newFile.startsWith("//")){
//We only want up to one slash here
newFile = newFile.substring(1);
}
file = new File(newFile);
}
//make sure file is absolute
file = file.getAbsoluteFile();
this.file = file;
//We need to walk up the parents, putting those files onto the stack which are valid Zips
File f = file;
chainedPath.addFirst(f); //Gotta add the file itself to the path for everything to work
File tempTopZip = null;
while ((f = f.getParentFile()) != null) {
chainedPath.addFirst(f);
try {
//If this works, we'll know we have our top zip file. Everything else will have
//to be in memory, so we'll start with this if we have to dig deeper.
if (tempTopZip == null) {
ZipFile zf = new ZipFile(f);
tempTopZip = f;
}
} catch (ZipException ex) {
//This is fine, it's just not a zip file
} catch (IOException ex) {
//This is fine too, it may mean we don't have permission to access this directory,
//but that's ok, we don't need access yet.
}
}
//If it's not a zipped file, this will make operations easier to deal with,
//so let's save that information
isZipped = tempTopZip != null;
if(isZipped){
topZip = tempTopZip;
} else {
topZip = file;
}
}
/**
* Returns the top level file for the underlying file. If this is not zipped, the file
* returned will be the file this object was constructed with. Otherwise, the File
* representing the actual file on the filesystem will be returned. This is mostly
* useful for the case where locks need to be implemented, or to find the "root" of
* the directory.
* @return
*/
public File getTopLevelFile(){
return topZip;
}
/**
* Returns if this file exists or not. Note this is a non-trivial operation.
*
* @return
*/
public boolean exists(){
if(!topZip.exists()){
return false; //Don't bother trying
}
try{
getInputStream().close();
return true;
} catch(IOException e){
return false;
}
}
/**
* Returns true if this file is read accessible. Note that if the file is a zip,
* the permissions are checked on the topmost zip file.
* @return
*/
public boolean canRead(){
return topZip.canRead();
}
/**
* Returns true if this file has write permissions. Note that if the file is nested
* in a zip, then this will always return false. If the file doesn't exist, this will
* also return false, but that doesn't imply that you won't be able to create file here,
* so you may also need to check isZipped().
* @return
*/
public boolean canWrite(){
if(isZipped){
return false;
} else {
return topZip.canWrite();
}
}
/**
* Returns whether or not the file is inside of a zip file or not.
* @return
*/
public boolean isZipped(){
return isZipped;
}
/*
* This function recurses down into a zip file, ultimately returning the InputStream for the file,
* or throwing exceptions if it can't be found.
*/
private InputStream getFile(Deque<File> fullChain, String zipName, final ZipInputStream zis) throws FileNotFoundException, IOException {
ZipEntry entry;
InputStream zipReader = new InputStream() {
@Override
public int read() throws IOException {
if (zis.available() > 0) {
return zis.read();
} else {
return -1;
}
}
@Override
public void close() throws IOException {
zis.close();
}
};
boolean isZip = false;
List<String> recurseAttempts = new ArrayList<String>();
while ((entry = zis.getNextEntry()) != null) {
//This is at least a zip file
isZip = true;
Deque<File> chain = new LinkedList<File>(fullChain);
File chainFile = null;
while ((chainFile = chain.pollFirst()) != null) {
if (chainFile.equals(new File(zipName + File.separator + entry.getName()))) {
//We found it. Now, chainFile is one that is in our tree
//We have to do some further analyzation on it
break;
}
}
if (chainFile == null) {
//It's not in the chain at all, which means we don't care about it at all.
continue;
}
if (chain.isEmpty()) {
//It was the last file in the chain, so no point in looking at it at all.
//If it was a zip or not, it doesn't matter, because this is the file they
//specified, precisely. Read it out, and return it.
return zipReader;
}
//It's a single file, it's in the chain, and the chain isn't finished, so that
//must mean it's a container (or it's being used as one, anyways).
//It could be that either this is just a folder in the entry list, or it could
//mean that this is a zip. We will make note of this as one we need to attempt to
//recurse, but only if it doesn't pan out that this is a file.
recurseAttempts.add(zipName + File.separator + entry.getName());
}
for(String recurseAttempt : recurseAttempts){
ZipInputStream inner = new ZipInputStream(zipReader);
try{
return getFile(fullChain, recurseAttempt, inner);
} catch(IOException e){
//We don't care if this breaks, we'll throw out own top level exception
//in a moment if we got here. We still need to finish going through
//out recurse attempts.
}
}
//If we get down here, it means either we recursed into not-a-zip file, or
//the file was otherwise not found
if (isZip) {
//if this is the terminal node in the chain, it's due to a file not found.
throw new FileNotFoundException(zipName + " could not be found!");
} else {
//if not, it's due to this not being a zip file
throw new IOException(zipName + " is not a zip file!");
}
}
/**
* Returns a raw input stream for this file. If you just need the string contents,
* it would probably be easer to use getFileContents instead, however, this method
* is necessary for accessing binary files.
* @return An InputStream that will read the specified file
* @throws FileNotFoundException If the file is not found
* @throws IOException If you specify a file that isn't a zip file as if it were a folder
*/
public InputStream getInputStream() throws FileNotFoundException, IOException {
if (!isZipped) {
return new FileInputStream(file);
} else {
return getFile(chainedPath, topZip.getAbsolutePath(), new ZipInputStream(new FileInputStream(topZip)));
}
}
/**
* If the file is a simple text file, this function is your best option. It returns
* the contents of the file as a string.
* @return
* @throws FileNotFoundException If the file is not found
* @throws IOException If you specify a file that isn't a zip file as if it were a folder
*/
public String getFileContents() throws FileNotFoundException, IOException {
if (!isZipped) {
return FileUtil.read(file);
} else {
return StreamUtils.GetString(getInputStream());
}
}
/**
* Delegates the equals check to the underlying File object.
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final ZipReader other = (ZipReader) obj;
return other.file.equals(this.file);
}
/**
* Delegates the hashCode to the underlying File object.
* @return
*/
@Override
public int hashCode() {
return file.hashCode();
}
@Override
public String toString() {
return file.toString();
}
public File getFile(){
return file;
}
private void initList() throws IOException{
if(!isZipped){
return;
}
if(this.zipEntries == null){
zipEntries = new ArrayList<File>();
ZipInputStream zis = new ZipInputStream(new FileInputStream(topZip));
ZipEntry entry;
while((entry = zis.getNextEntry()) != null){
File f = new File(topZip, entry.getName());
zipEntries.add(f);
if(entry.isDirectory()){
zipDirectories.add(f);
}
}
zis.close();
}
}
public boolean isDirectory() throws IOException{
if(!isZipped){
return file.isDirectory();
} else {
initList();
return zipDirectories.contains(file);
}
}
public String getName(){
return file.getName();
}
/**
* Returns a list of File objects that are subfiles or directories in
* this directory.
* @return
* @throws IOException
*/
public File [] listFiles() throws IOException{
if(!isZipped){
return file.listFiles();
} else {
StringUtils.Join(new String[]{}, "");
initList();
List<File> files = new ArrayList<File>();
for(File f : zipEntries){
//If the paths start with the same thing...
if(f.getPath().startsWith(file.getPath())){
//...and it's not the file we're looking from to begin with...
if(!file.equals(f)){
//...and it's not in a sub-sub folder of this file...
if(!f.getPath().matches(Pattern.quote(file.getPath() + File.separatorChar) + "[^" + Pattern.quote(File.separator) + "]*" + Pattern.quote(File.separator) + ".*")){
//...add it to the list.
String root = f.getPath().replaceFirst(Pattern.quote(file.getPath() + File.separator), "");
f = new File(root);
files.add(f);
}
}
}
}
return ArrayUtils.asArray(File.class, files);
}
}
public ZipReader[] zipListFiles() throws IOException{
File[] ret = listFiles();
ZipReader[] zips = new ZipReader[ret.length];
for(int i = 0; i < ret.length; i++){
zips[i] = new ZipReader(new File(file, ret[i].getPath()));
}
return zips;
}
/**
* Copies all the files from this directory to the source directory.
* If create is false, and the folder doesn't already exist, and IOException
* will be thrown. This is similar to an "unzip" operation.
* @param dstFolder
*/
public void recursiveCopy(File dstFolder, boolean create) throws IOException{
if(create){
dstFolder.mkdirs();
}
if(!dstFolder.isDirectory()){
throw new IOException("Destination folder is not a directory!");
}
for(ZipReader r : zipListFiles()){
if(r.isDirectory()){
r.recursiveCopy(dstFolder, create);
} else {
File newFile = new File(dstFolder, r.file.getName());
newFile.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(newFile, false);
StreamUtils.Copy(r.getInputStream(), fos);
}
}
}
}