package com.openfarmanager.android.core.archive;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.webkit.MimeTypeMap;
import com.github.junrar.Archive;
import com.openfarmanager.android.App;
import com.openfarmanager.android.R;
import com.openfarmanager.android.filesystem.actions.CopyTask;
import com.openfarmanager.android.googledrive.model.exceptions.CreateFolderException;
import com.openfarmanager.android.model.exeptions.CreateArchiveException;
import com.openfarmanager.android.model.exeptions.SdcardPermissionException;
import com.openfarmanager.android.utils.FileUtilsExt;
import com.openfarmanager.android.utils.StorageUtils;
import com.openfarmanager.android.utils.SystemUtils;
import net.lingala.zip4j.model.FileHeader;
import org.apache.commons.compress.archivers.*;
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.compressors.CompressorException;
import org.apache.commons.compress.compressors.CompressorOutputStream;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import static com.openfarmanager.android.core.archive.MimeTypes.*;
import static com.openfarmanager.android.utils.StorageUtils.checkForPermissionAndGetBaseUri;
import static com.openfarmanager.android.utils.StorageUtils.checkUseStorageApi;
import static com.openfarmanager.android.utils.StorageUtils.getStorageOutputFileStream;
import static com.openfarmanager.android.utils.StorageUtils.mkDir;
public class ArchiveUtils {
private static final String TAG = "ArchiveUtils";
public enum ArchiveType {
zip, tar, ar, jar, cpio, rar, x7z;
public static ArchiveType getType(String mime) {
if (MIME_APPLICATION_ZIP.equals(mime)) {
return zip;
} else if (MIME_APPLICATION_X_TAR.equals(mime)) {
return tar;
} else if (MIME_APPLICATION_X_AR.equals(mime)) {
return ar;
} else if (MIME_APPLICATION_JAVA_ARCHIVE.equals(mime)) {
return jar;
} else if (MIME_APPLICATION_X_CPIO.equals(mime)) {
return cpio;
} else if (MIME_APPLICATION_X_RAR_COMPRESSED.equals(mime)) {
return rar;
} else if (MIME_APPLICATION_7Z.equals(mime)) {
return x7z;
}
return null;
}
}
public enum CompressionEnum {
gzip, bzip2, xz, pack200;
public static CompressionEnum getCompression(String mime) {
if (mime.equals(MimeTypes.MIME_APPLICATION_X_GZIP) || mime.equals(MimeTypes.MIME_APPLICATION_TGZ)) {
return gzip;
} else if (mime.equals(MimeTypes.MIME_APPLICATION_X_XZ)) {
return xz;
} else if (mime.equals(MimeTypes.MIME_APPLICATION_X_BZIP2)) {
return bzip2;
} else if (mime.equals(MimeTypes.MIME_APPLICATION_X_PACK200)) {
return pack200;
}
return null;
}
public static String toString(CompressionEnum type) {
switch (type) {
case gzip: return CompressorStreamFactory.GZIP;
case xz: return CompressorStreamFactory.XZ;
case bzip2: return CompressorStreamFactory.BZIP2;
case pack200: return CompressorStreamFactory.PACK200;
default: return "";
}
}
}
public static String getMimeType(File file) {
String extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(file).toString());
return MimeTypes.getMimeType(extension);
}
public static boolean isCompressionSupported(String mime) {
return CompressionEnum.getCompression(mime) != null;
}
public static boolean isCompressionSupported(File file) {
return CompressionEnum.getCompression(getMimeType(file)) != null;
}
public static boolean isArchiveSupported(String mime) {
return ArchiveType.getType(mime) != null;
}
public static boolean isArchiveSupported(File file) {
return ArchiveType.getType(getMimeType(file)) != null;
}
public static boolean isRarArchive(File file) {
return ArchiveType.getType(getMimeType(file)) == ArchiveType.rar;
}
public static boolean is7zArchive(File file) {
return ArchiveType.getType(getMimeType(file)) == ArchiveType.x7z;
}
public static boolean isZipArchive(File file) {
return ArchiveType.getType(getMimeType(file)) == ArchiveType.zip;
}
public static ArchiveInputStream createInputStream(InputStream stream) {
try {
return new ArchiveStreamFactory()
.createArchiveInputStream(new BufferedInputStream(stream));
} catch (ArchiveException e) {
return null;
}
}
public static boolean isArchiveFile(File file) {
return isArchiveSupported(file) || isCompressionSupported(file);
}
/**
* Extract archive files, which represented by <code>extractFileTree</code> -
* top node of archive files, which must be extracted. Note, that it's not required to extract files
* from root note - we can specify certain files by extractFileTree.
*
* How it works: while iteration through every archive entry in <code>inputFile</code>
* we get stream of every required for extraction item (from <code>extractFileTree</code>) and redirect it to output.
*
* @param inputFile input archive file (which files need to be extracted).
* @param outputDir directory where files need to be copied.
* @param extractFileTree tree representation of files, which need to be extracted.
* @param isCompressed is original archive compressed by additional compression (such as gzip or xz).
* @param encryptedZipPassword special case for encrypted zip files.
* If param provided - try to use password for zip encryption.
* Should be null for not encrypted archives.
* @param listener listener to notify upper layer about key events while extract operation.
*
* @throws IOException in case of problem with IO operations.
* @throws ArchiveException in case of problem with archive operation.
* @throws FileNotFoundException in case when output directory(ies) for extracted files doesn't exist and can't be created.
* @throws CompressorException in case when archive is compressed and some errors took place during creation of input stream.
*/
public static void extractArchive(final File inputFile, final File outputDir,
ArchiveScanner.File extractFileTree, boolean isCompressed, String encryptedZipPassword, ExtractArchiveListener listener)
throws IOException, ArchiveException, CompressorException {
String sdCardPath = SystemUtils.getExternalStorage(outputDir.getAbsolutePath());
boolean useStorageApi = checkUseStorageApi(sdCardPath);
Uri baseUri = null;
if (useStorageApi) {
baseUri = checkForPermissionAndGetBaseUri();
}
// if output directory doesn't exist and can't be created
if (!outputDir.exists()) {
boolean directoryCreated = createDirectory(outputDir, sdCardPath, useStorageApi, baseUri);
if (!directoryCreated) {
//throw new FileNotFoundException(App.sInstance.getResources().getString(R.string.error_output_directory_doesnt_exists));
throw new CreateFolderException();
}
}
if (encryptedZipPassword != null && isZipArchive(inputFile)) {
try {
net.lingala.zip4j.core.ZipFile zipFile = new net.lingala.zip4j.core.ZipFile(inputFile);
if (listener != null) {
listener.beforeExtractStarted(zipFile.getFileHeaders().size());
}
if (zipFile.isEncrypted()) {
zipFile.setPassword(encryptedZipPassword);
}
// Get the list of file headers from the zip file
List fileHeaderList = zipFile.getFileHeaders();
// Loop through the file headers
for (Object aFileHeaderList : fileHeaderList) {
FileHeader fileHeader = (FileHeader) aFileHeaderList;
// try to find current archive entry in list of files to extraction
ArchiveScanner.File file = extractFileTree.findFile(fileHeader.getFileName());
// if current entry shouldn't be extracted - goto next
if (file == null) {
continue;
}
// Extract the file to the specified destination
zipFile.extractFile(fileHeader, outputDir.getAbsolutePath());
if (listener != null) {
listener.onFileExtracted(null);
}
}
} catch (Exception e) {
throw new ArchiveException("error extracting encrypted zip");
}
return;
}
if (isRarArchive(inputFile)) {
Archive arch;
try {
arch = new Archive(inputFile);
} catch (Exception e) {
e.printStackTrace();
return;
}
if (listener != null) {
listener.beforeExtractStarted(arch.getFileHeaders().size());
}
List<com.github.junrar.rarfile.FileHeader> fileHeaderList = arch.getFileHeaders();
for (com.github.junrar.rarfile.FileHeader fileHeader : fileHeaderList) {
if (fileHeader.isEncrypted()) {
}
// try to find current archive entry in list of files to extraction
ArchiveScanner.File file = extractFileTree.findFile(fileHeader.getFileNameString());
// if current entry shouldn't be extracted - goto next
if (file == null || file.isDirectory()) {
continue;
}
try {
String outputPath = adjustExtractDirectory(file, extractFileTree, baseUri, sdCardPath, outputDir, useStorageApi);
File outputFile = new File(outputPath);
OutputStream stream = getArchiveOutputStream(sdCardPath, useStorageApi, baseUri, outputFile);
arch.extractFile(fileHeader, stream);
stream.close();
} catch (Exception e) {
e.printStackTrace();
}
if (listener != null) {
listener.onFileExtracted(null);
}
}
return;
}
if (is7zArchive(inputFile)) {
SevenZFile sevenZFile = new SevenZFile(inputFile, encryptedZipPassword == null ? null : encryptedZipPassword.getBytes());
if (listener != null) {
listener.beforeExtractStarted(extractFileTree.countFiles());
}
SevenZArchiveEntry entry = sevenZFile.getNextEntry();
while (entry != null) {
byte[] content = new byte[(int) entry.getSize()];
sevenZFile.read(content, 0, content.length);
ArchiveScanner.File file = extractFileTree.findFile(entry.getName());
if (file != null && !file.isDirectory()) {
String outputPath = adjustExtractDirectory(file, extractFileTree, baseUri, sdCardPath, outputDir, useStorageApi);
OutputStream stream = getArchiveOutputStream(sdCardPath, useStorageApi, baseUri, new File(outputPath));
IOUtils.write(content, stream);
stream.close();
}
entry = sevenZFile.getNextEntry();
if (listener != null) {
listener.onFileExtracted(file);
}
}
sevenZFile.close();
return;
}
if (listener != null) {
listener.beforeExtractStarted(extractFileTree.countFiles());
}
// get input stream from archive file
final ArchiveInputStream archiveInputStream = createInputStream(isCompressed ?
new CompressorStreamFactory().createCompressorInputStream(new BufferedInputStream(new FileInputStream(inputFile))) :
new FileInputStream(inputFile));
ArchiveEntry entry;
while ((entry = archiveInputStream.getNextEntry()) != null) {
String fullPath = entry.getName();
// try to find current archive entry in list of files to extraction
ArchiveScanner.File file = extractFileTree.findFile(fullPath);
// if current entry shouldn't be extracted - goto next
if (file == null) {
continue;
}
String internalPath = File.separator + file.getFullDirectoryPath();
if (!extractFileTree.isRoot()) {
if (!extractFileTree.isDirectory()) { // single file.
internalPath = "";
} else { // find sub path within current working directory.
internalPath = File.separator + extractFileTree.getSubDirectoryPath(file);
}
}
String directoryPath = outputDir + internalPath;
final File outputFile = new File(directoryPath, file.getName());
File directory = new File(directoryPath);
if (!directory.exists() && !createDirectory(directory, sdCardPath, useStorageApi, baseUri)) {
throw new FileNotFoundException(App.sInstance.getResources().getString(R.string.error_output_directory_doesnt_exists));
}
//final OutputStream outputFileStream = new FileOutputStream(outputFile);
OutputStream outputFileStream = getArchiveOutputStream(sdCardPath, useStorageApi, baseUri, outputFile);
IOUtils.copy(archiveInputStream, outputFileStream);
outputFileStream.close();
if (listener != null) {
listener.onFileExtracted(file);
}
}
archiveInputStream.close();
}
private static boolean createDirectory(File outputDir, String sdCardPath, boolean useStorageApi, Uri baseUri) {
return useStorageApi ? StorageUtils.mkDir(baseUri, sdCardPath, outputDir) : outputDir.mkdirs();
}
private static OutputStream getArchiveOutputStream(String sdCardPath, boolean useStorageApi, Uri baseUri, File outputFile) throws FileNotFoundException {
return useStorageApi ? getStorageOutputFileStream(outputFile, baseUri, sdCardPath) :
new FileOutputStream(outputFile);
}
private static String adjustExtractDirectory(ArchiveScanner.File file, ArchiveScanner.File extractFileTree,
Uri baseUri, String sdCardPath, File outputDir, boolean useStorageApi) {
String internalPath = File.separator + file.getFullDirectoryPath();
if (!extractFileTree.isRoot()) {
if (!extractFileTree.isDirectory()) { // single file.
internalPath = "";
} else { // find sub path within current working directory.
internalPath = File.separator + extractFileTree.getSubDirectoryPath(file);
}
}
String directoryPath = outputDir + internalPath;
final File outputFile = new File(directoryPath, file.getName());
File directory = new File(directoryPath);
if (!directory.exists()) {
createDirectory(directory, sdCardPath, useStorageApi, baseUri);
}
return outputFile.getPath();
}
/**
* Create archive <code>outputFile</code> from <code>inputFiles</code>.
*
* @param inputFiles files to be added to archive.
* @param outputFile target archive file.
* @param targetArchiveType target type for archive file.
* @param additionalCompression additional compression over the original archivation.
* Usually used for non compressed archives, such as <code>ArchiveType.tar</code> or <code>ArchiveType.ar</code>.
* @param compressionForZip do we need to compress <code>zip</code> archive. Actual only for <code>ArchiveType.zip</code>
* @param listener listener to notify upper layer about key events while adding items to archive.
*
* @throws ArchiveException
* @throws CompressorException
* @throws IOException
* @throws com.openfarmanager.android.model.exeptions.CreateArchiveException
*
* @see ArchiveType
* @see CompressionEnum
*/
public static void addToArchive(final List<File> inputFiles, String outputFile,
ArchiveType targetArchiveType, CompressionEnum additionalCompression,
boolean compressionForZip, AddToArchiveListener listener)
throws ArchiveException, CompressorException, IOException, CreateArchiveException {
String sdCardPath = SystemUtils.getExternalStorage(outputFile);
boolean checkUseStorageApi = checkUseStorageApi(sdCardPath);
File output = new File(outputFile + "." + targetArchiveType.name());
if (!output.exists() && (checkUseStorageApi ? !StorageUtils.createNewFile(output, sdCardPath) : !output.createNewFile())) {
throw new CreateArchiveException();
}
if (listener != null) {
listener.beforeStarted(FileUtilsExt.getFilesCount(inputFiles));
}
OutputStream out;
// output file stream
out = getOutputStream(output, sdCardPath, checkUseStorageApi);
ArchiveOutputStream outputStream = new ArchiveStreamFactory().createArchiveOutputStream(targetArchiveType.name(), out);
addFilesToArchiveStream(inputFiles, "", targetArchiveType, compressionForZip, outputStream, listener);
outputStream.finish();
outputStream.flush();
outputStream.close();
if (additionalCompression == null) {
return;
}
File finalOutputFile = new File(output.getAbsolutePath() + "." + CompressionEnum.toString(additionalCompression));
if (!finalOutputFile.exists() && (checkUseStorageApi ? !StorageUtils.createNewFile(output, sdCardPath) : !finalOutputFile.createNewFile())) {
throw new CreateArchiveException();
}
OutputStream outCompressed = getOutputStream(finalOutputFile, sdCardPath, checkUseStorageApi);
CompressorOutputStream stream = new CompressorStreamFactory().
createCompressorOutputStream(CompressionEnum.toString(additionalCompression), outCompressed);
// manual copying to provide feedback about progress
int bufferSize = 16384;
InputStream input = new FileInputStream(output);
if (listener != null) {
listener.beforeCompressionStarted((int) output.getAbsoluteFile().length() / bufferSize);
}
final byte[] buffer = new byte[bufferSize];
int n;
while (-1 != (n = input.read(buffer))) {
stream.write(buffer, 0, n);
if (listener != null) {
listener.onFileAdded(null);
}
}
stream.flush();
stream.close();
if (checkUseStorageApi) {
Uri uri = StorageUtils.getDestinationFileUri(checkForPermissionAndGetBaseUri(), sdCardPath, output.getAbsolutePath());
DocumentsContract.deleteDocument(App.sInstance.getContentResolver(), uri);
} else {
output.delete();
}
}
/**
* Create archive output stream to support external storage api.
*
* @param output target file
* @param sdCardPath already extracted sdCard path
* @param checkUseStorageApi <code>true</code> if use storage api, <code>false</code> otherwise
*
* @return output stream to save file
*/
private static OutputStream getOutputStream(File output, String sdCardPath, boolean checkUseStorageApi) throws FileNotFoundException {
OutputStream out;
if (checkUseStorageApi) {
checkForPermissionAndGetBaseUri();
out = StorageUtils.getStorageOutputFileStream(output, sdCardPath);
} else {
out = new FileOutputStream((output));
}
return out;
}
/**
* Add <code>inputFiles</code> to archive output stream (keeping files tree).
* This code extracted to separate method for recursive calls.
*
* @param inputFiles files to be added to archive stream.
* @param parentPath parent path relatively to current files. used for keeping files tree.
* @param targetArchiveType target type for archive file.
* @param compressionForZip do we need to compress <code>zip</code> archive. Actual only for <code>ArchiveType.zip</code>.
* @param outputStream target archive output stream.
* @param listener listener to notify upper layer about key events while adding items to archive.
*
* @throws IOException
*/
private static void addFilesToArchiveStream(List<File> inputFiles, String parentPath, ArchiveType targetArchiveType,
boolean compressionForZip, ArchiveOutputStream outputStream,
AddToArchiveListener listener) throws IOException {
for (File file : inputFiles) {
if (file.isDirectory()) {
addFilesToArchiveStream(new ArrayList<File>(Arrays.asList(file.listFiles())), parentPath + file.getName() + "/" ,
targetArchiveType, compressionForZip, outputStream, listener);
continue;
}
ArchiveEntry entry = outputStream.createArchiveEntry(file, parentPath + file.getName());
if (targetArchiveType == ArchiveType.zip) {
ZipArchiveEntry zipArchiveEntry = (ZipArchiveEntry) entry;
zipArchiveEntry.setSize(file.length());
zipArchiveEntry.setCompressedSize(file.length());
zipArchiveEntry.setCrc(FileUtils.checksumCRC32(file.getAbsoluteFile()));
zipArchiveEntry.setMethod(compressionForZip ? ZipEntry.DEFLATED : ZipEntry.STORED);
}
outputStream.putArchiveEntry(entry);
IOUtils.copy(new FileInputStream(file), outputStream);
outputStream.closeArchiveEntry();
if (listener != null) {
listener.onFileAdded(file);
}
}
}
public static interface ExtractArchiveListener {
void beforeExtractStarted(int filesToExtract);
void onFileExtracted(ArchiveScanner.File extractedFile);
}
public static interface AddToArchiveListener {
void beforeStarted(int filesToArchive);
void beforeCompressionStarted(int fileParts);
void onFileAdded(File file);
}
}